Skip to main content

a1/
context.rs

1use std::sync::Arc;
2
3use ed25519_dalek::VerifyingKey;
4
5use crate::audit::{AuditSink, NoopAuditSink};
6use crate::chain::{AuthorizedAction, BatchAuthorizeResult, Clock, DyoloChain, SystemClock};
7use crate::error::A1Error;
8use crate::intent::{IntentHash, MerkleProof};
9use crate::policy::PolicySet;
10use crate::registry::{MemoryNonceStore, MemoryRevocationStore, NonceStore, RevocationStore};
11
12// ── Sync context ──────────────────────────────────────────────────────────────
13
14/// A wiring context that holds all runtime dependencies required for chain
15/// authorization.
16///
17/// `A1Context` is the recommended entry point for applications that do not
18/// need fine-grained control over each authorization call. Configure once at
19/// startup, share across threads via `Arc<A1Context>`, and call `authorize`.
20///
21/// # Example
22///
23/// ```rust,ignore
24/// use a1::context::A1Context;
25/// use a1::{DyoloChain, intent::{Intent, MerkleProof}};
26///
27/// let ctx = A1Context::builder().build();
28///
29/// let action = ctx.authorize(&chain, &agent_pk, &intent_hash, &proof)?;
30/// println!("authorized depth={}", action.receipt().chain_depth);
31/// ```
32pub struct A1Context {
33    pub revocation: Arc<dyn RevocationStore>,
34    pub nonces: Arc<dyn NonceStore>,
35    pub clock: Arc<dyn Clock + Send + Sync>,
36    pub policy: Option<PolicySet>,
37    pub audit: Arc<dyn AuditSink>,
38    pub namespace: Option<String>,
39}
40
41impl A1Context {
42    pub fn builder() -> A1ContextBuilder {
43        A1ContextBuilder::default()
44    }
45
46    pub fn authorize(
47        &self,
48        chain: &DyoloChain,
49        agent_pk: &VerifyingKey,
50        intent: &IntentHash,
51        proof: &MerkleProof,
52    ) -> Result<AuthorizedAction, A1Error> {
53        chain.authorize_with_options(
54            agent_pk,
55            intent,
56            proof,
57            self.clock.as_ref(),
58            self.revocation.as_ref(),
59            self.nonces.as_ref(),
60            self.policy.as_ref(),
61            self.audit.as_ref(),
62        )
63    }
64
65    pub fn authorize_batch(
66        &self,
67        chain: &DyoloChain,
68        agent_pk: &VerifyingKey,
69        intents: &[(IntentHash, MerkleProof)],
70    ) -> BatchAuthorizeResult {
71        chain.authorize_batch(
72            agent_pk,
73            intents,
74            self.clock.as_ref(),
75            self.revocation.as_ref(),
76            self.nonces.as_ref(),
77        )
78    }
79
80    /// Probe both storage backends. Returns `Err` if either is unhealthy.
81    ///
82    /// Call this from your process health endpoint so load balancers can drain
83    /// a replica before its backing store degrades authorization decisions.
84    pub fn health_check(&self) -> Result<(), A1Error> {
85        self.revocation
86            .health_check()
87            .map_err(|e| A1Error::StorageUnhealthy(format!("revocation: {e}")))?;
88        self.nonces
89            .health_check()
90            .map_err(|e| A1Error::StorageUnhealthy(format!("nonces: {e}")))?;
91        Ok(())
92    }
93}
94
95// ── Async context ─────────────────────────────────────────────────────────────
96
97#[cfg(feature = "async")]
98pub struct AsyncA1Context {
99    pub revocation: Arc<dyn crate::registry::r#async::AsyncRevocationStore>,
100    pub nonces: Arc<dyn crate::registry::r#async::AsyncNonceStore>,
101    pub clock: Arc<dyn Clock + Send + Sync>,
102    pub policy: Option<PolicySet>,
103    pub audit: Arc<dyn AuditSink>,
104    pub namespace: Option<String>,
105}
106
107#[cfg(feature = "async")]
108impl AsyncA1Context {
109    pub fn builder() -> AsyncA1ContextBuilder {
110        AsyncA1ContextBuilder::default()
111    }
112
113    pub async fn authorize(
114        &self,
115        chain: &DyoloChain,
116        agent_pk: &VerifyingKey,
117        intent: &IntentHash,
118        proof: &MerkleProof,
119    ) -> Result<AuthorizedAction, A1Error> {
120        chain
121            .authorize_async_with_options(
122                agent_pk,
123                intent,
124                proof,
125                self.clock.as_ref(),
126                self.revocation.as_ref(),
127                self.nonces.as_ref(),
128                self.policy.as_ref(),
129                self.audit.as_ref(),
130            )
131            .await
132    }
133
134    pub async fn authorize_batch(
135        &self,
136        chain: &DyoloChain,
137        agent_pk: &VerifyingKey,
138        intents: &[(IntentHash, MerkleProof)],
139    ) -> BatchAuthorizeResult {
140        chain
141            .authorize_batch_async(
142                agent_pk,
143                intents,
144                self.clock.as_ref(),
145                self.revocation.as_ref(),
146                self.nonces.as_ref(),
147            )
148            .await
149    }
150
151    pub async fn health_check(&self) -> Result<(), A1Error> {
152        self.revocation
153            .health_check()
154            .await
155            .map_err(|e| A1Error::StorageUnhealthy(format!("revocation: {e}")))?;
156        self.nonces
157            .health_check()
158            .await
159            .map_err(|e| A1Error::StorageUnhealthy(format!("nonces: {e}")))?;
160        Ok(())
161    }
162}
163
164// ── A1ContextBuilder ─────────────────────────────────────────────────────────
165
166#[derive(Default)]
167pub struct A1ContextBuilder {
168    revocation: Option<Arc<dyn RevocationStore>>,
169    nonces: Option<Arc<dyn NonceStore>>,
170    clock: Option<Arc<dyn Clock + Send + Sync>>,
171    policy: Option<PolicySet>,
172    audit: Option<Arc<dyn AuditSink>>,
173    namespace: Option<String>,
174}
175
176impl A1ContextBuilder {
177    pub fn revocation(mut self, store: impl RevocationStore + 'static) -> Self {
178        self.revocation = Some(Arc::new(store));
179        self
180    }
181
182    pub fn nonces(mut self, store: impl NonceStore + 'static) -> Self {
183        self.nonces = Some(Arc::new(store));
184        self
185    }
186
187    pub fn clock(mut self, clock: impl Clock + Send + Sync + 'static) -> Self {
188        self.clock = Some(Arc::new(clock));
189        self
190    }
191
192    pub fn policy(mut self, policy: PolicySet) -> Self {
193        self.policy = Some(policy);
194        self
195    }
196
197    pub fn audit(mut self, sink: impl AuditSink + 'static) -> Self {
198        self.audit = Some(Arc::new(sink));
199        self
200    }
201
202    pub fn namespace(mut self, ns: impl Into<String>) -> Self {
203        self.namespace = Some(ns.into());
204        self
205    }
206
207    pub fn build(self) -> A1Context {
208        A1Context {
209            revocation: self
210                .revocation
211                .unwrap_or_else(|| Arc::new(MemoryRevocationStore::new())),
212            nonces: self
213                .nonces
214                .unwrap_or_else(|| Arc::new(MemoryNonceStore::new())),
215            clock: self.clock.unwrap_or_else(|| Arc::new(SystemClock)),
216            policy: self.policy,
217            audit: self.audit.unwrap_or_else(|| Arc::new(NoopAuditSink)),
218            namespace: self.namespace,
219        }
220    }
221}
222
223// ── AsyncA1ContextBuilder ────────────────────────────────────────────────────
224
225#[cfg(feature = "async")]
226#[derive(Default)]
227pub struct AsyncA1ContextBuilder {
228    revocation: Option<Arc<dyn crate::registry::r#async::AsyncRevocationStore>>,
229    nonces: Option<Arc<dyn crate::registry::r#async::AsyncNonceStore>>,
230    clock: Option<Arc<dyn Clock + Send + Sync>>,
231    policy: Option<PolicySet>,
232    audit: Option<Arc<dyn AuditSink>>,
233    namespace: Option<String>,
234}
235
236#[cfg(feature = "async")]
237impl AsyncA1ContextBuilder {
238    pub fn revocation(
239        mut self,
240        store: impl crate::registry::r#async::AsyncRevocationStore + 'static,
241    ) -> Self {
242        self.revocation = Some(Arc::new(store));
243        self
244    }
245
246    pub fn nonces(
247        mut self,
248        store: impl crate::registry::r#async::AsyncNonceStore + 'static,
249    ) -> Self {
250        self.nonces = Some(Arc::new(store));
251        self
252    }
253
254    pub fn clock(mut self, clock: impl Clock + Send + Sync + 'static) -> Self {
255        self.clock = Some(Arc::new(clock));
256        self
257    }
258
259    pub fn policy(mut self, policy: PolicySet) -> Self {
260        self.policy = Some(policy);
261        self
262    }
263
264    pub fn audit(mut self, sink: impl AuditSink + 'static) -> Self {
265        self.audit = Some(Arc::new(sink));
266        self
267    }
268
269    pub fn namespace(mut self, ns: impl Into<String>) -> Self {
270        self.namespace = Some(ns.into());
271        self
272    }
273
274    pub fn build(self) -> AsyncA1Context {
275        use crate::registry::r#async::{SyncNonceAdapter, SyncRevocationAdapter};
276
277        AsyncA1Context {
278            revocation: self.revocation.unwrap_or_else(|| {
279                Arc::new(SyncRevocationAdapter(
280                    Arc::new(MemoryRevocationStore::new()),
281                ))
282            }),
283            nonces: self
284                .nonces
285                .unwrap_or_else(|| Arc::new(SyncNonceAdapter(Arc::new(MemoryNonceStore::new())))),
286            clock: self.clock.unwrap_or_else(|| Arc::new(SystemClock)),
287            policy: self.policy,
288            audit: self.audit.unwrap_or_else(|| Arc::new(NoopAuditSink)),
289            namespace: self.namespace,
290        }
291    }
292}
293
294// ── Tests ─────────────────────────────────────────────────────────────────────
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    #[allow(deprecated)]
300    use crate::{
301        cert::CertBuilder,
302        identity::DyoloIdentity,
303        intent::{intent_hash, IntentTree},
304    };
305
306    #[test]
307    #[allow(deprecated)]
308    fn context_builder_defaults_and_authorizes() {
309        let ctx = A1Context::builder().build();
310        let human = DyoloIdentity::generate();
311        let agent = DyoloIdentity::generate();
312        let trade = intent_hash("trade.equity", b"");
313        let tree = IntentTree::build(vec![trade]).unwrap();
314        let root = tree.root();
315        let now = SystemClock.unix_now();
316        let cert =
317            CertBuilder::new(agent.verifying_key(), root, now, now + 86400 * 365).sign(&human);
318
319        let mut chain = DyoloChain::new(human.verifying_key(), root).with_drift_tolerance(9999999);
320        chain.push(cert);
321
322        let proof = tree.prove(&trade).unwrap();
323        let result = ctx.authorize(&chain, &agent.verifying_key(), &trade, &proof);
324        assert!(
325            result.is_ok(),
326            "context builder authorization failed: {:?}",
327            result.err()
328        );
329    }
330
331    #[test]
332    fn sync_context_health_check_returns_ok() {
333        let ctx = A1Context::builder().build();
334        assert!(ctx.health_check().is_ok());
335    }
336}