Expand description
Session-token capability traits and the shared CodecError.
Tokens split along the mint/verify capability axis (R019): TokenMinter
can forge sessions (origin-only); TokenVerifier only checks them
(edge-safe). Whether one key does both jobs is the codec’s defining property.
This module is the keyless half of that split — only the trait
definitions and the shared error type. No keys, no crypto: the concrete
codecs live in crates layered above cheers-core, so a verify-only consumer
never compiles a minter:
- Symmetric (
Codec) — one key both mints and verifies, so any holder can forge.PasetoV4Codec(v4.local, encrypted) andHmacBlobCodec(HMAC-SHA256, cleartext) live incheers-server, origin-side. - Asymmetric (Ed25519, v4.public) — mint and verify are different keys,
so the edge can verify without the power to mint.
PasetoV4SecretMinterlives incheers-server; the matchingPasetoV4PublicVerifierlives incheers-verify— the onlyTokenVerifierthat cannot also mint, which is exactly what makes edge verification safe.
All impls reject expired tokens during verify, enforced against the caller’s
now in verify_at rather than the crypto layer’s wall clock. A runnable
mint/verify round-trip lives in cheers-server’s codec module — cheers-core
ships no concrete codec to exercise.
@yah:relay(R019, “Edge-verifiable session auth: mint/verify split + asymmetric codec + access/refresh tiers + revocation”) @yah:at(2026-05-26T17:51:47Z) @yah:status(open) @yah:next(“Full design, the locality contract, and the five implementation moves are in .yah/docs/working/edge-verifiable-auth.md; each move is filed as a child feature under this relay.”) @yah:next(“Suggested quest placement: foundation (Q002) owns the core codec/claims/store changes; the driver is the yah-platform edge deployment (Q005). Filed standalone to avoid presuming where it slots — maintainers reparent.”) @yah:gotcha(“The current Codec (PasetoV4Codec v4.local / HmacBlobCodec) is SYMMETRIC — the same key mints AND verifies. Edge verification therefore can’t be done without shipping minting power to the CF edge (forge-any-session blast radius). The asymmetric codec is the prerequisite for ANY edge verification; do not edge-verify the symmetric token.”) @yah:gotcha(“cheers is pre-launch, so splitting the Codec trait can be a breaking change; a blanket impl keeps PasetoV4Codec/HmacBlobCodec working as both minter and verifier.”) @yah:gotcha(“Consumer mapping (yah side): mesofact CF Worker (yah R327) = EdgeVerifier; mesofact axum SSR origin = SessionAuthority; Yubaba backs RefreshStore + RevocationWriter.”) @yah:assumes(“Auth has no cross-session OLTP (every check validates one session) — that licenses a stateless/global access token and an eventually-consistent revocation set. Only refresh replay-detection needs consistency, and it’s homed (origin/Yubaba) on the cold path.”) @arch:see(.yah/docs/working/edge-verifiable-auth.md)
@yah:ticket(R019-F1, “Split Codec into TokenMinter + TokenVerifier traits”)
@yah:assignee(agent:claude)
@yah:at(2026-05-26T17:52:34Z)
@yah:status(review)
@yah:parent(R019)
@yah:next(“Split the Codec trait (codec.rs) into TokenVerifier { verify_at(&str, now) -> Claims } and TokenMinter { mint(&Claims) -> String }.”)
@yah:next(“Impl BOTH traits on PasetoV4Codec and HmacBlobCodec (symmetric: one key mints+verifies) so existing callers keep working AND so the type signature documents that a symmetric codec at the edge carries minting power.”)
@yah:next(“Keystone for the relay: the edge depends only on TokenVerifier; the sole way to satisfy it verify-but-can’t-mint is the asymmetric verifier (sibling: asymmetric PasetoV4Public codec).”)
@yah:verify(“cd external/cheers && cargo test -p cheers-core”)
@yah:verify(“cd external/cheers && cargo check –workspace –all-features”)
@arch:see(.yah/docs/working/edge-verifiable-auth.md)
@yah:handoff(“Codec split landed (codec.rs). TokenMinter { mint } + TokenVerifier { verify_at, verify default }; both impl’d on PasetoV4Codec + HmacBlobCodec. Kept Codec: TokenMinter + TokenVerifier with blanket impl<T: TokenMinter+TokenVerifier> Codec for T {} so existing dyn Codec / impl Codec callers are unchanged. Exported both new traits from lib.rs. Updated codec doctest + proptest imports + a claims.rs doc ref. Verified: cargo test -p cheers-core (34 unit + 6 proptest + 3 doctest) and cargo check –workspace –all-features both green.”)
@yah:handoff(“Cross-camp: re-ran the R009 mesofact consumer (in review) after the split — ZERO source changes needed (supertrait methods resolve through Box
@yah:ticket(R019-F2, “Asymmetric access-token codec: PasetoV4Public (Ed25519) — verify-only public key, mint-only secret key”)
@yah:assignee(agent:claude)
@yah:at(2026-05-26T17:52:46Z)
@yah:status(review)
@yah:parent(R019)
@yah:next(“Add PasetoV4PublicVerifier (impl TokenVerifier, holds AsymmetricPublicKeypublic module (V4 = Ed25519). Origin mints with the secret key; edge verifies with the public key ONLY.”)
@yah:next(“v4.public is signed-not-encrypted, so claims are client-readable — document that only non-secret claims (identity + expiry + jti) belong in the access token, and reposition v4.local as ‘encrypted claims, origin-only verification’.”)
@yah:next(“verify_at must enforce now itself (mirror the symmetric impls’ allow_non_expiring + is_expired_at pattern) rather than relying on pasetors’ wall-clock validation.”)
@yah:verify(“cd external/cheers && cargo test -p cheers-core”)
@yah:verify(“cd external/cheers && cargo deny –all-features check”)
@arch:see(.yah/docs/working/edge-verifiable-auth.md)
@yah:depends_on(R019-F1)
@yah:handoff(“Landed PasetoV4SecretMinter (impl TokenMinter, AsymmetricSecretKey
@yah:ticket(R019-F6, “Carve cheers-verify (PublicVerifier + RevocationReader + EdgeVerifier) into a verify-only crate; minter/symmetric-codecs/stores stay server-side”)
@yah:at(2026-05-27T06:37:41Z)
@yah:status(review)
@yah:parent(R019)
@yah:next(“Create cheers-verify holding PasetoV4PublicVerifier + RevocationReader + the EdgeVerifier facade. Depends on cheers-core (types + traits) and NOT on any minter — the edge consumes only this crate.”)
@yah:next(“Move PasetoV4SecretMinter + the symmetric codecs (PasetoV4Codec, HmacBlobCodec) + UserStore/RefreshStore/RevocationWriter + SessionAuthority into a cheers-server crate that depends on cheers-verify. The one load-bearing arrow: cheers-server -> cheers-verify, never the reverse.”)
@yah:next(“Capability boundary is enforced by the DAG, not a feature flag (a feature is additive and unifiable; a missing dep edge is a compile error). Full target topology in the design doc Crate topology section.”)
@yah:verify(“cd external/cheers && cargo tree -p cheers-verify shows no PasetoV4SecretMinter path and no UserStore/RefreshStore (verify-only)”)
@yah:verify(“cd external/cheers && cargo check –workspace –all-features”)
@yah:gotcha(“The symmetric codecs impl BOTH TokenMinter AND TokenVerifier on one type — they MUST land in cheers-server, never cheers-verify, or the edge regains mint power through the back door (the trap codec.rs already warns about in prose).”)
@yah:gotcha(“Does NOT fix wasm: pasetors sits in cheers-verify too and pulls getrandom via ed25519-compact even on the verify-only path. wasm32-unknown-unknown still needs getrandom wasm_js backend enabled (both 0.3 and 0.4 majors). Orthogonal to this split.”)
@yah:assumes(“R019-F3 (EdgeVerifier facade) and R019-F4 (RevocationReader) land first or alongside — cheers-verify is where they live, so this carve-out assembles their output.”)
@arch:see(.yah/docs/working/edge-verifiable-auth.md)
@yah:depends_on(R019-F3)
@yah:depends_on(R019-F4)
@yah:handoff(“Carve done. 3-crate DAG: cheers-server -> cheers-verify -> cheers-core. cheers-core slimmed to the keyless contract (no crypto deps at all); the F5 server feature + optional crypto deps are GONE — the crate boundary replaced the feature gate, exactly as F5’s handoff predicted.”)
@yah:handoff(“cheers-core now holds: claims (identity types), the full error vocabulary (CodecError/StoreError/RefreshError/Error/Result — all keyless), CredentialStore, and the TokenMinter/TokenVerifier/Codec traits + blanket impl. Nothing else.”)
@yah:handoff(“cheers-verify (-> cheers-core, pasetors, NO minter): PasetoV4PublicVerifier, RevocationReader, EdgeVerifier, plus pub fn codec_err(pasetors::errors::Error)->CodecError. cargo tree -p cheers-verify confirms cheers-core+pasetors but NO cheers-server path — the edge is minter-free by DAG, not by feature.”)
@yah:handoff(“cheers-server (-> cheers-verify -> cheers-core): PasetoV4SecretMinter + symmetric PasetoV4Codec/HmacBlobCodec, refresh rotation (RefreshToken/ChainId/Rotated/RefreshRotator), UserStore/RefreshStore/RefreshTokenRecord/ProviderKey/NewUser, RevocationWriter, SessionAuthority/SessionPolicy/NewSession. Re-exports EdgeVerifier/PasetoV4PublicVerifier/RevocationReader so an origin assembles both tiers from one crate.”)
@yah:handoff(“Two forced calls: (a) From<pasetors::errors::Error> for CodecError cannot live in keyless core (orphan rule) -> it is now cheers_verify::codec_err; cheers/email/magic_link.rs was RELYING on that From impl (not caught by a name grep — it used CodecError::from/.into()) and got a local map_paseto_err. (b) RefreshError stays in core (keyless) so the Error umbrella is unchanged and both facades still return cheers_core::Error.”)
@yah:handoff(“Verified GREEN: cargo check –workspace –all-features; cargo test –workspace (cheers 119+9doc, core 16+1doc, server 34+9proptest+2doc, verify 4); cargo tree -p cheers-verify (no cheers-server). Annotation blocks for R019/F1/F2/F4/F6 preserved across the codec.rs rewrite + store.rs trim.”)
@yah:handoff(“Concurrent work: cheers-store (device CredentialStore over OS keyring) landed mid-ticket and is now a workspace member. It depends on cheers-core with default-features=false — still correct, but post-F6 that flag is a redundant no-op (core has no features) and its Cargo.toml comment about core’s ‘default-on server feature’ is now stale. Left untouched to avoid clobbering in-flight edits; trivial follow-up.”)
@yah:handoff(“Cross-camp (NOT this workspace): mesofact consumes cheers-core’s Codec/PasetoV4Codec. Moving codec impls out of core WILL break mesofact’s imports — it must add cheers-server (axum SSR / SessionAuthority) + cheers-verify (CF Worker / EdgeVerifier) per the consumer mapping. File under yah R327.”)
Enums§
- Codec
Error - Errors returned by
TokenMinter::mintandTokenVerifier::verify.
Traits§
- Codec
- A symmetric codec: one key both mints and verifies.
- Token
Minter - Mint session tokens carrying
Claims. - Token
Verifier - Verify session tokens into
Claims.