Skip to main content

cheers_core/
lib.rs

1//! # cheers-core — auth contract surface
2//!
3//! Pure types and traits shared between cheers's providers and downstream
4//! consumers (mesofact, yah-platform, …). No I/O, no platform code.
5//!
6//! See the design doc at `.yah/docs/working/cheers.md` and the build plan at
7//! `.yah/docs/working/cheers-plan.md`.
8//!
9//! @yah:ticket(R019-F3, "SessionAuthority (origin) + EdgeVerifier (edge) facades + SessionPolicy TTL defaults + jti in Claims")
10//! @yah:assignee(agent:claude)
11//! @yah:at(2026-05-26T17:52:52Z)
12//! @yah:status(review)
13//! @yah:parent(R019)
14//! @yah:next("Add SessionAuthority { minter, refresh: RefreshStore, users: UserStore, revoke: RevocationWriter } (origin) and EdgeVerifier { verifier: TokenVerifier, revoked: RevocationReader } (edge). EdgeVerifier takes a TokenVerifier so it physically cannot mint.")
15//! @yah:next("Add SessionPolicy with sane TTL defaults (access ≈ minutes, refresh ≈ days) so consumers don't pick the wrong durations.")
16//! @yah:next("Add a jti field to Claims (claims.rs, #[non_exhaustive] — additive) so the revocation set has a key.")
17//! @yah:next("Do AFTER the trait split (R019-F1), the asymmetric codec, and the revocation traits — it assembles all three into the two deployment-tier facades.")
18//! @yah:verify("cd external/cheers && cargo test -p cheers-core")
19//! @yah:verify("cd external/cheers && cargo check --workspace --all-features")
20//! @yah:gotcha("Claims (claims.rs) is the documented mesofact<->cheers contract and #[non_exhaustive]; adding jti is additive but coordinate with the R009/R011 mesofact resolver swap so the cookie format stays in sync.")
21//! @arch:see(.yah/docs/working/edge-verifiable-auth.md)
22//! @yah:depends_on(R019-F1)
23//! @yah:depends_on(R019-F4)
24//! @yah:handoff("Landed both facades in new session.rs (exported from lib.rs): SessionAuthority<M,R,U,W>{minter,refresh,users,revoke,policy} (origin) and EdgeVerifier<V,Rd>{verifier,revoked} (edge). Generic, not dyn, so the assembled capability set — and crucially the ABSENCE of a minter in EdgeVerifier — is a fact about the type, not a runtime convention. EdgeVerifier::new takes a TokenVerifier; there is no code path to mint.")
25//! @yah:handoff("SessionPolicy: access_ttl=15min, refresh_ttl=30d defaults + const DEFAULT_*_TTL_SECONDS + with_access_ttl/with_refresh_ttl builders. SessionAuthority methods: establish (mint access w/ fresh jti + mint_root refresh chain), rotate (RefreshRotator::rotate + fresh access, binding passed in since the refresh record doesn't carry it), revoke_session(jti) -> RevocationWriter::revoke, revoke_device -> UserStore::revoke_device. EdgeVerifier::verify_at = signature-verify (gates the read) THEN is_revoked check -> Error::Revoked.")
26//! @yah:handoff("Depends on R019-F4 (RevocationReader/RevocationWriter) which I did first per maintainer ordering — it's in review. jti landed under F4 (its revocation key); this ticket assembles it. Added Error::Refresh(#[from] RefreshError) + Error::Revoked variants (additive, #[non_exhaustive]).")
27//! @yah:handoff("Verified GREEN: cargo test -p cheers-core (51 unit incl. 6 session-facade tests covering establish/rotate/edge-accept-then-revoke/expired-before-revocation/policy-defaults/revoke_device, 9 proptest, 3 doctest) + cargo check --workspace --all-features. cargo doc has no new broken intra-doc links (the asymmetric/symmetric forward refs from F4 now resolve).")
28//!
29//! @yah:ticket(R019-F5, "Factor the no-crypto client surface (Claims + CredentialStore) so device targets do not compile the codec")
30//! @yah:assignee(agent:claude)
31//! @yah:at(2026-05-27T06:02:55Z)
32//! @yah:status(review)
33//! @yah:parent(R019)
34//! @yah:verify("cd external/cheers && cargo tree -p <client-crate> shows no pasetors/hmac/getrandom (or: cargo check -p cheers-core --no-default-features after gating)")
35//! @yah:verify("cd external/cheers && cargo check --workspace --all-features")
36//! @yah:gotcha("apple/native.rs AppleNativeVerifier is SERVER-side despite \"native\" in the name. \"Native\" = the native-platform Apple Sign In flow (no code exchange), not \"runs on device\". The device just invokes ASAuthorizationController and POSTs the id_token up; the origin verifies. Do not let the filename pull verification onto the client.")
37//! @yah:gotcha("A browser needs ~zero cheers Rust: OAuth redirect / navigator.credentials / form-POST are JS, and the session lands in an httpOnly cookie the page cannot read. The real client-Rust consumer is iOS/Android/desktop native apps (Tauri/uniffi).")
38//! @yah:gotcha("P8/P9/P10 impls are not written yet (no cheers/src/store or native dir) — set the crate boundary NOW, before there is anything to migrate. Pre-launch is the cheapest this will ever be.")
39//! @yah:assumes("The device tier touches no token bytes — it acquires an opaque token and stores it, never minting or verifying. That absence is the license to give it a crypto-free dependency.")
40//! @arch:see(.yah/docs/working/edge-verifiable-auth.md)
41//! @yah:next("cheers-core is monolithic and un-feature-gated: any consumer (including a browser/iOS/Android client that only acquires + stores an opaque token) compiles in pasetors/hmac/sha2/subtle/getrandom — the entire mint/verify/refresh machinery it never calls.")
42//! @yah:next("Option A (clean): extract Claims + Credential + CredentialStore into a thin no-crypto crate (cheers-client or cheers-types) with zero crypto deps; have cheers-core depend on it for the shared types. Option B (cheaper): feature-gate cheers-core so default-features=false yields only the identity types + CredentialStore, with codec/refresh/UserStore/RefreshStore behind a \"server\" feature.")
43//! @yah:next("Device-Rust surface = CredentialStore (local keychain) + identity types + the planned P9 native passkey UX glue. It needs zero codec/verify/mint/openidconnect/argon2/webauthn-rs. This is the third, thinnest tier below the edge — pairs with the R019-F3 SessionAuthority/EdgeVerifier facades.")
44//! @yah:next("LEAD PLATFORM: Android is the first dogfood consumer (Android -> authenticate into a yah camp), so cheers-android (Credential Manager + native UX, target_os=android) + cheers-store are the first client crates to build, ahead of cheers-apple/iOS. iOS is harder due to background-execution + Local Network privacy, not traversal. Transport rides xlb-net/iroh; see edge-verifiable-auth.md Crate topology.")
45//! @yah:handoff("Feature-gated cheers-core (Option B). Crypto deps (pasetors/hmac/sha2/subtle/base64/getrandom) are now optional, pulled only by a default-on `server` feature. --no-default-features yields the no-crypto client surface.")
46//! @yah:handoff("Always-compiled (no-crypto) surface: claims (identity types), store traits (CredentialStore/UserStore/RefreshStore + records), revocation traits (RevocationReader/Writer), and the keyless codec traits TokenMinter/TokenVerifier/Codec + CodecError + Error/Result. A device crate can hold a `dyn TokenVerifier` without compiling a codec.")
47//! @yah:handoff("Behind `server`: codec concrete impls (PasetoV4Codec, HmacBlobCodec, PasetoV4SecretMinter, PasetoV4PublicVerifier) + the pasetors From impl, the whole refresh and session modules, and error.rs's Refresh(RefreshError) variant. codec_proptests.rs is #![cfg(feature=server)].")
48//! @yah:handoff("Verified GREEN: cargo check -p cheers-core --no-default-features (crypto-free tree confirmed via cargo tree); cargo test -p cheers-core (default); cargo check --workspace + --all-features. cheers consumer needed no changes (depends on cheers-core default = server).")
49//! @yah:handoff("For F6: the server/non-server feature boundary IS the target crate boundary. Promote the `server`-gated modules into cheers-verify (PublicVerifier+RevocationReader+EdgeVerifier) and cheers-server (SecretMinter+symmetric codecs+stores+SessionAuthority); the keyless traits stay in cheers-core so cheers-verify can name TokenVerifier.")
50//!
51//! @yah:relay(R020, "MCP auth and ownership — principal kinds, ownership table, mint paths, audit")
52//! @yah:at(2026-06-04T01:34:48Z)
53//! @yah:status(open)
54//! @yah:next("Resolve wire-envelope open question (PASETO v4.public vs JWT/Ed25519) in the -S1 spike before any mint-path ticket starts.")
55//! @yah:next("Land foundation tickets (principal kinds, scope vocab, ownership table) in cheers-core/cheers-server before mint paths.")
56//! @yah:next("Mint paths, admin endpoints, JWKS, audit can ship in parallel once the foundation is in.")
57//! @yah:verify("cargo test -p cheers-core && cargo test -p cheers-server && cargo test -p cheers-verify")
58//! @yah:gotcha("This relay PRODUCES the wire contract that yah's kamaji consumes. Any wire-shape change here is a coordinated change with yah's R426/R427/R428 — flag the yah-side relay in any handoff.")
59//! @yah:gotcha("ownership:write and audit:write are kind=service ONLY. The grant API must reject (principal_kind=user, scope=ownership:write|audit:write) at write time, not just at mint.")
60//! @yah:assumes("R019-F5/F6 crate split is effectively landed (in review) — MCP-token mint paths bolt onto cheers-server's signer; cheers-verify verifies them unchanged.")
61//! @yah:assumes("yah-side consumer spec (W159) keeps the wire claim shapes verbatim with this doc (act, owns, camp_id, auth_strength).")
62//! @arch:see(.yah/docs/working/mcp-auth-and-ownership.md)
63//! @arch:see(.yah/docs/working/edge-verifiable-auth.md)
64//! @yah:depends_on(R019-F6)
65
66pub mod claims;
67pub mod codec;
68pub mod delegation;
69pub mod error;
70pub mod mcp;
71pub mod principal;
72pub mod store;
73
74pub use claims::{Claims, Credential, DeviceBinding, DeviceId, User, UserId};
75pub use delegation::{DelegationError, UserDelegation};
76// The keyless capability traits + the codec error. The verify/mint impls that
77// satisfy these live in cheers-verify / cheers-server — cheers-core ships only
78// the contract, so a device or verify-only consumer can name `TokenVerifier`
79// (e.g. hold a `dyn TokenVerifier`) without compiling any crypto.
80pub use codec::{Codec, CodecError, TokenMinter, TokenVerifier};
81pub use error::{Error, RefreshError, Result};
82pub use mcp::{
83    validate_grant, Actor, AuthStrength, GrantError, McpClaims, Owns, Scope, ScopeParseError,
84};
85pub use principal::{
86    Principal, PrincipalError, PrincipalId, PrincipalIdParseError, PrincipalKind, PrincipalStatus,
87};
88pub use store::{CredentialStore, StoreError};