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
//! The SDK family's OAuth2 scope-request contract.
//!
//! Each face SDK owns a **sealed** family of phantom scope-set markers —
//! `pcs_session::scopes::{ReadOnly, Notify, ReadAndSend, FullAccess}`,
//! `pas_plims::PlimsScopeSet`, `pcs_external::PcsExternalScopeSet` — whose
//! members gate which of that face's methods compile. Those families live
//! beside the behavior they gate and stay there; this module contributes the
//! one thing they cannot express on their own: a **common contract naming the
//! scope atoms a marker requests at consent**, so a crate that performs the
//! *authorization* leg can read a marker it has never heard of.
//!
//! That is the whole reason this is here rather than in one of the SDKs.
//! `pas-external` runs the OAuth flow; `pcs-session` owns the chat scope
//! vocabulary; neither depends on the other, and adding an edge either way
//! would drag an HTTP/OAuth stack into a chat client or a chat client into an
//! auth SDK. Both already depend on this crate, feature-free. The contract
//! goes in the shared node, and one type parameter then spans the whole
//! credential story:
//!
//! ```ignore
//! NativeAuthFlow::<Notify> // acquisition — puts Notify's atoms on the authorize wire
//! PcsSessionClient<Notify> // use — compile-gates the callable RPCs to the same tier
//! ```
//!
//! # Open parent, sealed children
//!
//! [`ConsentScopes`] is deliberately **not** sealed, and this is the only
//! workable reading of the "sealed-trait family parent" slot reserved in this
//! crate's `lib.rs`: a sealed parent could not be implemented by
//! `pcs-session` at all, because sealing means "only the declaring crate may
//! implement this". Each SDK's own family stays sealed exactly as before —
//! sealing restricts who may implement *that family's* trait, not which other
//! traits its members implement, so nothing is loosened by adopting a
//! supertrait here.
//!
//! # It provides coherence, not authorization
//!
//! Implementing [`ConsentScopes`] grants nothing. The atoms are what the
//! client *asks for*; the server decides what to give. PAS rejects unknown or
//! un-allowlisted scopes loudly at the authorize endpoint (`invalid_scope`) —
//! it never silently intersects them down to what the client is entitled to.
//! The authorization boundary is, and stays, server-side.
/// The OAuth2 scope atoms a marker type requests at consent.
///
/// Implemented by each face SDK for its own sealed scope-set markers. A crate
/// running the authorization leg takes `S: ConsentScopes` and puts
/// [`scope_line`](ConsentScopes::scope_line) on the wire without knowing which
/// SDK `S` came from.
///
/// # Atoms, not a joined string
///
/// [`SCOPES`](ConsentScopes::SCOPES) is a slice because two things need set
/// semantics, and neither can be had from a pre-joined string:
///
/// 1. **The granted-vs-requested covers-check.** RFC 6749 §5.1 lets the token
/// response echo the granted scope; §3.3 makes order and whitespace
/// insignificant. So the check is `granted ⊇ requested` over atom sets —
/// never string equality, which would reject a reordered echo.
/// 2. **Per-atom drift pinning.** Workspace tests assert every atom of every
/// family is present in the PAS scope catalog and in the PCS capability
/// map. Set membership is the assertion; a joined string would have to be
/// re-split by every test.
///
/// **Do not add a parallel joined const.** Two declarations of the same fact
/// is precisely the drift this contract exists to remove — the joined form is
/// derived on demand by `scope_line`.
///
/// # Membership is not this trait's business
///
/// Which atoms a tier contains is the owning SDK's decision and changes on the
/// owning SDK's release cadence alone. This trait's shape is
/// membership-invariant: a tier gaining an atom releases one SDK minor and
/// requires nothing from this crate. (Server-first, always — the atom must be
/// live in the PAS catalog and the client's allowlist before a binary requests
/// it, or authorize fails loud. The owning SDK's `scopes` module carries the
/// full runbook.)
/// A [`TokenSource`](crate::token_cache::TokenSource) whose credential has
/// been **checked to cover** `S`'s atoms.
///
/// This is a witness, not a capability: implementing it asserts that at the
/// moment the token was obtained, the server's granted scope was verified to
/// contain every atom in `S::SCOPES` (absent echo = granted-as-requested,
/// RFC 6749 §5.1). A consumer can then require the *same* `S` for acquisition
/// and for use, so a credential requested at one tier cannot be fed to a
/// client built at another — a mismatch that otherwise compiles cleanly and
/// fails as a runtime 403 on the first call that needs the missing atom.
///
/// # What it does not do
///
/// Rust has no cross-crate privacy sharing, so this marker is **not
/// unforgeable** — a determined consumer can implement it on a type where no
/// check ever ran. Containment is by constructor discipline: the SDK
/// implements it only where a covers-check has actually run, and any public
/// constructor that yields one must itself take the granted scope line and run
/// the check. That makes the coherent path the easy path, which is the honest
/// claim; the server-side scope enforcement remains the authorization
/// boundary. Dev/test sources that cannot know their grant (a statically
/// supplied token) deliberately do **not** implement this, so reaching for one
/// is opt-out by construction rather than an accident.
///
/// Gated on `token-cache`, the feature that owns
/// [`TokenSource`](crate::token_cache::TokenSource); every SDK that mints or
/// renews credentials already enables it.