Repository navigation
Description
Gap
#364 retires credential epoch 0 permanently, and AGENTS.md says "a missing or malformed claim is always a mismatch". That holds only while every stored CredentialEpoch is at least 1. Nothing enforces it: the column defaults to 1, and there is no check constraint, no domain guard and no refusal in the verifier.
Codex Astra proved the dependency with a throwaway real-Postgres probe during the #857 design review (2026-10-03, /home/mforce/.cluckwork-slices/reviews/857-design-astra.md). It set a user's stored epoch to 0 with SQL, then sent correctly signed Owner bearers with no epoch claim, a malformed claim, and an explicit "0". All three got 200 from /api/v1/users.
No code path writes 0 today, so this is not a live hole. It is a fail-closed guarantee that rests on data instead of structure. A bad backfill, manual SQL, or a future code path that writes 0 would silently re-enable claimless and malformed bearers for that user.
What closing it needs
- Verifier: treat a stored epoch below 1 as a mismatch, never as a match. That is
CredentialEpochMiddleware, orICredentialEpochVerifierafter [C] #514 slice 15: Access contract — security hold point, runs last #857 PR A. - Refresh admission: the same refusal at the epoch comparison in
IdentityProvider(about line 226 at4036702d). - Issuance: never mint a token whose epoch claim would be below 1.
- Schema: a
CHECK ("CredentialEpoch" >= 1)constraint, added by a new migration.InitialCreateis frozen (feat(eggs): make cracked and dirty eggs sellable stock via condition grades (#396) #407). Verify there are no existing violating rows first, and regenerate the schema docs (chore(schema): generate PostgreSQL schema documentation #417).
Done when
- A stored epoch of
0makes the claimless, malformed and"0"bearers fail with 401, both on access and on refresh. Prove it with mutations per AGENTS.md "Writing a guard". - AGENTS.md's Credential epoch: per-request revocation check, deployed inert ahead of the user-admin mutations #364 paragraph states the guarantee unconditionally again. [C] #514 slice 15: Access contract — security hold point, runs last #857 PR A adds the interim sentence saying it currently depends on stored epoch ≥ 1.
This is a behaviour change, so it is deliberately not part of the #857 extraction.
Activity
test(auth): seed lockout state so a refused login cannot reset it
test(auth): check every field and log entry of the code step's events…