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, PlanConfirmation, PlanFingerprintBuildError, PlanFingerprintDigest,
15 PlanFingerprintRef, PlanSubject, build_canonical_plan, build_plan_digest,
16};
17pub use shared::{
18 SharedCostPermit, SharedDestructivePermit, SharedMutationPermit, SharedPermitState,
19};
20pub use state::PermitAttempt;
21
22use core::fmt;
23
24use subtle::{Choice, ConstantTimeEq};
25
26pub const MAX_PLAN_SCOPE_BYTES: usize = 1024;
28pub const MIN_PERMIT_IDEMPOTENCY_BYTES: usize = 16;
30pub const MAX_PERMIT_IDEMPOTENCY_BYTES: usize = 64;
32
33#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
35pub enum PermitScope {
36 Mutation,
38 Destructive,
40 Cost,
42}
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub enum PlanChange {
47 NoOp,
49 ChangesState,
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub enum ReplayPolicy {
56 SingleAttempt,
58 RecoverNotSent,
60 ReconcileThenRetry,
62}
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub enum AttemptBudgetError {
67 Zero,
69}
70
71impl_static_error!(AttemptBudgetError, Self::Zero => "permit attempt budget must be nonzero");
72
73#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
75pub struct AttemptBudget(u16);
76
77impl AttemptBudget {
78 pub const fn new(value: u16) -> Result<Self, AttemptBudgetError> {
80 if value == 0 {
81 return Err(AttemptBudgetError::Zero);
82 }
83 Ok(Self(value))
84 }
85
86 #[must_use]
88 pub const fn get(self) -> u16 {
89 self.0
90 }
91}
92
93#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
95pub struct PermitTimestamp(u64);
96
97impl PermitTimestamp {
98 #[must_use]
100 pub const fn from_seconds(seconds: u64) -> Self {
101 Self(seconds)
102 }
103
104 #[must_use]
106 pub const fn as_seconds(self) -> u64 {
107 self.0
108 }
109}
110
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub enum PermitValidityError {
114 Empty,
116 TooLong,
118}
119
120impl_static_error!(PermitValidityError,
121 Self::Empty => "permit expiry must follow issuance",
122 Self::TooLong => "permit validity interval is too long",
123);
124
125#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub struct PermitValidity {
128 issued_at: PermitTimestamp,
129 expires_at: PermitTimestamp,
130 duration: u32,
131}
132
133impl PermitValidity {
134 pub fn new(
136 issued_at: PermitTimestamp,
137 expires_at: PermitTimestamp,
138 ) -> Result<Self, PermitValidityError> {
139 let duration = expires_at
140 .0
141 .checked_sub(issued_at.0)
142 .ok_or(PermitValidityError::Empty)?;
143 if duration == 0 {
144 return Err(PermitValidityError::Empty);
145 }
146 let duration = u32::try_from(duration).map_err(|_| PermitValidityError::TooLong)?;
147 Ok(Self {
148 issued_at,
149 expires_at,
150 duration,
151 })
152 }
153
154 #[must_use]
156 pub const fn issued_at(self) -> PermitTimestamp {
157 self.issued_at
158 }
159
160 #[must_use]
162 pub const fn expires_at(self) -> PermitTimestamp {
163 self.expires_at
164 }
165
166 pub(crate) fn offset(self, now: PermitTimestamp) -> Result<u32, ExecutionPermitError> {
167 let elapsed = now
168 .0
169 .checked_sub(self.issued_at.0)
170 .ok_or(ExecutionPermitError::NotYetValid)?;
171 if elapsed >= u64::from(self.duration) {
172 return Err(ExecutionPermitError::Expired);
173 }
174 u32::try_from(elapsed).map_err(|_| ExecutionPermitError::Expired)
175 }
176}
177
178#[derive(Clone, Copy, Debug, Eq, PartialEq)]
180pub enum CurrencyCodeError {
181 Invalid,
183}
184
185impl_static_error!(CurrencyCodeError, Self::Invalid => "currency code must be three uppercase ASCII letters");
186
187#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
189pub struct CurrencyCode([u8; 3]);
190
191impl CurrencyCode {
192 pub fn new(value: &str) -> Result<Self, CurrencyCodeError> {
194 let bytes: [u8; 3] = value
195 .as_bytes()
196 .try_into()
197 .map_err(|_| CurrencyCodeError::Invalid)?;
198 if !bytes.iter().all(u8::is_ascii_uppercase) {
199 return Err(CurrencyCodeError::Invalid);
200 }
201 Ok(Self(bytes))
202 }
203
204 #[must_use]
206 pub const fn as_bytes(self) -> [u8; 3] {
207 self.0
208 }
209}
210
211#[derive(Clone, Copy, Debug, Eq, PartialEq)]
213pub enum PlanCostError {
214 ZeroObservedPrice,
216 SpendingCeilingExceeded,
218}
219
220impl_static_error!(PlanCostError,
221 Self::ZeroObservedPrice => "observed price must be nonzero",
222 Self::SpendingCeilingExceeded => "observed price exceeds spending ceiling",
223);
224
225#[derive(Clone, Copy, Debug, Eq, PartialEq)]
227pub struct PlanCost {
228 currency: CurrencyCode,
229 scale: u8,
230 observed_units: u128,
231 ceiling_units: u128,
232}
233
234impl PlanCost {
235 pub fn new(
237 currency: CurrencyCode,
238 scale: u8,
239 observed_units: u128,
240 ceiling_units: u128,
241 ) -> Result<Self, PlanCostError> {
242 if observed_units == 0 {
243 return Err(PlanCostError::ZeroObservedPrice);
244 }
245 if observed_units > ceiling_units {
246 return Err(PlanCostError::SpendingCeilingExceeded);
247 }
248 Ok(Self {
249 currency,
250 scale,
251 observed_units,
252 ceiling_units,
253 })
254 }
255
256 pub(crate) const fn fields(self) -> (CurrencyCode, u8, u128, u128) {
257 (
258 self.currency,
259 self.scale,
260 self.observed_units,
261 self.ceiling_units,
262 )
263 }
264}
265
266#[derive(Clone, Copy, Debug, Eq, PartialEq)]
268pub enum PermitContextError {
269 Empty,
271 TooLong,
273}
274
275impl_static_error!(PermitContextError,
276 Self::Empty => "permit context is empty",
277 Self::TooLong => "permit context exceeds the length limit",
278);
279
280#[derive(Clone, Copy)]
282pub struct PermitContext<'a>(&'a [u8]);
283
284impl<'a> PermitContext<'a> {
285 pub fn new(value: &'a [u8]) -> Result<Self, PermitContextError> {
287 if value.is_empty() {
288 return Err(PermitContextError::Empty);
289 }
290 if value.len() > MAX_PLAN_SCOPE_BYTES {
291 return Err(PermitContextError::TooLong);
292 }
293 Ok(Self(value))
294 }
295
296 pub(crate) const fn bytes(self) -> &'a [u8] {
297 self.0
298 }
299}
300
301impl fmt::Debug for PermitContext<'_> {
302 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
303 formatter.write_str("PermitContext([redacted])")
304 }
305}
306
307#[derive(Clone, Copy, Debug)]
309pub enum PlanFingerprintScope<'a> {
310 Absent,
312 Value(&'a [u8]),
314}
315
316impl<'a> PlanFingerprintScope<'a> {
317 pub(crate) fn bytes(self) -> Result<Option<&'a [u8]>, PermitContextError> {
318 match self {
319 Self::Absent => Ok(None),
320 Self::Value([]) => Err(PermitContextError::Empty),
321 Self::Value(value) if value.len() > MAX_PLAN_SCOPE_BYTES => {
322 Err(PermitContextError::TooLong)
323 }
324 Self::Value(value) => Ok(Some(value)),
325 }
326 }
327}
328
329#[derive(Clone, Copy, Debug, Eq, PartialEq)]
331pub enum PermitIdempotencyKeyError {
332 TooShort,
334 TooLong,
336 AllZero,
338}
339
340impl_static_error!(PermitIdempotencyKeyError,
341 Self::TooShort => "permit idempotency identity is too short",
342 Self::TooLong => "permit idempotency identity is too long",
343 Self::AllZero => "permit idempotency identity cannot be all zero",
344);
345
346#[derive(Clone, Copy)]
348pub struct PermitIdempotencyKey<'a>(&'a [u8]);
349
350impl<'a> PermitIdempotencyKey<'a> {
351 pub fn new(value: &'a [u8]) -> Result<Self, PermitIdempotencyKeyError> {
353 if value.len() < MIN_PERMIT_IDEMPOTENCY_BYTES {
354 return Err(PermitIdempotencyKeyError::TooShort);
355 }
356 if value.len() > MAX_PERMIT_IDEMPOTENCY_BYTES {
357 return Err(PermitIdempotencyKeyError::TooLong);
358 }
359 let mut nonzero = Choice::from(0);
360 for byte in value {
361 nonzero |= !byte.ct_eq(&0);
362 }
363 if !bool::from(nonzero) {
364 return Err(PermitIdempotencyKeyError::AllZero);
365 }
366 Ok(Self(value))
367 }
368
369 pub(crate) const fn bytes(self) -> &'a [u8] {
370 self.0
371 }
372
373 pub(crate) fn matches(self, other: Self) -> bool {
374 self.0.len() == other.0.len() && bool::from(self.0.ct_eq(other.0))
375 }
376}
377
378impl fmt::Debug for PermitIdempotencyKey<'_> {
379 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
380 formatter.write_str("PermitIdempotencyKey([redacted])")
381 }
382}
383
384#[derive(Clone, Copy, Debug, Eq, PartialEq)]
386pub enum PermitState {
387 Ready,
389 InFlight,
391 Recoverable,
393 PendingReconciliation,
395 Spent,
397}
398
399#[derive(Clone, Copy, Debug, Eq, PartialEq)]
401pub struct RecoveryToken(pub(crate) u16);
402
403#[derive(Clone, Copy, Debug, Eq, PartialEq)]
405pub struct ReconciliationToken(pub(crate) u16);
406
407#[derive(Clone, Copy, Debug, Eq, PartialEq)]
409pub enum PermitDisposition {
410 Spent,
412 Recoverable(RecoveryToken),
414 PendingReconciliation(ReconciliationToken),
416}
417
418#[derive(Clone, Copy, Debug, Eq, PartialEq)]
420pub enum ExecutionPermitError {
421 ScopeMismatch,
423 NotYetValid,
425 Expired,
427 ClockRollback,
429 AttemptInFlight,
431 RecoveryRequired,
433 ReconciliationRequired,
435 Spent,
437 StaleGeneration,
439 FingerprintMismatch,
441 IdempotencyMismatch,
443 ReplayForbidden,
445 GenerationExhausted,
447}
448
449impl_static_error!(ExecutionPermitError,
450 Self::ScopeMismatch => "execution permit scope does not match the plan",
451 Self::NotYetValid => "execution permit is not valid yet",
452 Self::Expired => "execution permit has expired",
453 Self::ClockRollback => "execution permit clock moved backward",
454 Self::AttemptInFlight => "execution permit already has an in-flight attempt",
455 Self::RecoveryRequired => "execution permit requires not-sent recovery",
456 Self::ReconciliationRequired => "execution permit requires reconciliation",
457 Self::Spent => "execution permit is spent",
458 Self::StaleGeneration => "execution permit generation is stale",
459 Self::FingerprintMismatch => "execution permit fingerprint does not match",
460 Self::IdempotencyMismatch => "execution permit idempotency identity does not match",
461 Self::ReplayForbidden => "execution permit replay policy forbids repetition",
462 Self::GenerationExhausted => "execution permit generation is exhausted",
463);
464
465#[cfg(test)]
466mod tests;