adk_payments/journal/
evidence_store.rs1use std::sync::Arc;
2
3use adk_artifact::{ArtifactService, LoadRequest, SaveRequest};
4use adk_core::{AdkError, ErrorCategory, ErrorComponent, MAX_INLINE_DATA_SIZE, Part, Result};
5use async_trait::async_trait;
6use sha2::{Digest, Sha256};
7
8use crate::kernel::commands::{EvidenceLookup, StoreEvidenceCommand, StoredEvidence};
9use crate::kernel::service::EvidenceStore;
10
11pub struct ArtifactBackedEvidenceStore {
13 artifact_service: Arc<dyn ArtifactService>,
14}
15
16impl ArtifactBackedEvidenceStore {
17 #[must_use]
19 pub fn new(artifact_service: Arc<dyn ArtifactService>) -> Self {
20 Self { artifact_service }
21 }
22
23 fn require_identity<'a>(
24 session_identity: &'a Option<adk_core::identity::AdkIdentity>,
25 code: &'static str,
26 ) -> Result<&'a adk_core::identity::AdkIdentity> {
27 session_identity.as_ref().ok_or_else(|| {
28 AdkError::new(
29 ErrorComponent::Artifact,
30 ErrorCategory::InvalidInput,
31 code,
32 "evidence storage requires a session identity",
33 )
34 })
35 }
36
37 fn artifact_key(command: &StoreEvidenceCommand) -> String {
38 let reference_hash = hash_text(&command.evidence_ref.evidence_id);
39 format!(
40 "payments:evidence:{}:{}:{}:{}",
41 command.transaction_id.as_str(),
42 command.evidence_ref.protocol.name,
43 command.evidence_ref.artifact_kind,
44 reference_hash
45 )
46 }
47}
48
49#[async_trait]
50impl EvidenceStore for ArtifactBackedEvidenceStore {
51 async fn store(&self, command: StoreEvidenceCommand) -> Result<StoredEvidence> {
52 if command.body.len() > MAX_INLINE_DATA_SIZE {
53 return Err(AdkError::new(
54 ErrorComponent::Artifact,
55 ErrorCategory::InvalidInput,
56 "payments.evidence.too_large",
57 format!(
58 "evidence payload exceeds inline artifact size limit of {} bytes",
59 MAX_INLINE_DATA_SIZE
60 ),
61 ));
62 }
63
64 let identity = Self::require_identity(
65 &command.session_identity,
66 "payments.evidence.identity_required",
67 )?;
68 let artifact_key = Self::artifact_key(&command);
69 self.artifact_service
70 .save(SaveRequest {
71 app_name: identity.app_name.as_ref().to_string(),
72 user_id: identity.user_id.as_ref().to_string(),
73 session_id: identity.session_id.as_ref().to_string(),
74 file_name: artifact_key.clone(),
75 part: Part::InlineData {
76 mime_type: command.content_type.clone(),
77 data: command.body.clone(),
78 },
79 version: None,
80 })
81 .await?;
82
83 let mut evidence_ref = command.evidence_ref;
84 evidence_ref.evidence_id = artifact_key;
85 evidence_ref.digest = Some(format!("sha256:{}", hash_bytes(&command.body)));
86
87 Ok(StoredEvidence { evidence_ref, body: command.body, content_type: command.content_type })
88 }
89
90 async fn load(&self, lookup: EvidenceLookup) -> Result<Option<StoredEvidence>> {
91 let identity = Self::require_identity(
92 &lookup.session_identity,
93 "payments.evidence.identity_required",
94 )?;
95 let response = self
96 .artifact_service
97 .load(LoadRequest {
98 app_name: identity.app_name.as_ref().to_string(),
99 user_id: identity.user_id.as_ref().to_string(),
100 session_id: identity.session_id.as_ref().to_string(),
101 file_name: lookup.evidence_ref.evidence_id.clone(),
102 version: None,
103 })
104 .await;
105
106 match response {
107 Ok(response) => {
108 let Part::InlineData { mime_type, data } = response.part else {
109 return Err(AdkError::new(
110 ErrorComponent::Artifact,
111 ErrorCategory::Internal,
112 "payments.evidence.unsupported_part",
113 "stored payment evidence must use inline artifact data",
114 ));
115 };
116
117 Ok(Some(StoredEvidence {
118 evidence_ref: lookup.evidence_ref,
119 body: data,
120 content_type: mime_type,
121 }))
122 }
123 Err(err) if err.is_not_found() => Ok(None),
124 Err(err) => Err(err),
125 }
126 }
127}
128
129fn hash_bytes(bytes: &[u8]) -> String {
130 hex::encode(Sha256::digest(bytes))
131}
132
133fn hash_text(text: &str) -> String {
134 hash_bytes(text.as_bytes())[..16].to_string()
135}