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                },
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}