@web-ts-toolkit/oidc-vault-dpop-client 0.50.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (7) hide show
  1. package/README.md +483 -73
  2. package/index.d.mts +182 -81
  3. package/index.d.ts +182 -81
  4. package/index.js +943 -226
  5. package/index.mjs +946 -229
  6. package/llms.txt +52 -37
  7. package/package.json +2 -2
package/index.d.mts CHANGED
@@ -1,26 +1,26 @@
1
- /**
2
- * Browser-only DPoP client (CLIENT-02 port of apps/oidc-vault-dpop-example/src/auth/*).
3
- * No `node:*` or `express` imports. Browser globals (crypto.subtle, indexedDB,
4
- * sessionStorage, navigator.locks, BroadcastChannel) only behind
5
- * assertDpopBrowserFeatures / lazy factory calls, never at module top-level.
6
- */
7
-
1
+ /** Public-only P-256 JWK carried in proof headers. */
8
2
  interface PublicDpopJwk {
9
3
  kty: 'EC';
10
4
  crv: 'P-256';
11
5
  x: string;
12
6
  y: string;
13
7
  }
8
+ /** Validated persistent signing key and its canonical RFC 7638 thumbprint. */
14
9
  interface DpopKey {
10
+ /** Non-extractable ES256 signing key; possession does not itself imply a live session. */
15
11
  readonly privateKey: CryptoKey;
16
12
  readonly publicJwk: Readonly<PublicDpopJwk>;
17
13
  readonly jkt: string;
18
14
  readonly scopeId: string;
19
15
  }
20
16
 
17
+ /** Persistent key identity; use the same public origins/mount as the session and backend. */
21
18
  interface DpopKeyScope {
19
+ /** Browser location.origin, HTTPS or loopback HTTP. */
22
20
  frontendOrigin: string;
21
+ /** Public vault origin, HTTPS or loopback HTTP, with no path/query/fragment. */
23
22
  backendOrigin: string;
23
+ /** Absolute public mount; unreserved escapes, dot segments and trailing slashes normalize. */
24
24
  basePath: string;
25
25
  }
26
26
 
@@ -31,8 +31,9 @@ interface DpopKeyScope {
31
31
  * Call with the default `create=true` ONLY at fresh login (`session.login()`
32
32
  * does this for you). All credential-bearing operations use `create=false`,
33
33
  * so losing IndexedDB cannot silently rebind a login: a missing or changed
34
- * key throws `DPOP_KEY_LOST` and the caller must start a fresh login. There is
35
- * no ephemeral-key or Bearer fallback.
34
+ * key throws `DPOP_KEY_LOST` and the caller must start a fresh login. An
35
+ * unusable existing record needs explicit `session.resetKey()` before login;
36
+ * this helper never deletes or replaces it. No ephemeral-key or Bearer fallback.
36
37
  *
37
38
  * Candidate crypto work happens outside the readwrite transaction. IndexedDB
38
39
  * serializes the final read/add across tabs, making exactly one candidate win.
@@ -44,44 +45,39 @@ declare const getOrCreateDpopKey: (input: DpopKeyScope, options?: {
44
45
  create?: boolean;
45
46
  }) => Promise<DpopKey>;
46
47
 
48
+ /** Inputs for one fresh ES256 request proof; the caller owns the HTTP request and retry policy. */
47
49
  interface DpopProofInput {
50
+ /** Exact uppercase HTTP method. */
48
51
  method: string;
52
+ /** Absolute HTTP(S) target. Proof normalization omits query/fragment and preserves reserved path escapes. */
49
53
  url: string | URL;
50
54
  /** API only. Vault login/exchange/refresh/logout present no access token. */
51
55
  accessToken?: string;
56
+ /** Optional server-issued value; reuse only within the trusted protection space and signing key. */
52
57
  nonce?: string;
58
+ /** Epoch-millisecond clock; default Date.now. Proof iat is integer epoch seconds. */
53
59
  now?: () => number;
60
+ /** Reject with signal.reason before/after hashing and while signing; already-started Web Crypto may still finish. */
61
+ signal?: AbortSignal;
54
62
  }
55
63
  /**
56
64
  * Mint a fresh DPoP proof for exactly one request attempt with the scoped key.
57
65
  *
58
66
  * Every invocation signs a new proof: exact uppercase `htm`, canonical absolute
59
- * `htu` without query/fragment (see `normalizeDpopTarget`), current integer
67
+ * `htu` without query/fragment, current integer
60
68
  * `iat`, fresh 128-bit random `jti`, API-only `ath`, and the optional server
61
69
  * `nonce`. The protected header is `{ typ: 'dpop+jwt', alg: 'ES256', jwk:
62
70
  * <public P-256 JWK> }`. Call again — with a new `jti`/`iat`/signature — for
63
71
  * every nonce or refresh retry; never reuse a proof across attempts.
72
+ * Unreserved path escapes and dot segments normalize; reserved escapes stay
73
+ * encoded. This helper signs only: it does not send HTTP or enforce API scopes.
64
74
  *
65
75
  * Canonical import: `import { createDpopProof } from
66
76
  * '@web-ts-toolkit/oidc-vault-dpop-client'` (named root import).
67
77
  */
68
78
  declare const createDpopProof: (key: DpopKey, input: DpopProofInput) => Promise<string>;
69
79
 
70
- /** DPoP proof algorithms, independent of the app-local access token's signing algorithm. */
71
- type OidcVaultDpopAlgorithm = 'ES256' | 'PS256' | 'RS256';
72
- /**
73
- * Immutable persisted sender constraint. `jkt` is the canonical RFC 7638
74
- * SHA-256 JWK thumbprint (43 base64url characters). Store no JWK, private key,
75
- * proof algorithm, or historical enforcement mode in this record.
76
- */
77
- interface OidcVaultDpopBinding {
78
- type: 'dpop';
79
- jkt: string;
80
- }
81
- /** Verified request context; `alg` is checked against the current proof policy. */
82
- interface OidcVaultVerifiedDpopBinding extends OidcVaultDpopBinding {
83
- alg: OidcVaultDpopAlgorithm;
84
- }
80
+ /** Public JSON profile returned by exchange/refresh. */
85
81
  interface OidcVaultUserProfile {
86
82
  sub: string;
87
83
  email?: string;
@@ -89,46 +85,23 @@ interface OidcVaultUserProfile {
89
85
  preferredUsername?: string;
90
86
  [key: string]: unknown;
91
87
  }
92
- interface OidcVaultAccessTokenValidationResult {
93
- subject: string;
94
- sessionId?: string;
95
- scope?: string;
96
- claims?: Record<string, unknown>;
97
- /** Verified sender constraint. Legacy adapters may omit it only for genuinely unbound credentials. */
98
- confirmation?: OidcVaultAccessTokenConfirmation | null;
99
- }
100
- /** Canonical RFC 7638 SHA-256 thumbprint derived from verified token/introspection data, never from a proof alone. */
101
- interface OidcVaultAccessTokenConfirmation {
102
- jkt: string;
103
- }
104
- interface OidcVaultAuthContext extends OidcVaultAccessTokenValidationResult {
105
- token: string;
106
- /** Present only after original-key proof, API ath, nonce and shared replay admission all pass. */
107
- deviceBinding?: Readonly<OidcVaultVerifiedDpopBinding>;
108
- }
109
- declare module 'express-serve-static-core' {
110
- interface Request {
111
- auth?: OidcVaultAuthContext;
112
- }
113
- }
88
+ /** Vault handle transport; independent of DPoP mode. Default: body. */
114
89
  type OidcVaultSessionTransport = 'body' | 'cookie';
115
90
 
91
+ /** Contract-checked browser credential. Decoding does not authenticate the JWT; the API must verify it. */
116
92
  interface DpopAccessToken {
117
93
  readonly accessToken: string;
118
94
  readonly tokenType: 'DPoP';
95
+ /** Safe epoch milliseconds, capped by JWT exp and response/peer lifetime. May already be expired (e.g. expiresIn: 0); check before reuse. */
119
96
  readonly expiresAt: number;
97
+ /** Exact canonical RFC 7638 SHA-256 thumbprint from the one-field cnf object, matching the persistent browser key. */
120
98
  readonly jkt: string;
99
+ /** Opaque response generation for cookie-peer coordination, not a server session handle. */
121
100
  readonly generation: string;
122
101
  readonly user?: OidcVaultUserProfile;
123
102
  }
124
103
 
125
- /**
126
- * Browser-only DPoP client (CLIENT-02 port of apps/oidc-vault-dpop-example/src/auth/*).
127
- * No `node:*` or `express` imports. Browser globals (crypto.subtle, indexedDB,
128
- * sessionStorage, navigator.locks, BroadcastChannel) only behind
129
- * assertDpopBrowserFeatures / lazy factory calls, never at module top-level.
130
- */
131
- /** Copyable private example helper. Recognition is NOT proof of possession or an API sender constraint. */
104
+ /** Current opt-in recognition signal source. Recognition is not proof of possession or an API sender constraint. */
132
105
  type DeviceFingerprintSignalSource = () => Promise<string | undefined>;
133
106
  interface DeviceFingerprintOptions {
134
107
  /** Must match the backend's fingerprintRecognition.headerName; default X-Device-Fingerprint. */
@@ -161,30 +134,88 @@ interface FingerprintJsAgent {
161
134
  */
162
135
  declare const fingerprintJsSignalSource: (load: () => Promise<FingerprintJsAgent>) => DeviceFingerprintSignalSource;
163
136
 
137
+ /** Configuration for one browser context and public vault mount. */
164
138
  interface OidcVaultDpopSessionOptions {
139
+ /** Pinned public scheme/host/port, HTTPS or loopback HTTP; no path/query/fragment. */
165
140
  backendOrigin: string;
141
+ /** Exact public vault mount; default /auth/oidc. Trailing slashes normalize away. */
166
142
  basePath?: string;
143
+ /** Default body (tab-local handle). Cookie additionally requires Web Locks and BroadcastChannel. */
167
144
  sessionTransport?: OidcVaultSessionTransport;
168
145
  /** Current signal on login/exchange/refresh only; optional recognition, never PoP. */
169
146
  fingerprint?: DeviceFingerprint;
147
+ /**
148
+ * Per-attempt vault HTTP deadline in milliseconds, including headers, streamed
149
+ * body and JSON parsing. Default 15,000; integer 1..2,147,483,647. Starts at
150
+ * fetch, not queue/key/proof work. Timeout never automatically retries a mutation.
151
+ */
152
+ requestTimeoutMs?: number;
153
+ /**
154
+ * Maximum actual vault response bytes read from the decoded HTTP body stream,
155
+ * independent of Content-Length. Default 1,048,576 (1 MiB); positive safe integer.
156
+ */
157
+ maxResponseBytes?: number;
158
+ /** Default global fetch. Injected responses must expose a standard streaming body. */
170
159
  fetch?: typeof globalThis.fetch;
160
+ /** Login redirect after authoritative initiation; default location.assign. */
171
161
  navigate?: (authorizationUrl: string) => void;
162
+ /** Epoch-millisecond clock; default Date.now. Credential deadline arithmetic must remain safe. */
172
163
  now?: () => number;
173
164
  }
165
+ /** One tab's session. Both transports serialize lifecycle mutations; access JWTs stay in memory. */
174
166
  interface OidcVaultDpopSession {
167
+ /** Opaque persistent-key scope for this frontend origin, backend origin and normalized mount. */
175
168
  readonly scopeId: string;
176
169
  readonly sessionTransport: OidcVaultSessionTransport;
170
+ /** FIFO with exchange/refresh/logout. Clears frontend auth, creates a missing key and navigates after POST initiation. */
177
171
  login(returnTo?: string): Promise<void>;
172
+ /** FIFO. Exchanges the one-time callback code using the initiating key and temporary cookie. */
178
173
  exchange(code: string): Promise<DpopAccessToken>;
179
- /** Single-flight; no Authorization/ath even if the old JWT expired. */
174
+ /**
175
+ * Same Promise for concurrent callers until login/exchange/logout/clear/resetKey
176
+ * intervenes; the first caller's rejectedToken owns that batch. FIFO with
177
+ * other lifecycle mutations. No Authorization/ath, even with an expired JWT.
178
+ */
180
179
  refresh(options?: {
181
180
  rejectedToken?: string;
182
181
  }): Promise<DpopAccessToken>;
182
+ /** FIFO: an earlier refresh publishes its rotated handle before logout uses it. Revokes server refresh state. */
183
183
  logout(): Promise<void>;
184
+ /** Memory snapshot only; may already be expired, including a valid expiresIn: 0 response. */
184
185
  getAccessToken(): DpopAccessToken | undefined;
185
- /** Existing key only. Missing/changed persistence clears auth and requires login. */
186
+ /**
187
+ * Existing-key read alongside the lifecycle FIFO. A still-current
188
+ * missing/changed key clears auth; superseded reads cannot clear a newer
189
+ * login. Absence of credentials preserves an in-progress login marker.
190
+ */
186
191
  getKey(): Promise<DpopKey>;
192
+ /**
193
+ * Immediately invalidates local work/auth and pending login. Cookie mode
194
+ * resolves after inactive metadata/peer notification under the shared lock,
195
+ * which can wait for other lock owners. Aborts this context's vault HTTP.
196
+ * Keeps the key; no server logout.
197
+ */
187
198
  clear(): Promise<void>;
199
+ /**
200
+ * Explicit local key recovery, including corrupt/mismatched persisted keys.
201
+ * Immediately invalidates local auth/pending login and aborts owned vault HTTP;
202
+ * then atomically deletes only this scope's key AND cookie-session metadata,
203
+ * under the shared lock in cookie mode. Repeating on absent records is harmless;
204
+ * concurrent resets share removal until another lifecycle mutation intervenes.
205
+ * Await completion before login() creates a new non-extractable key through
206
+ * atomic first-key-wins. Never creates a key, calls the backend, removes an
207
+ * HttpOnly cookie, or revokes server sessions/JWTs. Other sessions using the old
208
+ * key need fresh login; old server sessions remain until revocation/expiry.
209
+ * Already-started API/crypto work and retained key references cannot be undone.
210
+ * Storage/other lock owners may delay completion. Disposed contexts reject.
211
+ */
212
+ resetKey(): Promise<void>;
213
+ /**
214
+ * Closes this context and peer waits immediately, preserving committed
215
+ * handles/key/metadata and pending login for reload. Aborts owned vault HTTP;
216
+ * does not log out, cancel caller-owned API requests, or undo server mutations.
217
+ * Await clear()/resetKey() first if their persistence work must finish. Later calls reject.
218
+ */
188
219
  dispose(): void;
189
220
  }
190
221
  /**
@@ -193,72 +224,142 @@ interface OidcVaultDpopSession {
193
224
  * '@web-ts-toolkit/oidc-vault-dpop-client'` (named root import, no default export).
194
225
  * Lifecycle: `login` → `exchange(code)` → `getAccessToken`/`fetchWithDpop` →
195
226
  * `refresh` → `logout`. Cookie transport additionally needs Web Locks +
196
- * `BroadcastChannel` at runtime (see `assertDpopBrowserFeatures`).
197
- * Copyable app helper. It is not an export of the Express package.
227
+ * `BroadcastChannel` at runtime, in addition to secure-context Web Crypto,
228
+ * sessionStorage and IndexedDB CryptoKey persistence.
229
+ * Mutating calls are ordered per instance in both transports; cookie mutations
230
+ * also hold the same-origin/scope Web Lock. clear/resetKey/dispose invalidate entry-time
231
+ * authority immediately. Superseded work rejects with non-terminal
232
+ * DPOP_OPERATION_SUPERSEDED (or DPOP_SESSION_DISPOSED after disposal).
233
+ * Vault HTTP has a per-attempt deadline/byte bound and aborts on invalidation.
234
+ * Valid response nonces are learned for this vault/key, including on success.
235
+ * Vault nonce challenges permit one retry with a fresh proof; a second yields
236
+ * non-terminal DPOP_NONCE_RETRY_EXHAUSTED. Only explicit terminal session,
237
+ * browser/device-binding or recognition errors clear auth, not generic 401s.
238
+ * Peer coordination failures are advisory and cannot clear auth. A new login
239
+ * intent supersedes older peer callbacks even while queued; validated current-key
240
+ * failures and explicit terminal operations retain their own failure cleanup.
241
+ * A lost response leaves server commit/cookie application uncertain: local
242
+ * cancellation cannot roll it back or coordinate across frontend origins.
198
243
  */
199
244
  declare const createOidcVaultDpopSession: (options: OidcVaultDpopSessionOptions) => OidcVaultDpopSession;
200
245
 
201
246
  /**
202
- * Browser-only DPoP client (CLIENT-02 port of apps/oidc-vault-dpop-example/src/auth/*).
203
- * No `node:*` or `express` imports. Browser globals (crypto.subtle, indexedDB,
204
- * sessionStorage, navigator.locks, BroadcastChannel) only behind
205
- * assertDpopBrowserFeatures / lazy factory calls, never at module top-level.
247
+ * In-memory opaque nonces, isolated by the caller's trusted protection space and
248
+ * signing-key thumbprint. At most 64 tuples: a successful remember renews write
249
+ * order; insertion at capacity evicts the oldest remembered tuple. Reads do not
250
+ * promote entries. No TTL or server-specific nonce decoding is performed.
206
251
  */
207
252
  declare class DpopNonceCache {
208
253
  private readonly nonces;
209
254
  get(space: string, jkt: string): string | undefined;
255
+ /**
256
+ * Accept one 1..512-character visible-ASCII value, excluding whitespace,
257
+ * double quotes, backslashes and commas (including coalesced duplicate header
258
+ * values). Invalid input returns false without overwriting or changing order.
259
+ */
210
260
  remember(space: string, jkt: string, nonce: string | null): boolean;
261
+ /** Forget every tuple in this cache; does not invalidate credentials or server nonce state. */
211
262
  clear(): void;
212
263
  }
213
264
 
214
265
  /**
215
- * Browser-only DPoP client (CLIENT-02 port of apps/oidc-vault-dpop-example/src/auth/*).
216
- * No `node:*` or `express` imports. Browser globals (crypto.subtle, indexedDB,
217
- * sessionStorage, navigator.locks, BroadcastChannel) only behind
218
- * assertDpopBrowserFeatures / lazy factory calls, never at module top-level.
266
+ * Credential/nonce policy for a public API subtree, selected by `fetchWithDpop`.
267
+ * @example
268
+ * const apis: DpopApi[] = [
269
+ * { origin: 'https://gateway.example.com', pathPrefix: '/orders', replayNamespace: 'orders-api' },
270
+ * { origin: 'https://gateway.example.com', pathPrefix: '/billing', replayNamespace: 'billing-api' },
271
+ * ];
219
272
  */
220
-
221
273
  interface DpopApi {
222
- /** Exact origin receiving the proof/Authorization. No wildcard or redirects. */
274
+ /**
275
+ * Exact public scheme/host/port receiving auth, with an optional root slash.
276
+ * HTTPS or loopback HTTP; no path, query, fragment, wildcard or redirects.
277
+ */
223
278
  origin: string;
224
- /** Backend API replayNamespace (the nonce protection space), not a route. */
279
+ /**
280
+ * Absolute path subtree. Omission or `/` permits the entire origin. Matches
281
+ * segment boundaries (`/orders` or `/orders/...`, never `/orders-evil`);
282
+ * the longest normalized matching prefix wins, independent of array order.
283
+ * Trailing slashes are removed; unreserved percent escapes and dot segments
284
+ * normalize as in the proof/outbound path. A prefix normalizing to `/` is
285
+ * origin-wide too. Reserved escapes remain encoded.
286
+ * No URL, `//` authority-like prefix, query, fragment, whitespace, backslash,
287
+ * or malformed escape. Duplicate normalized origin/path scopes are rejected,
288
+ * even if their namespace/credential policies are identical.
289
+ */
290
+ pathPrefix?: string;
291
+ /**
292
+ * Backend API replayNamespace: 1–128 printable ASCII characters. Nonces use
293
+ * the selected `['api', normalizedOrigin, replayNamespace]` plus signing jkt;
294
+ * paths with the same origin/namespace intentionally share that nonce space.
295
+ */
225
296
  replayNamespace: string;
226
- /** Usually omit for JWT APIs; include only for an explicitly cookie-using API. */
297
+ /** Default `omit`. Use `include` only for a cookie-using API. Snapshotted before auth awaits. */
227
298
  credentials?: 'omit' | 'include';
228
299
  }
229
300
  interface DpopFetchContext {
230
301
  session: OidcVaultDpopSession;
302
+ /** All scopes are validated on each invocation, including entries that do not match its target. */
231
303
  apis: readonly DpopApi[];
232
304
  fetch?: typeof globalThis.fetch;
305
+ /** Optional shared cache; each instance holds at most 64 protection-space/key tuples. */
233
306
  nonces?: DpopNonceCache;
234
307
  now?: () => number;
235
308
  }
236
309
  interface DpopFetchOptions extends Omit<RequestInit, 'redirect' | 'credentials'> {
310
+ /**
311
+ * Abort this caller's wait before/through auth, proof and HTTP work. Pre-aborted
312
+ * calls do no auth work. Shared refresh/key work continues for other waiters;
313
+ * already-started Web Crypto/server mutations cannot be undone.
314
+ */
315
+ signal?: AbortSignal | null;
237
316
  /**
238
317
  * GET/HEAD/OPTIONS may retry. All other methods default to one attempt.
239
318
  * Explicitly authorize idempotent retries only for a server operation whose
240
319
  * contract supports them (e.g. PUT or POST with a server idempotency key).
320
+ * Nonce learning still occurs on single-attempt and success responses, so a
321
+ * later explicit invocation can use the nonce without resubmitting this call.
241
322
  */
242
323
  retry?: 'never' | 'idempotent';
243
324
  }
244
325
  /**
245
- * Scoped DPoP `fetch` for one configured API origin: mints a fresh proof per
246
- * attempt, sends `Authorization: DPoP`, then retries once on a DPoP nonce
247
- * challenge and at most once via `session.refresh()` on `invalid_token`.
326
+ * Scoped DPoP `fetch` for a configured API origin/path: mints a fresh proof per
327
+ * attempt and sends `Authorization: DPoP`. When the retry policy permits it,
328
+ * retries once on a DPoP nonce challenge and at most once via `session.refresh()`
329
+ * on `invalid_token`. Learns valid response nonces even on single-attempt,
330
+ * success and final challenge responses, isolated by protection space/key.
331
+ * Only an unambiguous HTTP-401 DPoP challenge authorizes recovery; parameter
332
+ * order is flexible, but malformed/duplicate challenges and fields over 8,192
333
+ * characters do not trigger retries. Final API responses belong to the caller.
248
334
  * Canonical import: `import { fetchWithDpop } from
249
- * '@web-ts-toolkit/oidc-vault-dpop-client'` (named root import). Only origins
250
- * listed in `context.apis` are called; authentication headers are owned by the
251
- * helper and must not be set by callers.
335
+ * '@web-ts-toolkit/oidc-vault-dpop-client'` (named root import). Selects the
336
+ * longest normalized segment-prefix match in `context.apis`; omission/root is
337
+ * origin-wide. Validates all scopes and snapshots the target and selected
338
+ * origin/path/namespace/credentials before auth awaits. A missing match rejects
339
+ * with DPOP_API_OUT_OF_SCOPE before credential/key/refresh/network work. Bad
340
+ * policies or duplicate normalized scopes reject with INVALID_DPOP_API_CONFIG;
341
+ * malformed paths/origins retain INVALID_DPOP_TARGET / INSECURE_DPOP_ORIGIN.
342
+ * All are non-terminal. Authentication headers are owned by the helper.
343
+ * Outbound path normalization matches the proof: unreserved escapes and dot
344
+ * segments normalize, reserved escapes stay encoded, and query/fragment data
345
+ * is preserved for fetch (HTTP omits fragments). Neither enters the proof.
346
+ * The uppercase method is the same on the proof and every outbound attempt.
347
+ * A nonlive deadline (including an expiresIn: 0 refresh result) stops the
348
+ * attempt with DPOP_ACCESS_TOKEN_EXPIRED, requiresLogin: false. The rotated
349
+ * session handle is retained; no additional refresh loop is started.
350
+ * Caller cancellation rejects with signal.reason, including while waiting on
351
+ * shared refresh/key work, without cancelling that shared work. Discarded retry
352
+ * responses are cancelled best effort; cleanup never delays or replaces a result.
252
353
  */
253
354
  declare const fetchWithDpop: (context: DpopFetchContext, input: string | URL, options?: DpopFetchOptions) => Promise<Response>;
254
355
 
255
356
  /**
256
- * Browser-only DPoP client (CLIENT-02 port of apps/oidc-vault-dpop-example/src/auth/*).
257
- * No `node:*` or `express` imports. Browser globals (crypto.subtle, indexedDB,
258
- * sessionStorage, navigator.locks, BroadcastChannel) only behind
259
- * assertDpopBrowserFeatures / lazy factory calls, never at module top-level.
357
+ * Built-in failures use fixed codes/messages, never server text or credentials.
358
+ * requiresLogin identifies terminal session/binding/recognition or local key
359
+ * failures, not every HTTP 401. Non-terminal errors retain auth and do not by
360
+ * themselves authorize an automatic retry. Unknown vault codes are sanitized to
361
+ * OIDC_REQUEST_FAILED with requiresLogin: false; status remains available.
260
362
  */
261
- /** Fixed local failures; never include keys, credentials, fingerprints or URLs. */
262
363
  declare class OidcVaultDpopClientError extends Error {
263
364
  readonly code: string;
264
365
  readonly requiresLogin: boolean;