1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
//! Client-side persistence contract — [`CredentialStore`] + the shared
//! [`StoreError`].
//!
//! `cheers-core` keeps only the *device-side* store trait: [`CredentialStore`],
//! the opaque-blob credential storage a native client (keyring, encrypted-file,
//! in-memory) implements. The *origin-side* store traits — `UserStore` and
//! `RefreshStore` — moved to `cheers-server` (R019-F6), so a verify-only or
//! device-only consumer never even names them. [`StoreError`] stays here: it's
//! the shared error every store and the revocation traits return.
//!
//! All traits are `async` via [`async_trait`] so they remain dyn-compatible.
//!
//! `StoreError` here is the **adapter-facing** error; R007-T4 lands the
//! workspace-wide error hierarchy and re-exports a unified type.
//!
//! @yah:ticket(R019-F4, "Revocation read/write split: RevocationWriter (origin) + RevocationReader (edge)")
//! @yah:assignee(agent:claude)
//! @yah:at(2026-05-26T17:52:56Z)
//! @yah:status(review)
//! @yah:parent(R019)
//! @yah:next("Promote store.rs's 'cheers does not enforce revocation server-side; the product wires up the check' note into two traits: RevocationWriter { revoke(jti | chain) } (origin, Yubaba Redis/gossip) and RevocationReader { is_revoked(jti) } (edge, local replica / CF KV).")
//! @yah:next("Eventually-consistent by documented contract; the short access-token TTL is the stated propagation bound. Wire revoke() into logout + UserStore::revoke_device + RefreshStore::revoke_chain.")
//! @yah:next("Keyed on the token's jti — depends on the Claims.jti field added alongside the facades feature.")
//! @yah:verify("cd external/cheers && cargo test -p cheers-core")
//! @arch:see(.yah/docs/working/edge-verifiable-auth.md)
//! @yah:handoff("Landed RevocationReader{is_revoked(jti)} + RevocationWriter{revoke(jti)} in new revocation.rs, exported from lib.rs. Reader = edge hot path (point membership check), Writer = origin cold path; both async + Send+Sync + dyn-compatible, mirroring the store.rs traits. The read/write split is the same capability-by-type discipline as TokenVerifier/TokenMinter.")
//! @yah:handoff("Settled the 'revoke(jti | chain)' shape: the WRITER is jti-only. Chain/device revocation = RefreshStore::revoke_chain (blocks re-issue on the cold path) composed with per-jti revoke + natural expiry of in-flight access tokens within the access TTL. The module doc owns the full eventually-consistent contract (revoke propagates async; access-token TTL is the staleness bound; sound because auth has no cross-session OLTP). store.rs revoke_device doc promoted to point at the new traits.")
//! @yah:handoff("jti landed on Claims (claims.rs) as F4's revocation key — nominally an F3 line-item, but F4 keys on it so it moved up. #[serde(default, skip_serializing_if=String::is_empty)] keeps the wire/cookie format byte-identical when unset; with_jti() builder; Claims::new() kept at 5 args so existing + cross-camp (mesofact R009) call sites still compile.")
//! @yah:handoff("Verified GREEN: cargo test -p cheers-core (45 unit incl. 4 revocation + jti tests, 9 proptest, 3 doctest) + cargo check --workspace --all-features. NOTE: revocation.rs + store.rs doc-link to crate::session::* (SessionAuthority/EdgeVerifier/SessionPolicy), which land in R019-F3 — forward refs that resolve when F3 lands; cargo test/check don't validate intra-doc links, only cargo doc does.")
//! @yah:handoff("Facade-level wiring (SessionAuthority composing revoke_chain + revoke; EdgeVerifier consulting is_revoked after signature check) is R019-F3 — picked up next per the maintainer's F4-first ordering.")
use async_trait;
use HashMap;
use Mutex;
use crateCredential;
/// Errors a store impl may return.
/// Opaque-blob credential storage, keyed by a caller-chosen string.
///
/// The one store trait the device tier needs: native-client features (P8)
/// implement it over keyring, encrypted-file, or in-memory backends.
/// `Credential::material` is the provider-specific blob; cheers-core does not
/// interpret it. Kept in `cheers-core` (not `cheers-server`) because the client
/// stores credentials without ever touching a token codec.
/// Single-use tracking for magic-link tokens (`cheers::email::magic_link`).
///
/// Implementors should hold each `jti` until at least `expires_at` so a
/// token cannot be replayed before it would have expired anyway. After
/// expiry the entry can be GC'd — the codec's own expiry check will reject
/// any token whose record is missing.
///
/// R727-B1: declared here (not in the `cheers` crate, where the magic-link
/// codec lives) so `cheers-turso` and `cheers-sqlx` — which depend on
/// `cheers-core` but not on `cheers` — can implement it directly. Re-exported
/// from `cheers::email::magic_link` so existing call sites keep resolving.
/// In-process [`UsedJtiStore`] backed by a `Mutex<HashMap>`. For tests, dev,
/// and single-replica deployments. Production multi-replica deployments
/// want a shared backend — see `cheers_turso::TursoUsedJtiStore` /
/// `cheers_sqlx::SqliteUsedJtiStore`.