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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
//! [`DeviceResolver`]: the request-time bridge from raw HTTP requests to a
//! [`DeviceId`].
//!
//! # What an implementation does
//!
//! Per the design in [`docs/identity/device.md`](../../../../docs/identity/device.md),
//! a production `DeviceResolver` performs:
//!
//! 1. Parse the long-lived device-binding cookie from `Cookie:` header (if
//! present) → candidate [`DeviceId`].
//! 2. Compute the keyed [`FingerprintHash`](super::types::FingerprintHash)
//! from `User-Agent`, `Accept-Language`, `Accept`, and any other inputs
//! configured per tenant.
//! 3. If the cookie supplied an id, [`DeviceStore::load`](super::store::DeviceStore::load) it and verify the
//! stored fingerprint matches → emit `DeviceFingerprintMismatch
//! Suspicious` on mismatch, return `None` (caller invalidates the
//! session). Match → return that id and call
//! [`DeviceStore::record_sighting`](super::store::DeviceStore::record_sighting).
//! 4. If no cookie, [`DeviceStore::find_by_fingerprint`](super::store::DeviceStore::find_by_fingerprint) on `(tenant, hash)`
//! → return that id (and emit `DeviceFirstSeen` if newly inserted).
//! 5. If neither path resolves, [`DeviceStore::save`](super::store::DeviceStore::save) a fresh `Unknown`
//! [`Device`](super::types::Device) and return its id, emitting
//! `DeviceFirstSeen`.
//!
//! All of (1) through (5) is application-glue: the resolver implementation owns
//! both the [`DeviceStore`] and the per-tenant fingerprint key registry,
//! and decides how to extract tenant context from request headers /
//! routing / TLS SNI / extensions populated by upstream middleware.
//!
//! # Layer integration
//!
//! [`SessionLayer::with_device_resolver`](crate::session::SessionLayer::with_device_resolver)
//! wires a `DeviceResolver` into the session middleware. On every request
//! the layer calls [`DeviceResolver::resolve`] before the inner handler
//! runs and stamps the result onto
//! [`SessionData::device_id`](crate::session::SessionData::device_id),
//! marking the session modified iff the value changed (so the new id is
//! persisted on response).
//!
//! Errors returned by `resolve` are logged and treated as `None`; device
//! resolution is best-effort and never causes the request to fail.
//!
//! [`DeviceStore`]: super::store::DeviceStore
use crateDeviceId;
use Parts;
use Future;
use Pin;
/// Resolve (or create) the [`DeviceId`] associated with an inbound HTTP
/// request.
///
/// Implementors hold whatever state they need (a [`DeviceStore`], a tenant
/// fingerprint key registry, etc.) and produce a `DeviceId` per request.
///
/// # Failure semantics
///
/// `Ok(None)` means "no device could be associated with this request",
/// not an error. Reserve `Self::Error` for genuine storage / configuration
/// faults the caller should propagate. The session layer treats `Ok(None)`
/// as a no-op (leaves
/// [`SessionData::device_id`](crate::session::SessionData::device_id) at
/// `None`); it logs `Err(_)` and continues with `None`.
///
/// # Tenant context
///
/// Implementations that need tenant scoping should read the [`TenantId`]
/// from `request.extensions()` populated by an upstream tenant-resolver
/// middleware. The trait does not pass tenant explicitly because the
/// session layer that drives the resolver is itself tenant-agnostic.
///
/// [`DeviceStore`]: super::store::DeviceStore
/// [`TenantId`]: crate::authn::ids::TenantId
/// No-op resolver used as the default plug when the `device` feature is on
/// but the application has not configured a real
/// [`DeviceResolver`].
///
/// Always returns `Ok(None)`, leaving
/// [`SessionData::device_id`](crate::session::SessionData::device_id) at
/// `None`. Applications that want device tracking must replace this with
/// an implementation backed by a [`DeviceStore`](super::store::DeviceStore).
;
// ── ErasedDeviceResolver ──────────────────────────────────────────────────────
/// Internal dyn-safe wrapper around [`DeviceResolver`].
///
/// The user-facing trait uses RPITIT (`impl Future<...>`) and an associated
/// `Error` type, neither of which is dyn-compatible today. The session
/// layer needs to hold an arbitrary resolver behind `Arc<dyn ...>` so the
/// layer's type signature does not pick up an extra generic parameter that
/// would ripple through every `SessionLayer::new` call site.
///
/// This trait closes the gap: a blanket impl for every `R: DeviceResolver`
/// boxes the future and swallows the error to a `tracing::warn!` log,
/// honouring the documented best-effort contract.
pub