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 AuthorityAlreadyMinted,
440 StaleGeneration,
442 FingerprintMismatch,
444 CredentialMismatch,
446 IdempotencyMismatch,
448 ReplayForbidden,
450 GenerationExhausted,
452}
453
454impl_static_error!(ExecutionPermitError,
455 Self::ScopeMismatch => "execution permit scope does not match the plan",
456 Self::NotYetValid => "execution permit is not valid yet",
457 Self::Expired => "execution permit has expired",
458 Self::ClockRollback => "execution permit clock moved backward",
459 Self::AttemptInFlight => "execution permit already has an in-flight attempt",
460 Self::RecoveryRequired => "execution permit requires not-sent recovery",
461 Self::ReconciliationRequired => "execution permit requires reconciliation",
462 Self::Spent => "execution permit is spent",
463 Self::AuthorityAlreadyMinted => "execution authority was already minted",
464 Self::StaleGeneration => "execution permit generation is stale",
465 Self::FingerprintMismatch => "execution permit fingerprint does not match",
466 Self::CredentialMismatch => "execution credential differs from authorization evidence",
467 Self::IdempotencyMismatch => "execution permit idempotency identity does not match",
468 Self::ReplayForbidden => "execution permit replay policy forbids repetition",
469 Self::GenerationExhausted => "execution permit generation is exhausted",
470);
471
472#[cfg(test)]
473mod tests;