1use chrono::{DateTime, Utc};
9use rusqlite::Connection;
10use std::path::Path;
11use std::sync::{Arc, Mutex};
12use thiserror::Error;
13
14#[path = "mandate_store_next/mod.rs"]
15mod mandate_store_next;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct AuthzReceipt {
20 pub mandate_id: String,
21 pub use_id: String,
22 pub use_count: u32,
23 pub consumed_at: DateTime<Utc>,
24 pub tool_call_id: String,
25 pub was_new: bool,
28}
29
30#[derive(Debug, Error, PartialEq, Eq)]
32#[non_exhaustive]
33pub enum AuthzError {
34 #[error("Mandate not found: {mandate_id}")]
35 MandateNotFound { mandate_id: String },
36
37 #[error("Mandate already used (single_use=true)")]
38 AlreadyUsed,
39
40 #[error("Max uses exceeded: {current} > {max}")]
41 MaxUsesExceeded { max: u32, current: u32 },
42
43 #[error("Nonce replay detected: {nonce}")]
44 NonceReplay { nonce: String },
45
46 #[error("Mandate metadata conflict for {mandate_id}: stored {field} differs")]
47 MandateConflict { mandate_id: String, field: String },
48
49 #[error("Invalid mandate constraints: single_use=true with max_uses={max_uses}")]
50 InvalidConstraints { max_uses: u32 },
51
52 #[error("Mandate revoked at {revoked_at}")]
53 Revoked { revoked_at: DateTime<Utc> },
54
55 #[error("Database error: {0}")]
56 Database(String),
57}
58
59impl From<rusqlite::Error> for AuthzError {
60 fn from(e: rusqlite::Error) -> Self {
61 AuthzError::Database(e.to_string())
62 }
63}
64
65#[derive(Debug, Clone)]
67pub struct MandateMetadata {
68 pub mandate_id: String,
69 pub mandate_kind: String,
70 pub audience: String,
71 pub issuer: String,
72 pub expires_at: Option<DateTime<Utc>>,
73 pub single_use: bool,
74 pub max_uses: Option<u32>,
75 pub canonical_digest: String,
76 pub key_id: String,
77}
78
79#[derive(Debug, Clone)]
81pub struct ConsumeParams<'a> {
82 pub mandate_id: &'a str,
83 pub tool_call_id: &'a str,
84 pub nonce: Option<&'a str>,
85 pub audience: &'a str,
86 pub issuer: &'a str,
87 pub tool_name: &'a str,
88 pub operation_class: &'a str,
89 pub source_run_id: Option<&'a str>,
90}
91
92#[derive(Clone)]
94pub struct MandateStore {
95 conn: Arc<Mutex<Connection>>,
96}
97
98impl MandateStore {
99 pub fn open(path: &Path) -> Result<Self, AuthzError> {
101 mandate_store_next::schema::open_impl(path)
102 }
103
104 pub fn memory() -> Result<Self, AuthzError> {
106 mandate_store_next::schema::memory_impl()
107 }
108
109 pub fn from_connection(conn: Connection) -> Result<Self, AuthzError> {
111 mandate_store_next::schema::from_connection_impl(conn)
112 }
113
114 pub fn upsert_mandate(&self, meta: &MandateMetadata) -> Result<(), AuthzError> {
116 mandate_store_next::upsert::upsert_mandate_impl(self, meta)
117 }
118
119 pub fn consume_mandate(&self, params: &ConsumeParams<'_>) -> Result<AuthzReceipt, AuthzError> {
121 mandate_store_next::txn::consume_mandate_in_txn_impl(self, params)
122 }
123
124 fn consume_mandate_inner(
125 &self,
126 conn: &Connection,
127 params: &ConsumeParams<'_>,
128 ) -> Result<AuthzReceipt, AuthzError> {
129 mandate_store_next::consume::consume_mandate_inner_impl(conn, params)
130 }
131
132 pub fn get_use_count(&self, mandate_id: &str) -> Result<Option<u32>, AuthzError> {
134 mandate_store_next::stats::get_use_count_impl(self, mandate_id)
135 }
136
137 pub fn count_uses(&self, mandate_id: &str) -> Result<u32, AuthzError> {
139 mandate_store_next::stats::count_uses_impl(self, mandate_id)
140 }
141
142 pub fn nonce_exists(
144 &self,
145 audience: &str,
146 issuer: &str,
147 nonce: &str,
148 ) -> Result<bool, AuthzError> {
149 mandate_store_next::stats::nonce_exists_impl(self, audience, issuer, nonce)
150 }
151
152 pub fn upsert_revocation(&self, r: &RevocationRecord) -> Result<(), AuthzError> {
160 mandate_store_next::revocation::upsert_revocation_impl(self, r)
161 }
162
163 pub fn get_revoked_at(&self, mandate_id: &str) -> Result<Option<DateTime<Utc>>, AuthzError> {
165 mandate_store_next::revocation::get_revoked_at_impl(self, mandate_id)
166 }
167
168 pub fn is_revoked(&self, mandate_id: &str) -> Result<bool, AuthzError> {
170 mandate_store_next::revocation::is_revoked_impl(self, mandate_id)
171 }
172}
173
174#[derive(Debug, Clone)]
176pub struct RevocationRecord {
177 pub mandate_id: String,
178 pub revoked_at: DateTime<Utc>,
179 pub reason: Option<String>,
180 pub revoked_by: Option<String>,
181 pub source: Option<String>,
182 pub event_id: Option<String>,
183}
184
185pub fn compute_use_id(mandate_id: &str, tool_call_id: &str, use_count: u32) -> String {
191 mandate_store_next::stats::compute_use_id_impl(mandate_id, tool_call_id, use_count)
192}
193
194#[cfg(test)]
195#[path = "mandate_store_next/tests.rs"]
196mod tests;