Skip to main content

adk_payments/journal/
evidence_store.rs

1use 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
11/// Evidence store backed by `adk-artifact`.
12pub struct ArtifactBackedEvidenceStore {
13    artifact_service: Arc<dyn ArtifactService>,
14}
15
16impl ArtifactBackedEvidenceStore {
17    /// Creates an artifact-backed evidence store.
18    #[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}