Skip to main content

Module store

Module store 

Source
Expand description

Origin-side persistence contracts — UserStore and RefreshStore.

These are the origin store traits: identity/provider linkage and refresh-token rotation state. They moved out of cheers-core (R019-F6) so a verify-only or device-only consumer never names them. The device store, CredentialStore, stays in cheers-core; the shared StoreError does too.

All traits are async via [async_trait] so they remain dyn-compatible. Concrete impls (Postgres, Yubaba-backed, in-memory) live in product code.

@yah:ticket(R020-F4, “Ownership table schema + writers (POST/DELETE /ownership) + cascading revoke”) @yah:assignee(agent:claude) @yah:at(2026-06-04T01:35:23Z) @yah:status(review) @yah:phase(P1) @yah:parent(R020) @yah:gotcha(“CHECK (granted_by LIKE ‘svc:%’) and CHECK (on_behalf_of IS NULL OR on_behalf_of LIKE ‘user:%’) are invariants. A row violating either is a bug — humans never appear in granted_by, services never in on_behalf_of.”) @yah:assumes(“ownership_version (yah/W159 Layer 3 freshness backstop) is intentionally deferred in v1 — add the column/endpoint when the staleness budget needs tightening.”) @arch:see(.yah/docs/working/mcp-auth-and-ownership.md) @yah:depends_on(R020-F2) @yah:next(“Sign off the store layer + cascade primitive (this ticket).”) @yah:next(“On sign-off: R020-T1 (McpClaims mint/verify helpers) unblocks → R020-T2 (Bearer middleware) unblocks → R020-T3 (HTTP routes) unblocks. After T3 lands the F4 title’s HTTP claim is fully realized in the codebase.”) @yah:next(“Separately: wire the cascade revoke caller into SessionAuthority’s user-revocation hook — file as a peer task when that surface grows MCP awareness.”) @yah:handoff(“STORE LAYER COMPLETE & TESTED. Landed: (a) cheers-server/src/ownership.rs — OwnershipStore trait (insert/get/revoke_by_id/revoke_by_on_behalf_of/list_for_principal) + NewOwnership::new() enforcing the granted_by=Service + on_behalf_of=User|None invariants up front + OwnershipRow + OwnershipValidationError. (b) cheers-sqlx migrations/{pg,sqlite}/0002_ownership.sql with the schema verbatim from §Ownership table — CHECKs + ix_ownership_principal + ix_ownership_on_behalf_of (both partial WHERE revoked_at IS NULL). (c) PgOwnershipStore + SqliteOwnershipStore. (d) common::ownership_store_lifecycle + check_constraints_reject_bad_rows scenarios.”) @yah:handoff(“Cascade revoke landed at the STORE level (revoke_by_on_behalf_of — single UPDATE … WHERE on_behalf_of=$1 AND revoked_at IS NULL, returns row count). revoke_by_id is idempotent (re-revoke doesn’t overwrite revoked_at; unknown id => NotFound). Row id is UUIDv4 hex (load-bearing property is ‘crypto-random 128-bit’, not the ULID encoding the doc names).”) @yah:handoff(“SCOPE NOTE — HTTP routes (POST/DELETE /ownership) split out. The ‘writers’ in the F4 title are satisfied at the OwnershipStore trait layer (the layer that actually enforces the row invariants); the HTTP wrappers need cheers-axum infrastructure that didn’t exist (Bearer/McpClaims middleware, McpClaims mint/verify helpers — TokenMinter/Verifier are hard-coded to Claims). Split to peer tasks R020-T1 (McpClaims mint/verify helpers in cheers-server) → R020-T2 (Bearer/McpClaims middleware in cheers-axum) → R020-T3 (POST/DELETE /ownership routes). Cleaner than expanding F4 inline; also resolves the F6→F4 circular dep noted in the prior handoff.”) @yah:handoff(“DEFERRED: caller of revoke_by_on_behalf_of in the user-revocation hook (SessionAuthority::revoke_device or peer). The cascade primitive itself is tested; composing it into user-revocation is a follow-up under the same store-side scope (peer task — file when SessionAuthority grows MCP awareness).”) @yah:handoff(“Verified GREEN: cargo test -p cheers-core (51), -p cheers-server (39 incl. 4 new ownership tests), -p cheers-verify (35+9+2+4), -p cheers-sqlx –features sqlite (7 incl. 2 new ownership integration tests). pg-integration –tests compiles (full pg run needs Docker).”) @yah:verify(“cargo test -p cheers-server && cargo test -p cheers-sqlx –features sqlite”) @yah:verify(“ownership_store_lifecycle scenario: insert → revoke_by_id → list_for_principal excludes the revoked row; re-revoke is idempotent (revoked_at unchanged).”) @yah:verify(“check_constraints_reject_bad_rows: a row with granted_by NOT LIKE ‘svc:%’ or on_behalf_of NOT LIKE ‘user:%’ fails the SQL CHECK (both pg + sqlite).”) @yah:verify(“Cascade revoke: revoke_by_on_behalf_of(user, now) sweeps every live row with that on_behalf_of in one UPDATE, returns the row count, and a follow-up revoke returns 0.”)

@yah:ticket(R020-F13, “Audit ingest endpoint + centralized audit table (POST /audit/ingest)”) @yah:assignee(agent:claude) @yah:at(2026-06-04T01:36:39Z) @yah:status(review) @yah:phase(P4) @yah:parent(R020) @yah:gotcha(“audit:write is kind=service only. Same enforcement as ownership:write — reject at grant write time.”) @arch:see(.yah/docs/working/mcp-auth-and-ownership.md) @yah:depends_on(R020-F3) @yah:depends_on(R020-F4) @yah:handoff(“LANDED end-to-end (store layer + sqlx persistence + HTTP route). cheers-server/src/audit.rs: AuditRecord { at, sub, act, camp_id, aud, method, scope, result, request_id } + AuditValidationError (NonPositiveAt / EmptyAud / EmptyMethod / EmptyResult / EmptyRequestId) + AuditRecord::new() validates up front + AuditRecord::validate() so deserialized records re-check + AuditRow { id, record, ingested_at } + AuditStore trait (single mutation — insert_batch) + MemoryAuditStore. Re-exported from cheers_server::* (AuditRecord, AuditRow, AuditStore, AuditValidationError, MemoryAuditStore).”) @yah:handoff(“PERSISTENCE: cheers-sqlx/migrations/{pg,sqlite}/0004_audit.sql adds an append-only audit table with the wire-shape columns + (id, ingested_at) cheers stamps. Two indexes: ix_audit_sub_at on (sub, at DESC) for the F14 ‘who did what’ lookup, and a partial ix_audit_act_sub_at WHERE act_sub IS NOT NULL for agent-attribution queries. cheers-sqlx/src/audit_store.rs: PgAuditStore + SqliteAuditStore — each insert_batch runs in a single sqlx transaction so a mid-batch DB failure leaves the table untouched (matches kamaji’s atomic-retry contract). scope column stored as JSON text (serde array of wire scope strings) — keeps it forward-compatible if Scope grows variants between writer and reader.”) @yah:handoff(“HTTP ROUTE: cheers-axum/src/audit.rs adds AuditState { mcp, store } + AuditIngestBody (transparent Vec) + AuditIngestResponse { rows } + POST /audit/ingest handler. Pipeline: authenticate_mcp → claims.require_scope(Scope::AuditWrite) → validate every record (whole-batch first, atomic) → store.insert_batch(records, now). Returns 201 + the inserted rows. RouteError::AuditInvalid(#[from] AuditValidationError) variant maps to 400 ‘audit_invalid’ (distinct from 401/403 so kamaji can tell auth failure from a forbidden-shape batch and not retry as-is). audit module re-exported from cheers_axum::* (AuditIngestBody, AuditIngestResponse, AuditState).”) @yah:handoff(“USER-PRINCIPAL AUDIT:WRITE REJECTION is the canonical grant-time check — validate_grant(PrincipalKind::User, Scope::AuditWrite) is already pinned by cheers-core::mcp::tests::validate_grant_rejects_service_only_for_user (it covers OwnershipWrite + AuditWrite explicitly). Mint paths run validate_grant per expanded scope (R020-F6/F7/F8). This route is defense-in-depth: a token reaching POST /audit/ingest with audit:write must already have cleared grant-time + mint-time checks.”) @yah:handoff(“ATOMIC BATCH SEMANTICS: validate-all-then-insert at the handler; sqlx impls run insert_batch in a transaction. A 4xx on a malformed record leaves the audit table untouched, so kamaji’s bounded-backoff retry of the corrected batch sees a clean ledger — no partial commits, no dedup logic needed on either side. Empty batch is a no-op (returns 201 + empty rows).”) @yah:handoff(“LIBSQL/TURSO MIGRATION COMPAT (R020-T19): the libsql-integration harness applies every migration in migrations/sqlite/ statement-by-statement; 0004_audit.sql is picked up automatically when the harness runs. Smoke verified locally via cargo test -p cheers-sqlx --features sqlite (10 → 11 tests, all green); a libsql-integration run is the standing follow-up per T19’s @yah:assumes (Docker required).”) @yah:handoff(“R020-F14 (read endpoint) consumes this directly — the trait grows a paged read method, the schema is in place with the correct index, and AuditRow carries the columns F14’s response needs. depends_on(R020-F13) edge on F14 stands.”) @yah:next(“Sign off F13 — tasks-met, awaiting human review.”) @yah:next(“Claim R020-F14 to add the AuditStore read surface (list_by_on_behalf_of with cursor pagination) and the GET /audit/by-on-behalf-of/ HTTP route.”) @yah:next(“When kamaji’s wire-side begins forwarding live batches, coordinate yah-side W159 / R428 to point at POST /audit/ingest + the AuditIngestBody shape (bare JSON array of records).”) @yah:verify(“cargo test -p cheers-server audit:: — 8/8 trait + memory-impl tests pass.”) @yah:verify(“cargo test -p cheers-sqlx –features sqlite audit_store_batch_insert_round_trip — sqlite trait conformance pass (100-record batch + act-bearing record + svc-sub record round-trip via SqliteAuditStore).”) @yah:verify(“cargo test -p cheers-axum –test audit_basic — 4/4 integration tests pass: batch POST 100 records all landed; forbidden shape returns 400 + corrected retry succeeds; token without audit:write → 403 before any store call; missing bearer → 401.”) @yah:verify(“cargo test -p cheers-core validate_grant_rejects_service_only_for_user — already pins composition rule (4) for AuditWrite at the grant-time edge (verify item ‘User-principal token requesting audit:write at grant time is rejected’).”) @yah:verify(“Parent relay smoke: cargo test -p cheers-core -p cheers-server -p cheers-verify -p cheers-axum + cargo test -p cheers-sqlx –features sqlite — all green; cargo check –workspace –all-features clean.”)

Structs§

NewUser
Fields required to mint a fresh User.
RefreshTokenRecord
One row in a refresh-token rotation chain.

Enums§

ProviderKey
The external identity-provider namespace a subject string lives in.

Traits§

PasskeyCredentialStore
Origin-side multi-passkey-per-user persistence for the WebAuthn relying-party flow (P7 / R014).
RefreshStore
Persistence for refresh-token rotation chains.
UserStore
User identity + provider-link persistence.