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 Veccargo 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/
Structs§
- NewUser
- Fields required to mint a fresh
User. - Refresh
Token Record - One row in a refresh-token rotation chain.
Enums§
- Provider
Key - The external identity-provider namespace a
subjectstring lives in.
Traits§
- Passkey
Credential Store - Origin-side multi-passkey-per-user persistence for the WebAuthn relying-party flow (P7 / R014).
- Refresh
Store - Persistence for refresh-token rotation chains.
- User
Store - User identity + provider-link persistence.