0039. The freshness and time-budget system
Context
Several ADRs each define a latency or age budget and each says, correctly, "numbers later, shape now": revocation propagation N (ADR-0005), status-list refresh and max-age (ADR-0032), key publish-before-sign lead and JWKS cache staleness (ADR-0023), the clock-skew window (ADR-0033), access-token TTL and refresh-token lifetime (ADR-0014, ADR-0005), and the session-termination bound (ADR-0016). Deferring the numbers is right. But these budgets are not independent — they form a coupled system, and the headline guarantee ("kill now within a stated budget", ADR-0005) depends on relationships between them that today live implicitly and scattered across six records. An operator can therefore set an internally-inconsistent profile — status-list max-age longer than the revocation budget, or a key overlap window shorter than the access-token TTL — and silently break the guarantee while every individual number looks defensible. This ADR fixes the system, not the numbers: it names the budget set, pins the ordering invariants between them, and makes an inconsistent profile a startup failure rather than a latent hole.
Decision
-
One named budget registry. The freshness/time budgets are a single, enumerated, per-realm-tunable set: access-token TTL; refresh-token lifetime and idle timeout; status-list refresh interval and max-age (ADR-0032); revocation propagation budget N (ADR-0005); key publish-before-sign lead and retiring-key overlap window (ADR-0023); JWKS cache staleness bound (ADR-0023); clock-skew tolerance window (ADR-0033); session lifetime and session-termination bound (ADR-0016). Each source ADR keeps owning its parameter; this ADR owns the set and the relationships.
-
The binding ordering invariants — architectural, fixed now.
- retiring-key overlap window ≥ max access-token TTL — no orphaned token (restates ADR-0023 §3 as an invariant).
- key publish-before-sign lead > JWKS cache staleness bound — the edge learns a
kidbefore any token bearing it arrives (restates ADR-0023 §4). - status-list refresh interval < max-age − clock-skew window — a healthy edge refreshes with room to spare, rather than routinely failing closed between publishes (ties ADR-0032's two knobs to each other).
- JWKS cache staleness bound (and the JWKS document max-age, ADR-0043) ≤ the key-compromise containment budget — the emergency kill path (ADR-0036) is only as fast as the stalest JWKS an edge may still trust.
- the clock-skew window is small and shared by
exp/nbf/iat, TOTP, and status-list freshness — set once (ADR-0033 §2). - the session-termination bound equals the revocation budget — same machinery, same bound (ADR-0016).
-
The product's revocation-freshness promise is one composed, published number — and ADR-0005's N is defined as that number. The effective kill-now bound is
max(status-list max-age, urgent-NATS-push latency) + clock-skew window. Defining N as the composed value (rather than an independently-set knob gated against it) dissolves a circularity: there is no separate N for max-age to violate. Of the composition's inputs, max-age and the skew window are configured knobs the startup gate (decision 4) validates; the urgent-push latency is a measured runtime property — no config-time gate can validate a network's future behavior — so it is tracked as a first-class SLO on the security/freshness plane (architecture §8, proposal 0012) and alarmed when it erodes the published bound. That composite — not the internal knobs — is the figure operators and relying parties are given. -
A startup validation gate, fail-closed. A realm's budget profile is validated at config load against the invariants of decision 2; a profile that violates one refuses to start rather than degrading the guarantee silently — the budget analog of the V1 edge-independence check and consistent with the fail-closed posture throughout (ADR-0010, ADR-0032, ADR-0033). An abuse-case test (ADR-0028) asserts an inconsistent profile is rejected.
Budgets live at two layers, and the gate validates across them: per-realm knobs (token TTLs, session bounds, the published kill-now figure) and per-deployment edge properties (JWKS/status-list cache staleness, the clock-skew window) — an edge node serves many realms, so its staleness and skew are fleet-wide facts, not realm settings. A realm profile that promises a bound tighter than the deployment's staleness floor fails the gate exactly like an intra-realm inconsistency.
-
The single-binary profile ships consistent defaults (ADR-0024). The dev profile carries an illustrative, self-consistent budget set (short TTLs, a tight skew window, an overlap window ≥ the TTL) — a worked example of a valid profile, not a production prescription. Production numbers stay milestone- and operator-set per each source ADR; only their consistency is enforced here.
Consequences
- The headline promise finally has a single stated budget — the composed value of decision 3 — instead of an N deferred independently in ADR-0005, ADR-0023, ADR-0032, ADR-0033, and ADR-0016.
- An inconsistent profile is caught mechanically at config time, not discovered in production as a revocation that never propagates or a rotation that orphans tokens.
- No single number is re-litigated: each source ADR still owns its parameter; this ADR owns only the couplings and the gate, so it composes with them rather than overriding.
- The dev profile is self-consistent out of the box, so the walking skeleton can't ship a budget set that quietly violates the constitution.
- Refines ADR-0005, ADR-0023, ADR-0032, ADR-0033, and ADR-0016.