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 uri: None,
79 annotations: None,
80 },
81 version: None,
82 })
83 .await?;
84
85 let mut evidence_ref = command.evidence_ref;
86 evidence_ref.evidence_id = artifact_key;
87 evidence_ref.digest = Some(format!("sha256:{}", hash_bytes(&command.body)));
88
89 Ok(StoredEvidence { evidence_ref, body: command.body, content_type: command.content_type })
90 }
91
92 async fn load(&self, lookup: EvidenceLookup) -> Result<Option<StoredEvidence>> {
93 let identity = Self::require_identity(
94 &lookup.session_identity,
95 "payments.evidence.identity_required",
96 )?;
97 let response = self
98 .artifact_service
99 .load(LoadRequest {
100 app_name: identity.app_name.as_ref().to_string(),
101 user_id: identity.user_id.as_ref().to_string(),
102 session_id: identity.session_id.as_ref().to_string(),
103 file_name: lookup.evidence_ref.evidence_id.clone(),
104 version: None,
105 })
106 .await;
107
108 match response {
109 Ok(response) => {
110 let Part::InlineData { mime_type, data, .. } = response.part else {
111 return Err(AdkError::new(
112 ErrorComponent::Artifact,
113 ErrorCategory::Internal,
114 "payments.evidence.unsupported_part",
115 "stored payment evidence must use inline artifact data",
116 ));
117 };
118
119 Ok(Some(StoredEvidence {
120 evidence_ref: lookup.evidence_ref,
121 body: data,
122 content_type: mime_type,
123 }))
124 }
125 Err(err) if err.is_not_found() => Ok(None),
126 Err(err) => Err(err),
127 }
128 }
129}
130
131fn hash_bytes(bytes: &[u8]) -> String {
132 hex::encode(Sha256::digest(bytes))
133}
134
135fn hash_text(text: &str) -> String {
136 hash_bytes(text.as_bytes())[..16].to_string()
137}