1mod clock;
4mod direct;
5mod execution_error;
6mod fingerprint;
7mod shared;
8mod state;
9
10pub use clock::PermitClock;
11pub use direct::{CostPermit, DestructivePermit, MutationPermit};
12pub use execution_error::PermitExecutionError;
13pub use fingerprint::{
14 CanonicalPlanFingerprint, PlanAuthorizationEvidence, PlanConfirmation,
15 PlanFingerprintBuildError, PlanFingerprintDigest, PlanFingerprintRef, PlanSubject,
16 build_canonical_plan, build_plan_digest, build_plan_digest_with_authorization_evidence,
17};
18pub use shared::{
19 SharedCostPermit, SharedDestructivePermit, SharedMutationPermit, SharedPermitState,
20};
21pub use state::PermitAttempt;
22
23use core::fmt;
24
25use subtle::{Choice, ConstantTimeEq};
26
27pub const MAX_PLAN_SCOPE_BYTES: usize = 1024;
29pub const MIN_PERMIT_IDEMPOTENCY_BYTES: usize = 16;
31pub const MAX_PERMIT_IDEMPOTENCY_BYTES: usize = 64;
33
34#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
36pub enum PermitScope {
37 Mutation,
39 Destructive,
41 Cost,
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum PlanChange {
48 NoOp,
50 ChangesState,
52}
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub enum ReplayPolicy {
57 SingleAttempt,
59 RecoverNotSent,
61 ReconcileThenRetry,
63}
64
65#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67pub enum AttemptBudgetError {
68 Zero,
70}
71
72impl_static_error!(AttemptBudgetError, Self::Zero => "permit attempt budget must be nonzero");
73
74#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
76pub struct AttemptBudget(u16);
77
78impl AttemptBudget {
79 pub const fn new(value: u16) -> Result<Self, AttemptBudgetError> {
81 if value == 0 {
82 return Err(AttemptBudgetError::Zero);
83 }
84 Ok(Self(value))
85 }
86
87 #[must_use]
89 pub const fn get(self) -> u16 {
90 self.0
91 }
92}
93
94#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
96pub struct PermitTimestamp(u64);
97
98impl PermitTimestamp {
99 #[must_use]
101 pub const fn from_seconds(seconds: u64) -> Self {
102 Self(seconds)
103 }
104
105 #[must_use]
107 pub const fn as_seconds(self) -> u64 {
108 self.0
109 }
110}
111
112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
114pub enum PermitValidityError {
115 Empty,
117 TooLong,
119}
120
121impl_static_error!(PermitValidityError,
122 Self::Empty => "permit expiry must follow issuance",
123 Self::TooLong => "permit validity interval is too long",
124);
125
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub struct PermitValidity {
129 issued_at: PermitTimestamp,
130 expires_at: PermitTimestamp,
131 duration: u32,
132}
133
134impl PermitValidity {
135 pub fn new(
137 issued_at: PermitTimestamp,
138 expires_at: PermitTimestamp,
139 ) -> Result<Self, PermitValidityError> {
140 let duration = expires_at
141 .0
142 .checked_sub(issued_at.0)
143 .ok_or(PermitValidityError::Empty)?;
144 if duration == 0 {
145 return Err(PermitValidityError::Empty);
146 }
147 let duration = u32::try_from(duration).map_err(|_| PermitValidityError::TooLong)?;
148 Ok(Self {
149 issued_at,
150 expires_at,
151 duration,
152 })
153 }
154
155 #[must_use]
157 pub const fn issued_at(self) -> PermitTimestamp {
158 self.issued_at
159 }
160
161 #[must_use]
163 pub const fn expires_at(self) -> PermitTimestamp {
164 self.expires_at
165 }
166
167 pub(crate) fn offset(self, now: PermitTimestamp) -> Result<u32, ExecutionPermitError> {
168 let elapsed = now
169 .0
170 .checked_sub(self.issued_at.0)
171 .ok_or(ExecutionPermitError::NotYetValid)?;
172 if elapsed >= u64::from(self.duration) {
173 return Err(ExecutionPermitError::Expired);
174 }
175 u32::try_from(elapsed).map_err(|_| ExecutionPermitError::Expired)
176 }
177}
178
179#[derive(Clone, Copy, Debug, Eq, PartialEq)]
181pub enum CurrencyCodeError {
182 Invalid,
184}
185
186impl_static_error!(CurrencyCodeError, Self::Invalid => "currency code must be three uppercase ASCII letters");
187
188#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
190pub struct CurrencyCode([u8; 3]);
191
192impl CurrencyCode {
193 pub fn new(value: &str) -> Result<Self, CurrencyCodeError> {
195 let bytes: [u8; 3] = value
196 .as_bytes()
197 .try_into()
198 .map_err(|_| CurrencyCodeError::Invalid)?;
199 if !bytes.iter().all(u8::is_ascii_uppercase) {
200 return Err(CurrencyCodeError::Invalid);
201 }
202 Ok(Self(bytes))
203 }
204
205 #[must_use]
207 pub const fn as_bytes(self) -> [u8; 3] {
208 self.0
209 }
210}
211
212#[derive(Clone, Copy, Debug, Eq, PartialEq)]
214pub enum PlanCostError {
215 ZeroObservedPrice,
217 SpendingCeilingExceeded,
219}
220
221impl_static_error!(PlanCostError,
222 Self::ZeroObservedPrice => "observed price must be nonzero",
223 Self::SpendingCeilingExceeded => "observed price exceeds spending ceiling",
224);
225
226#[derive(Clone, Copy, Debug, Eq, PartialEq)]
228pub struct PlanCost {
229 currency: CurrencyCode,
230 scale: u8,
231 observed_units: u128,
232 ceiling_units: u128,
233}
234
235impl PlanCost {
236 pub fn new(
238 currency: CurrencyCode,
239 scale: u8,
240 observed_units: u128,
241 ceiling_units: u128,
242 ) -> Result<Self, PlanCostError> {
243 if observed_units == 0 {
244 return Err(PlanCostError::ZeroObservedPrice);
245 }
246 if observed_units > ceiling_units {
247 return Err(PlanCostError::SpendingCeilingExceeded);
248 }
249 Ok(Self {
250 currency,
251 scale,
252 observed_units,
253 ceiling_units,
254 })
255 }
256
257 pub(crate) const fn fields(self) -> (CurrencyCode, u8, u128, u128) {
258 (
259 self.currency,
260 self.scale,
261 self.observed_units,
262 self.ceiling_units,
263 )
264 }
265}
266
267#[derive(Clone, Copy, Debug, Eq, PartialEq)]
269pub enum PermitContextError {
270 Empty,
272 TooLong,
274}
275
276impl_static_error!(PermitContextError,
277 Self::Empty => "permit context is empty",
278 Self::TooLong => "permit context exceeds the length limit",
279);
280
281#[derive(Clone, Copy)]
283pub struct PermitContext<'a>(&'a [u8]);
284
285impl<'a> PermitContext<'a> {
286 pub fn new(value: &'a [u8]) -> Result<Self, PermitContextError> {
288 if value.is_empty() {
289 return Err(PermitContextError::Empty);
290 }
291 if value.len() > MAX_PLAN_SCOPE_BYTES {
292 return Err(PermitContextError::TooLong);
293 }
294 Ok(Self(value))
295 }
296
297 pub(crate) const fn bytes(self) -> &'a [u8] {
298 self.0
299 }
300}
301
302impl fmt::Debug for PermitContext<'_> {
303 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
304 formatter.write_str("PermitContext([redacted])")
305 }
306}
307
308#[derive(Clone, Copy, Debug)]
310pub enum PlanFingerprintScope<'a> {
311 Absent,
313 Value(&'a [u8]),
315}
316
317impl<'a> PlanFingerprintScope<'a> {
318 pub(crate) fn bytes(self) -> Result<Option<&'a [u8]>, PermitContextError> {
319 match self {
320 Self::Absent => Ok(None),
321 Self::Value([]) => Err(PermitContextError::Empty),
322 Self::Value(value) if value.len() > MAX_PLAN_SCOPE_BYTES => {
323 Err(PermitContextError::TooLong)
324 }
325 Self::Value(value) => Ok(Some(value)),
326 }
327 }
328}
329
330#[derive(Clone, Copy, Debug, Eq, PartialEq)]
332pub enum PermitIdempotencyKeyError {
333 TooShort,
335 TooLong,
337 AllZero,
339}
340
341impl_static_error!(PermitIdempotencyKeyError,
342 Self::TooShort => "permit idempotency identity is too short",
343 Self::TooLong => "permit idempotency identity is too long",
344 Self::AllZero => "permit idempotency identity cannot be all zero",
345);
346
347#[derive(Clone, Copy)]
349pub struct PermitIdempotencyKey<'a>(&'a [u8]);
350
351impl<'a> PermitIdempotencyKey<'a> {
352 pub fn new(value: &'a [u8]) -> Result<Self, PermitIdempotencyKeyError> {
354 if value.len() < MIN_PERMIT_IDEMPOTENCY_BYTES {
355 return Err(PermitIdempotencyKeyError::TooShort);
356 }
357 if value.len() > MAX_PERMIT_IDEMPOTENCY_BYTES {
358 return Err(PermitIdempotencyKeyError::TooLong);
359 }
360 let mut nonzero = Choice::from(0);
361 for byte in value {
362 nonzero |= !byte.ct_eq(&0);
363 }
364 if !bool::from(nonzero) {
365 return Err(PermitIdempotencyKeyError::AllZero);
366 }
367 Ok(Self(value))
368 }
369
370 pub(crate) const fn bytes(self) -> &'a [u8] {
371 self.0
372 }
373
374 pub(crate) fn matches(self, other: Self) -> bool {
375 self.0.len() == other.0.len() && bool::from(self.0.ct_eq(other.0))
376 }
377}
378
379impl fmt::Debug for PermitIdempotencyKey<'_> {
380 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
381 formatter.write_str("PermitIdempotencyKey([redacted])")
382 }
383}
384
385#[derive(Clone, Copy, Debug, Eq, PartialEq)]
387pub enum PermitState {
388 Ready,
390 InFlight,
392 Recoverable,
394 PendingReconciliation,
396 Spent,
398}
399
400#[derive(Clone, Copy, Debug, Eq, PartialEq)]
402pub struct RecoveryToken(pub(crate) u16);
403
404#[derive(Clone, Copy, Debug, Eq, PartialEq)]
406pub struct ReconciliationToken(pub(crate) u16);
407
408#[derive(Clone, Copy, Debug, Eq, PartialEq)]
410pub enum PermitDisposition {
411 Spent,
413 Recoverable(RecoveryToken),
415 PendingReconciliation(ReconciliationToken),
417}
418
419#[derive(Clone, Copy, Debug, Eq, PartialEq)]
421pub enum ExecutionPermitError {
422 ScopeMismatch,
424 NotYetValid,
426 Expired,
428 ClockRollback,
430 AttemptInFlight,
432 RecoveryRequired,
434 ReconciliationRequired,
436 Spent,
438 StaleGeneration,
440 FingerprintMismatch,
442 CredentialMismatch,
444 IdempotencyMismatch,
446 ReplayForbidden,
448 GenerationExhausted,
450}
451
452impl_static_error!(ExecutionPermitError,
453 Self::ScopeMismatch => "execution permit scope does not match the plan",
454 Self::NotYetValid => "execution permit is not valid yet",
455 Self::Expired => "execution permit has expired",
456 Self::ClockRollback => "execution permit clock moved backward",
457 Self::AttemptInFlight => "execution permit already has an in-flight attempt",
458 Self::RecoveryRequired => "execution permit requires not-sent recovery",
459 Self::ReconciliationRequired => "execution permit requires reconciliation",
460 Self::Spent => "execution permit is spent",
461 Self::StaleGeneration => "execution permit generation is stale",
462 Self::FingerprintMismatch => "execution permit fingerprint does not match",
463 Self::CredentialMismatch => "execution credential differs from authorization evidence",
464 Self::IdempotencyMismatch => "execution permit idempotency identity does not match",
465 Self::ReplayForbidden => "execution permit replay policy forbids repetition",
466 Self::GenerationExhausted => "execution permit generation is exhausted",
467);
468
469#[cfg(test)]
470mod tests;