@web-ts-toolkit/express-oidc-vault 0.43.0 → 0.48.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 (6) hide show
  1. package/README.md +1217 -88
  2. package/index.d.mts +697 -15
  3. package/index.d.ts +697 -15
  4. package/index.js +2494 -487
  5. package/index.mjs +2503 -497
  6. package/package.json +6 -4
package/index.d.ts CHANGED
@@ -3,6 +3,97 @@ import { KeyObject } from 'node:crypto';
3
3
  import { JWK } from 'jose';
4
4
 
5
5
  type OidcVaultRouteName = 'login' | 'callback' | 'exchange' | 'refresh' | 'logout' | 'backchannel-logout';
6
+ /** Enabling the option defaults to `optional`; omitting it keeps bearer behavior. */
7
+ type OidcVaultDeviceBindingMode = 'optional' | 'required';
8
+ /** DPoP proof algorithms, independent of the app-local access token's signing algorithm. */
9
+ type OidcVaultDpopAlgorithm = 'ES256' | 'PS256' | 'RS256';
10
+ /**
11
+ * Immutable persisted sender constraint. `jkt` is the canonical RFC 7638
12
+ * SHA-256 JWK thumbprint (43 base64url characters). Store no JWK, private key,
13
+ * proof algorithm, or historical enforcement mode in this record.
14
+ */
15
+ interface OidcVaultDpopBinding {
16
+ type: 'dpop';
17
+ jkt: string;
18
+ }
19
+ /** Verified request context; `alg` is checked against the current proof policy. */
20
+ interface OidcVaultVerifiedDpopBinding extends OidcVaultDpopBinding {
21
+ alg: OidcVaultDpopAlgorithm;
22
+ }
23
+ /**
24
+ * Optional stateless, key/context-bound DPoP nonce policy. Challenges are issued
25
+ * only after other proof/target checks and before replay reservation. Any
26
+ * authentic live nonce permits parallel fresh proofs; it does not replace the
27
+ * normal iat window or single-use JTI reservation. Shared instances need the
28
+ * same secret; rotation requires a new challenge, with no previous-secret list.
29
+ */
30
+ interface OidcVaultDpopNonceOptions {
31
+ /** Shared random secret of at least 32 bytes, copied at construction. */
32
+ secret: Uint8Array;
33
+ /** Integer lifetime in seconds, 1–300; default 60. */
34
+ lifetimeSeconds?: number;
35
+ }
36
+ /**
37
+ * Shared RFC 9449 proof profile for vault POSTs and request-aware APIs.
38
+ * Proofs are limited to 8192 wire bytes and 2048 decoded protected-header/JWK
39
+ * bytes, use typ dpop+jwt and public-only asymmetric keys, and require exact
40
+ * method/pinned normalized URL plus a signed printable ASCII JTI (1–128 bytes).
41
+ * Signature verification precedes claims; strict iat validity is
42
+ * nowSeconds - age - skew < iat <= nowSeconds + skew. APIs additionally match
43
+ * original verified cnf.jkt and SHA-256 ath of the ASCII access token.
44
+ */
45
+ interface OidcVaultDpopProofOptions {
46
+ /** Nonempty asymmetric allowlist; default `['ES256']` (P-256). RSA proofs require 2048–4096 bits. */
47
+ algorithms?: readonly OidcVaultDpopAlgorithm[];
48
+ /** Integer maximum proof age in seconds, 1–300; default 60. */
49
+ proofMaxAgeSeconds?: number;
50
+ /** Integer clock tolerance in seconds, 0–30; default 5. */
51
+ clockSkewSeconds?: number;
52
+ /** Off by default. Challenges are at most 512 bytes; retry once with a fresh proof, never reuse its JTI. */
53
+ nonce?: false | OidcVaultDpopNonceOptions;
54
+ }
55
+ /**
56
+ * Opt-in sender constraint. `optional` permits legacy unbound records without
57
+ * enrolling them from a later proof; `required` rejects unbound use. Persisted
58
+ * bound records never downgrade when the option or a proof is omitted.
59
+ */
60
+ interface OidcVaultDeviceBindingOptions extends OidcVaultDpopProofOptions {
61
+ /** Default optional when configured. Required rejects unbound use; there is no legacy downgrade switch. */
62
+ mode?: OidcVaultDeviceBindingMode;
63
+ }
64
+ /**
65
+ * Opt-in, vendor-independent browser recognition/change detection, NOT proof of
66
+ * possession. A copyable signal never satisfies required DPoP or constrains API
67
+ * access. POST login alone enrolls a supplied signal; absence is intentionally
68
+ * unenrolled. Callback stores only SHA-256 in reserved session metadata.
69
+ */
70
+ interface OidcVaultFingerprintRecognitionOptions {
71
+ /**
72
+ * HTTP field name; default X-Device-Fingerprint. Must not collide with auth,
73
+ * cookie, origin, content, or transport headers. One raw field with a nonempty
74
+ * printable ASCII value, at most 256 bytes; no trimming or normalization.
75
+ * Malformed/duplicate/oversized opted-in signals return 400
76
+ * OIDC_VAULT_INVALID_FINGERPRINT before credential work.
77
+ */
78
+ headerName?: string;
79
+ }
80
+ /**
81
+ * Request-aware API sender constraint. Every API accepting bound JWTs must
82
+ * enforce this policy; signature-only validation does not prove possession.
83
+ * Construction snapshots plain options, URLs, algorithms and nonce bytes.
84
+ */
85
+ interface OidcVaultApiDeviceBindingOptions extends OidcVaultDeviceBindingOptions {
86
+ /** Static HTTPS origin (loopback HTTP development allowed); never derived from Host/proxy headers. */
87
+ publicOrigin: string;
88
+ /** External mount prefix stripped by a proxy, normalized at construction; default empty. No query/fragment. */
89
+ publicPathPrefix?: string;
90
+ /** Shared static printable ASCII protection-space label, 1–128 bytes; never a token, route or instance ID. */
91
+ replayNamespace: string;
92
+ /** Explicit shared atomic service, normally the vault store. No per-middleware fallback. */
93
+ replayStore: OidcVaultDpopReplayStore;
94
+ /** Epoch-millisecond clock for proof age/nonce/replay; default Date.now. Shared instances need synchronized clocks. */
95
+ now?: () => number;
96
+ }
6
97
  interface OidcVaultUserProfile {
7
98
  sub: string;
8
99
  email?: string;
@@ -10,6 +101,21 @@ interface OidcVaultUserProfile {
10
101
  preferredUsername?: string;
11
102
  [key: string]: unknown;
12
103
  }
104
+ /**
105
+ * Stored session identity. Exchange, refresh, and logout compare each defined
106
+ * field verbatim against resolved middleware config before use. Opt-in/guarded
107
+ * exchange checks before code consumption; built-in logout also checks the
108
+ * surviving lineage through unexpired aliases before proof or deletion.
109
+ * Known mismatches fail with 401 `OIDC_VAULT_INVALID_SESSION` without changing
110
+ * the session or cookie. Issuer trailing-slash variants are distinct.
111
+ *
112
+ * Omitted fields remain legacy-compatible and are not backfilled by refresh.
113
+ * Use separate session/alias, exchange-code, and transaction namespaces for
114
+ * complete isolation. Disabled legacy bearer exchange retains its historical
115
+ * consume-before-identity ordering; old custom bearer stores without revocation
116
+ * context retain their legacy alias behavior. Missing identifiers are not an
117
+ * isolation guarantee.
118
+ */
13
119
  interface OidcVaultProviderMetadata {
14
120
  issuer?: string;
15
121
  clientId?: string;
@@ -25,6 +131,8 @@ interface OidcVaultSession {
25
131
  subject: string;
26
132
  providerSessionId?: string;
27
133
  provider?: OidcVaultProviderMetadata;
134
+ /** Original login-selected sender constraint, enforced before exchange/refresh/logout (including aliases); never enrolled by a later proof. */
135
+ deviceBinding?: OidcVaultDpopBinding;
28
136
  refreshToken: string;
29
137
  idToken: string;
30
138
  accessToken?: string;
@@ -35,11 +143,21 @@ interface OidcVaultSession {
35
143
  * This controls the lifetime of the refresh-token-backed server-side session.
36
144
  * It is not derived from upstream OAuth `expires_in`, which only describes the
37
145
  * upstream access token lifetime. Leave unset when your application or store
38
- * owns session lifetime through another policy.
146
+ * owns session lifetime through another policy. `sessionTtlMs` assigns an
147
+ * absolute maximum at callback creation; a precreate hook may shorten it.
148
+ * Refresh preserves this timestamp rather than renewing the lifetime.
39
149
  */
40
150
  expiresAt?: number;
41
151
  createdAt: number;
42
152
  updatedAt: number;
153
+ /**
154
+ * Verified session profile. On refresh with a new `id_token`, the profile
155
+ * is rebuilt from fresh verified ID claims overlaid with freshly fetched
156
+ * matching UserInfo; retained values are never carried forward, so removed
157
+ * provider claims disappear. Application custom attributes belong in
158
+ * `metadata`, not in `user`. Without a new `id_token`, the retained
159
+ * profile is kept verbatim (fresh UserInfo still overlays per key).
160
+ */
43
161
  user?: OidcVaultUserProfile;
44
162
  /**
45
163
  * Store-portable application metadata.
@@ -49,6 +167,11 @@ interface OidcVaultSession {
49
167
  * serialization round-trips, so callers must not rely on object identity,
50
168
  * custom prototypes, functions, symbols, Dates, Maps, Sets, undefined object
51
169
  * properties, or other runtime-only values surviving persistence.
170
+ * `oidcVaultFingerprintRecognition` is reserved: `{ version: 1, hash }` is
171
+ * immutable login-selected SHA-256 recognition evidence, never raw signal
172
+ * data. Core preserves it through callback/refresh and removes it from token
173
+ * issuer input and public profiles/responses. Disclose collection/retention;
174
+ * it lasts with the transaction/session, not the local access-token lifetime.
52
175
  */
53
176
  metadata?: Record<string, unknown>;
54
177
  }
@@ -58,9 +181,19 @@ interface AuthorizationTransactionInput {
58
181
  pkceVerifier: string;
59
182
  codeChallenge: string;
60
183
  returnTo?: string;
184
+ /** Login-selected sender constraint, never reconstructed from a later proof. */
185
+ deviceBinding?: OidcVaultDpopBinding;
186
+ /** SHA-256 hash of the temporary HttpOnly browser-binding cookie; never store the cookie secret. */
187
+ browserBindingHash?: string;
61
188
  createdAt: number;
62
189
  expiresAt: number;
63
- /** See `OidcVaultSession.metadata` for the portable metadata value domain. */
190
+ /**
191
+ * See `OidcVaultSession.metadata` for the portable value domain. POST login
192
+ * reserves `oidcVaultTransactionProvider` for its resolved issuer/clientId;
193
+ * stores must preserve it for callback identity preflight. Opted-in POST login
194
+ * also carries immutable `oidcVaultFingerprintRecognition` for callback
195
+ * session creation; no raw fingerprint signal is stored.
196
+ */
64
197
  metadata?: Record<string, unknown>;
65
198
  }
66
199
  type AuthorizationTransaction = AuthorizationTransactionInput;
@@ -68,6 +201,10 @@ interface ExchangeCodeRecordInput {
68
201
  code: string;
69
202
  sessionId: string;
70
203
  returnTo?: string;
204
+ /** Copied from the authenticated login transaction; must agree with the session. */
205
+ deviceBinding?: OidcVaultDpopBinding;
206
+ /** Copied transaction-cookie hash; guarded exchange matches both cookie and key. */
207
+ browserBindingHash?: string;
71
208
  createdAt: number;
72
209
  expiresAt: number;
73
210
  }
@@ -78,6 +215,7 @@ interface OidcVaultSessionInput extends Omit<OidcVaultSession, 'createdAt' | 'up
78
215
  }
79
216
  interface RotateSessionInput {
80
217
  sessionId: string;
218
+ /** Omitted binding inherits the source; changing its key or enrolling an unbound source is rejected. */
81
219
  nextSession: OidcVaultSession;
82
220
  }
83
221
  interface DeleteSessionsBySubjectInput {
@@ -98,41 +236,209 @@ interface ConsumeBackchannelLogoutTokenJtiInput {
98
236
  /** Finite future epoch-millisecond expiry. Values `<= now` are expired. */
99
237
  expiresAt: number;
100
238
  }
239
+ /**
240
+ * Complete guarded-record match. Explicit `null` requires an absent/undefined
241
+ * stored field; it never means ignore the field. Both fields are mandatory.
242
+ * Valid persisted shapes are legacy (neither field), cookie-only (hash), or
243
+ * bound (hash + key). Stored null/malformed fields and a key without a hash
244
+ * are invalid. Both hashes use canonical 43-character SHA-256 base64url.
245
+ */
246
+ interface OidcVaultRecordBindingMatch {
247
+ deviceBinding: OidcVaultDpopBinding | null;
248
+ browserBindingHash: string | null;
249
+ }
250
+ interface ConsumeAuthorizationTransactionIfMatchesInput {
251
+ state: string;
252
+ match: OidcVaultRecordBindingMatch;
253
+ }
254
+ interface ConsumeExchangeCodeIfMatchesInput {
255
+ code: string;
256
+ /** Session ID from preflight, included in the atomic consume comparison. */
257
+ expectedSessionId: string;
258
+ match: OidcVaultRecordBindingMatch;
259
+ }
260
+ interface ReserveDpopProofInput {
261
+ /** Opaque `dpop:v1:` + SHA-256 base64url of JSON([effectiveNamespace, jkt, jti]), supplied after proof/target/nonce checks. */
262
+ replayKey: string;
263
+ /** Future safe-integer epoch milliseconds; maximum remaining TTL 360000 ms. */
264
+ expiresAt: number;
265
+ }
266
+ /**
267
+ * Shared atomic per-request replay admission; no implicit per-core-instance
268
+ * memory fallback. Independent memory-store objects are not shared protection.
269
+ * Shared instances need identical protection spaces/proof windows and synchronized
270
+ * clocks. Never release a reservation after a later route/issuer failure.
271
+ */
272
+ interface OidcVaultDpopReplayStore {
273
+ /**
274
+ * Reserve once through expiry. Duplicate/invalid/expired input returns false
275
+ * without extending expiry or allocating state. Capacity/provider failures
276
+ * throw; callers must fail closed, never evict live reservations. Built-ins
277
+ * default to 100000 entries per shared namespace. Expired entries awaiting
278
+ * bounded cleanup may conservatively occupy capacity. Duplicate detection
279
+ * precedes capacity rejection; capacity throws `OidcVaultDpopReplayCapacityError`.
280
+ */
281
+ reserveDpopProof(input: ReserveDpopProofInput): Promise<boolean>;
282
+ }
283
+ /**
284
+ * Detached revocation authority for a currently live lineage, resolved through
285
+ * a live handle or unexpired rotation alias. Never authenticates an alias and
286
+ * contains no tokens, profile, metadata, or provider fields beyond issuer/clientId.
287
+ * Inconsistent/malformed lineage binding or identity throws a private diagnostic.
288
+ */
289
+ interface OidcVaultSessionRevocationContext {
290
+ logicalSessionId: string;
291
+ provider?: OidcVaultProviderMetadata;
292
+ deviceBinding?: OidcVaultDpopBinding;
293
+ }
101
294
  declare class OidcVaultStoreConflictError extends Error {
102
295
  constructor(message?: string);
103
296
  }
297
+ /**
298
+ * Shared DPoP replay capacity is exhausted. No live reservation is evicted and
299
+ * no proof is admitted without replay state. Handle like any replay-store
300
+ * failure: fail closed; expose only the sanitized replay-unavailable error.
301
+ */
302
+ declare class OidcVaultDpopReplayCapacityError extends Error {
303
+ constructor(message?: string);
304
+ }
305
+ /**
306
+ * Portable store boundary for JSON-compatible plain objects/arrays and scalar
307
+ * values (strings, finite numbers, booleans, null). Built-in providers capture
308
+ * inputs at invocation before asynchronous work; returned portable data is
309
+ * detached from caller input and stored state. Native BSON/JSON/structured-clone
310
+ * values outside that subset retain provider-specific serialization semantics;
311
+ * opaque native objects have no cross-provider mutation-isolation guarantee.
312
+ *
313
+ * Bulk deletion counts primary records removed, never alias cleanup. Memory
314
+ * excludes expired sessions; MongoDB can count expired rows awaiting TTL cleanup;
315
+ * Redis counts actual primary deletions during a cursor traversal. Counts are not
316
+ * proof that no matches remain: concurrent arrivals can survive. MongoDB scoped
317
+ * deletion repeats until an empty query; Redis traverses once. Neither provides
318
+ * a global logout snapshot, and errors can follow earlier committed deletions.
319
+ */
104
320
  interface OidcVaultStoreProvider {
105
321
  /** Upsert an authorization transaction by `state`. */
106
322
  createAuthorizationTransaction(input: AuthorizationTransactionInput): Promise<void>;
323
+ /** Legacy consume: refuses guarded (cookie/key-bound) records without consuming them. */
107
324
  consumeAuthorizationTransaction(state: string): Promise<AuthorizationTransaction | null>;
325
+ /** Optional opt-in capability: detached live preflight snapshot, not a lock. */
326
+ getAuthorizationTransaction?(state: string): Promise<AuthorizationTransaction | null>;
327
+ /** Optional opt-in capability: atomic expiry + complete exact/null match + consume; mismatch leaves the record live. */
328
+ consumeAuthorizationTransactionIfMatches?(input: ConsumeAuthorizationTransactionIfMatchesInput): Promise<AuthorizationTransaction | null>;
108
329
  /** Upsert an exchange code record by `code`. */
109
330
  createExchangeCode(input: ExchangeCodeRecordInput): Promise<void>;
331
+ /** Legacy consume: refuses guarded (cookie/key-bound) records without consuming them. */
110
332
  consumeExchangeCode(code: string): Promise<ExchangeCodeRecord | null>;
111
- /** Upsert a session by `sessionId`, defaulting timestamps and logical lineage when omitted. */
333
+ /** Optional opt-in capability: detached live preflight snapshot, not a lock. */
334
+ getExchangeCode?(code: string): Promise<ExchangeCodeRecord | null>;
335
+ /** Optional opt-in capability: atomically match the session ID and both binding fields before consuming. */
336
+ consumeExchangeCodeIfMatches?(input: ConsumeExchangeCodeIfMatchesInput): Promise<ExchangeCodeRecord | null>;
337
+ /**
338
+ * Create a session by `sessionId`, defaulting timestamps and logical lineage when omitted.
339
+ *
340
+ * Duplicate-ID behavior is provider-specific (SVH-06): the memory and
341
+ * MongoDB providers replace the existing session (upsert), while the Redis
342
+ * provider rejects a live duplicate with `OidcVaultStoreConflictError`
343
+ * without changing the existing record or indexes (create-only, preserving
344
+ * RVR-02 index ownership). A rejected create preserves the original record
345
+ * and its subject/logical/provider-session index memberships; an accepted
346
+ * replacement takes over the ID with its own subject/logical/provider-session
347
+ * scope. Reusing an ID that holds only a stale rotation alias clears that
348
+ * alias on memory/Redis, while MongoDB can retain the stale alias row until
349
+ * expiry or lineage cleanup (FU-SVH-05b).
350
+ *
351
+ * Portable callers must always create sessions with a fresh unused
352
+ * `sessionId` and handle `OidcVaultStoreConflictError`: reusing a live ID is
353
+ * non-portable (it may replace or reject), and concurrent reuse races
354
+ * rotation source checks (SVH-08). The core middleware only creates fresh
355
+ * random IDs, so it never depends on duplicate-create behavior.
356
+ */
112
357
  createSession(input: OidcVaultSessionInput): Promise<OidcVaultSession>;
113
358
  getSession(sessionId: string): Promise<OidcVaultSession | null>;
359
+ /** Optional opt-in capability: resolve only a currently live lineage, including through unexpired aliases. No upstream tokens. */
360
+ getSessionRevocationContext?(sessionId: string): Promise<OidcVaultSessionRevocationContext | null>;
361
+ /** Optional opt-in capability; see `OidcVaultDpopReplayStore.reserveDpopProof`. */
362
+ reserveDpopProof?(input: ReserveDpopProofInput): Promise<boolean>;
114
363
  /**
115
364
  * Atomically replace an existing session with a distinct unused `nextSession.sessionId`.
116
365
  *
117
366
  * Providers preserve the existing logical session ID when the next session omits
118
- * one, retain the old public session ID as a revocation alias while the lineage
119
- * remains live, and throw `OidcVaultStoreConflictError` without changing source
120
- * or target data when the source is missing, the target already exists, or the
121
- * target ID equals the source ID.
367
+ * one, inherit the original device binding when omitted, and reject changing
368
+ * a bound key or adding binding to an unbound source without mutation. Null or
369
+ * malformed security fields are invalid, never legacy. Retain the old public
370
+ * session ID as an in-flight-request revocation alias. Rotation itself does
371
+ * not revoke the lineage. Providers throw
372
+ * `OidcVaultStoreConflictError` without changing source or target data when
373
+ * the source is missing, the target already exists, or the target ID equals
374
+ * the source ID.
375
+ *
376
+ * Each alias expires with its immediate successor session's `expiresAt` and is
377
+ * not extended by later rotations, so an earlier alias can stop working while
378
+ * the lineage is still live. A rotation that assigns an explicitly different
379
+ * logical session ID assigns the new source-ID alias to the new lineage;
380
+ * earlier aliases keep the old lineage and deadline if retained. Memory
381
+ * eagerly retires inactive old-lineage aliases on ownership transitions;
382
+ * MongoDB/Redis can retain them until expiry or explicit cleanup. Use distinct
383
+ * logical IDs for unrelated login families. Sessions without `expiresAt` have
384
+ * provider-specific retention: memory and Redis impose no alias time limit, while
385
+ * MongoDB applies its finite `rotatedSessionAliasRetentionMs` fallback
386
+ * (default 5 minutes). Generic callers must treat an expired alias as a hint,
387
+ * not a revocation channel; core refresh rotates the live session directly and
388
+ * preserves `expiresAt`, so it does not depend on alias lifetime (SVH-05).
122
389
  */
123
390
  rotateSession(input: RotateSessionInput): Promise<OidcVaultSession>;
391
+ /**
392
+ * Delete a live public-ID record, or revoke the logical lineage through an
393
+ * unexpired alias when no live record exists. Aliases are not readable through
394
+ * `getSession` and have no issuer/client filter. Scoped/direct deletion preserves
395
+ * unexpired aliases while another live member survives, even in another scope.
396
+ */
124
397
  deleteSession(sessionId: string): Promise<void>;
398
+ /** Revoke a logical lineage without an issuer/client filter; see provider count/concurrency notes. */
125
399
  deleteSessionsByLogicalSessionId(input: string | DeleteSessionsByLogicalSessionIdInput): Promise<number>;
126
400
  /**
127
401
  * Record a backchannel logout JTI once until its finite future expiry.
128
402
  *
129
403
  * Returns `false` for duplicate, expired, exact-boundary, `NaN`, or infinite
130
404
  * expiries. A JTI rejected for invalid or already-expired expiry is not stored.
405
+ *
406
+ * The core middleware (BOV-01) passes an opaque replay key that already
407
+ * namespaces the raw logout-token `jti` by issuer and client ID, so
408
+ * providers must treat `jti` as an opaque string and need no schema change. Pre-BOV-01 raw-`jti` records use
409
+ * a different key shape and expire naturally with the logout-token `exp`;
410
+ * they are never matched by namespaced keys. Retry safety comes from the
411
+ * atomic single-key reservation plus idempotent catch-up deletion in the
412
+ * route handler, not from a multi-key store transaction.
131
413
  */
132
414
  consumeBackchannelLogoutTokenJti(input: ConsumeBackchannelLogoutTokenJtiInput): Promise<boolean>;
415
+ /** Delete matching subject sessions, filtering by each supplied issuer/client; string input is unscoped. */
133
416
  deleteSessionsBySubject(input: string | DeleteSessionsBySubjectInput): Promise<number>;
417
+ /** Delete matching provider-session IDs, filtering by each supplied issuer/client; string input is unscoped. */
134
418
  deleteSessionsByProviderSessionId(input: string | DeleteSessionsByProviderSessionIdInput): Promise<number>;
135
419
  }
420
+ /**
421
+ * Required store capabilities for opt-in device binding or fingerprint
422
+ * recognition. All six methods are checked at vault construction, including
423
+ * fingerprint-only flows (which do not reserve DPoP proofs). Old bearer-only
424
+ * custom stores retain the base interface. Guarded reads are preflight only:
425
+ * providers must compare/consume atomically and preserve original binding and
426
+ * portable reserved recognition metadata through core's rotation inputs.
427
+ */
428
+ interface OidcVaultDeviceBindingStoreProvider extends OidcVaultStoreProvider, OidcVaultDpopReplayStore {
429
+ /** Detached live snapshot, never a lock. Malformed/null binding/hash is invalid, not legacy. */
430
+ getAuthorizationTransaction(state: string): Promise<AuthorizationTransaction | null>;
431
+ /** Atomic expiry + full exact/null match + deletion. Mismatch leaves the live record available for an honest retry. */
432
+ consumeAuthorizationTransactionIfMatches(input: ConsumeAuthorizationTransactionIfMatchesInput): Promise<AuthorizationTransaction | null>;
433
+ /** Detached live preflight snapshot, without spending the code. */
434
+ getExchangeCode(code: string): Promise<ExchangeCodeRecord | null>;
435
+ /** Atomic expiry + expectedSessionId + both exact/null binding fields + deletion; no get/unconditional-delete fallback. */
436
+ consumeExchangeCodeIfMatches(input: ConsumeExchangeCodeIfMatchesInput): Promise<ExchangeCodeRecord | null>;
437
+ /** Resolve current lineage authority, never the alias's missing/historical binding; no credentials are returned. */
438
+ getSessionRevocationContext(sessionId: string): Promise<OidcVaultSessionRevocationContext | null>;
439
+ /** Atomic duplicate-first admission; false rejects the proof, capacity/provider errors fail closed with HTTP 503. */
440
+ reserveDpopProof(input: ReserveDpopProofInput): Promise<boolean>;
441
+ }
136
442
  interface OidcVaultHookContext {
137
443
  route: OidcVaultRouteName;
138
444
  req: Request;
@@ -141,6 +447,11 @@ interface OidcVaultHookContext {
141
447
  metadata?: Record<string, unknown>;
142
448
  }
143
449
  interface OidcVaultErrorContext extends OidcVaultHookContext {
450
+ /**
451
+ * Original private diagnostic, separate from sanitized browser JSON. Narrow
452
+ * before use; provider transport/replay admission errors retain the original failure in
453
+ * `error.cause`, while issuer-result validation supplies a TypeError directly.
454
+ */
144
455
  error: unknown;
145
456
  }
146
457
  interface OidcVaultHooks {
@@ -148,24 +459,70 @@ interface OidcVaultHooks {
148
459
  onAuthorizationUrl?(context: OidcVaultHookContext): void | Promise<void>;
149
460
  onCallbackTokens?(context: OidcVaultHookContext): void | Promise<void>;
150
461
  onUserInfo?(context: OidcVaultHookContext): void | Promise<void>;
462
+ /**
463
+ * May mutate the session before persistence or veto creation by throwing.
464
+ * With `sessionTtlMs`, the session already carries the absolute expiry.
465
+ * After this hook, a valid earlier epoch-millisecond expiry is preserved;
466
+ * removal, extension, or an invalid timestamp restores the configured cap.
467
+ * Without `sessionTtlMs`, application/store lifetime policy is unchanged.
468
+ * Security-owned `deviceBinding` is restored from core's private snapshot;
469
+ * the hook cannot enroll, remove, or rebind a sender constraint. Guarded POST
470
+ * callbacks also retain their fresh session/lineage IDs and resolved provider
471
+ * identity independently of hook mutations. Reserved fingerprint recognition
472
+ * metadata is restored from the original transaction, including unenrolled
473
+ * absence; a hook/profile cannot enroll, strip, or rebind it. Other application
474
+ * metadata/profile fields remain mutable; throwing is an authenticated terminal veto.
475
+ */
151
476
  onBeforeSessionCreate?(context: OidcVaultHookContext): void | Promise<void>;
152
477
  onSessionCreated?(context: OidcVaultHookContext): void | Promise<void>;
478
+ /** Post-rotation notification with owned plain session data; mutation cannot change stored/response authority or profile precedence. */
153
479
  onSessionRefreshed?(context: OidcVaultHookContext): void | Promise<void>;
480
+ /** Pre-revocation veto for a live handle, after identity/proof/nonce/replay. Receives owned session data; deletion uses original lineage authority. */
154
481
  onBeforeLogout?(context: OidcVaultHookContext): void | Promise<void>;
155
482
  onLogout?(context: OidcVaultHookContext): void | Promise<void>;
156
483
  onError?(context: OidcVaultErrorContext): void | Promise<void>;
157
484
  }
158
485
  interface IssueTokenInput {
486
+ /** Owned session data; reserved fingerprint recognition evidence is omitted to keep hashes out of application tokens. */
159
487
  session: OidcVaultSession;
160
488
  req: Request;
161
489
  res: Response;
490
+ /**
491
+ * Authoritative verified request/key context, detached and frozen by core.
492
+ * Use this thumbprint for `cnf.jkt`, not mutable session/profile fields.
493
+ * Absent for unbound issuance; a later proof never enrolls a legacy session.
494
+ */
495
+ deviceBinding?: Readonly<OidcVaultVerifiedDpopBinding>;
162
496
  }
497
+ /**
498
+ * Local application credentials returned by `OidcVaultTokenIssuer.issue`.
499
+ * Runtime validation copies only these fields into a fresh result; additional
500
+ * properties are ignored and cannot override the response's session ID/user.
501
+ */
163
502
  interface OidcVaultTokenIssueResult {
503
+ /** Nonempty opaque token for unbound issuance; bound issuance requires a compact signed JWT with matching `cnf.jkt`. */
164
504
  accessToken: string;
505
+ /** Token lifetime in seconds: a finite, nonnegative safe integer (zero is valid). */
165
506
  expiresIn: number;
166
- tokenType?: 'Bearer';
507
+ /** Exact `DPoP` is required when bound. Unbound permits only `Bearer` or omission; omitted/undefined stays absent in JSON. */
508
+ tokenType?: 'Bearer' | 'DPoP';
167
509
  }
510
+ /**
511
+ * Trusted application token issuer for exchange and refresh. Return a non-null,
512
+ * non-array object satisfying `OidcVaultTokenIssueResult`; only its declared
513
+ * credential fields enter the response. This contains accidental extensions,
514
+ * not arbitrary behavior of trusted issuers/hooks with request/response access.
515
+ * Core retains its own binding/lineage snapshots across issuer calls.
516
+ */
168
517
  interface OidcVaultTokenIssuer {
518
+ /**
519
+ * Invalid results fail inside issuance rollback: revoke the session lineage
520
+ * (including the rotated refresh session), clear a cookie-transport session
521
+ * cookie, and return sanitized HTTP 500 / `OIDC_VAULT_INTERNAL_ERROR`.
522
+ * Field diagnostics are available privately through `hooks.onError`.
523
+ * Core decodes a bound result only to check this trusted issuer contract;
524
+ * APIs must independently verify signature, issuer, audience, and expiry.
525
+ */
169
526
  issue(input: IssueTokenInput): Promise<OidcVaultTokenIssueResult>;
170
527
  }
171
528
  interface OidcVaultAccessTokenValidationResult {
@@ -173,29 +530,90 @@ interface OidcVaultAccessTokenValidationResult {
173
530
  sessionId?: string;
174
531
  scope?: string;
175
532
  claims?: Record<string, unknown>;
533
+ /** Verified sender constraint. Legacy adapters may omit it only for genuinely unbound credentials. */
534
+ confirmation?: OidcVaultAccessTokenConfirmation | null;
535
+ }
536
+ /** Canonical RFC 7638 SHA-256 thumbprint derived from verified token/introspection data, never from a proof alone. */
537
+ interface OidcVaultAccessTokenConfirmation {
538
+ jkt: string;
539
+ }
540
+ interface OidcVaultAccessTokenRequestInput {
541
+ token: string;
542
+ scheme: 'Bearer' | 'DPoP';
543
+ req: Request;
544
+ }
545
+ /** Mandatory confirmation: null means verified unbound; missing confirmation or malformed/unsupported cnf is invalid. */
546
+ interface OidcVaultRequestAwareAccessTokenValidationResult extends OidcVaultAccessTokenValidationResult {
547
+ confirmation: OidcVaultAccessTokenConfirmation | null;
176
548
  }
177
549
  interface OidcVaultAuthContext extends OidcVaultAccessTokenValidationResult {
178
550
  token: string;
551
+ /** Present only after original-key proof, API ath, nonce and shared replay admission all pass. */
552
+ deviceBinding?: Readonly<OidcVaultVerifiedDpopBinding>;
179
553
  }
180
554
  interface OidcVaultAccessTokenValidator {
555
+ /** Binding-disabled middleware invokes exactly validate(token), with one argument. Report any known confirmation. */
181
556
  validate(token: string): Promise<OidcVaultAccessTokenValidationResult>;
557
+ /** Required at middleware construction when deviceBinding is enabled; called for both Bearer and DPoP. */
558
+ validateWithRequest?(input: OidcVaultAccessTokenRequestInput): Promise<OidcVaultRequestAwareAccessTokenValidationResult>;
559
+ }
560
+ /**
561
+ * Trusted adapter for verified JWT/introspection data. Faithfully report the
562
+ * original confirmation independently of custom mapping: null is genuinely
563
+ * unbound, never a malformed/unsupported cnf. The middleware owns proof checks.
564
+ */
565
+ interface OidcVaultRequestAwareAccessTokenValidator extends OidcVaultAccessTokenValidator {
566
+ validateWithRequest(input: OidcVaultAccessTokenRequestInput): Promise<OidcVaultRequestAwareAccessTokenValidationResult>;
567
+ }
568
+ interface OidcVaultAccessTokenMiddlewareErrorContext {
569
+ error: unknown;
570
+ req: Request;
571
+ res: Response;
572
+ token?: string;
573
+ auth?: OidcVaultAuthContext;
182
574
  }
183
575
  interface OidcVaultAccessTokenMiddlewareOptions {
184
576
  validator: OidcVaultAccessTokenValidator;
577
+ /** Opt-in API DPoP enforcement; requires validateWithRequest. Omission preserves the one-argument legacy path. */
578
+ deviceBinding?: OidcVaultApiDeviceBindingOptions;
579
+ /**
580
+ * Pre-`next()` veto hook, not a post-commit notification: when it throws,
581
+ * downstream middleware never runs and `req.auth` is detached before the
582
+ * error response is sent. A valid credential plus a failing hook
583
+ * never surfaces as an invalid-token 401: a controlled package error from the
584
+ * hook keeps its own status/code/client message (a 401 uses the attempted
585
+ * scheme's challenge, other statuses carry no challenge), while any other hook
586
+ * error becomes a sanitized `500 OIDC_VAULT_AUTH_CONTEXT_FAILED` without
587
+ * leaking the original message. The original error is observable via
588
+ * `onError` for private server-side logs. Security-owned token/confirmation/
589
+ * deviceBinding and req.auth authority are restored after mutable hooks;
590
+ * a veto never releases an admitted proof's replay reservation.
591
+ */
185
592
  onAuthContext?(input: {
186
593
  req: Request;
187
594
  res: Response;
188
595
  auth: OidcVaultAuthContext;
189
596
  }): void | Promise<void>;
597
+ /**
598
+ * Observes the original error for every API-middleware failure
599
+ * (extraction, validator, proof/nonce/replay, and `onAuthContext`) without affecting
600
+ * the sanitized client response. Failures thrown by this observer are
601
+ * swallowed so the original error response is preserved. Replay failures
602
+ * retain the provider diagnostic in non-enumerable error.cause. No request
603
+ * auth context is attached before proof acceptance or after a veto.
604
+ */
605
+ onError?(context: OidcVaultAccessTokenMiddlewareErrorContext): void | Promise<void>;
190
606
  }
191
607
  interface OidcVaultAuthenticatedRequest extends Request {
192
608
  auth?: OidcVaultAuthContext;
193
609
  }
610
+ /** Local JWT verification options, independent of the asymmetric DPoP proof policy; captured at construction. */
194
611
  interface OidcVaultJwtAccessTokenValidatorOptions {
195
612
  key: CryptoKey | KeyObject | JWK | Uint8Array;
196
613
  issuer?: string;
197
614
  audience?: string | string[];
198
615
  algorithms?: string[];
616
+ /** Verified cnf is snapshotted before this mapper; mapper-supplied confirmation/deviceBinding is ignored. */
199
617
  mapClaims?(claims: Record<string, unknown>): OidcVaultAccessTokenValidationResult;
200
618
  }
201
619
  declare module 'express-serve-static-core' {
@@ -204,6 +622,19 @@ declare module 'express-serve-static-core' {
204
622
  }
205
623
  }
206
624
  interface OidcVaultConfig {
625
+ /**
626
+ * OIDC issuer identifier, preserved exactly after surrounding-whitespace
627
+ * trimming (no trailing slash is added, so `/tenant`, `/tenant/`, and
628
+ * `/tenant//` remain distinct). Must be an absolute http(s) URL without
629
+ * userinfo, query, or fragment; `http` is accepted for local-test
630
+ * providers. Required alongside `clientId` in discovery and manual modes.
631
+ * Any nonempty endpoint, including `userInfoEndpoint`/`endSessionEndpoint`,
632
+ * selects manual mode: supply `authorizationEndpoint`, `tokenEndpoint`, and
633
+ * `jwksUri`; no discovery or partial overrides. Blank strings are absent
634
+ * after trimming. Discovery responses
635
+ * must carry an exactly equal issuer, and ID/logout tokens are validated
636
+ * against this exact identifier.
637
+ */
207
638
  issuer?: string;
208
639
  authorizationEndpoint?: string;
209
640
  tokenEndpoint?: string;
@@ -215,6 +646,23 @@ interface OidcVaultConfig {
215
646
  scopes?: string;
216
647
  }
217
648
  interface OidcVaultLogoutResult {
649
+ /**
650
+ * Successful logout commits revocation before upstream work and clears the
651
+ * session cookie under cookie transport. Built-in live handles and unexpired
652
+ * aliases authenticate the surviving lineage's provider/original binding;
653
+ * wrong identity, missing/wrong/stale/replayed proof, or disabled acceptance
654
+ * of a bound lineage fails without deleting or clearing. No currently live
655
+ * target is idempotent success without deletion or proof reservation. Aliases
656
+ * grant revocation only, not refresh authentication or upstream redirects.
657
+ * Old custom bearer stores without revocation context retain legacy alias
658
+ * deletion behavior. Stateless application access tokens are not invalidated.
659
+ * Local-only logout (`redirect` unset or `false`) never contacts the provider.
660
+ * Redirected logout (`redirect: true`)
661
+ * treats the upstream end-session redirect as best-effort: when provider
662
+ * discovery fails or no `endSessionEndpoint` is available, the route still
663
+ * returns this local success. Discovery errors reach `onError` without
664
+ * undoing revocation or skipping the live session's `onLogout` notification.
665
+ */
218
666
  loggedOut: true;
219
667
  }
220
668
  interface OidcVaultBackchannelLogoutResult {
@@ -222,38 +670,213 @@ interface OidcVaultBackchannelLogoutResult {
222
670
  revokedSessions: number;
223
671
  }
224
672
  interface OidcVaultExchangeResult extends Partial<OidcVaultTokenIssueResult> {
673
+ /** Authoritative vault handle in body transport; omitted from cookie-transport JSON. */
225
674
  sessionId?: string;
675
+ /** Session profile; extra token-issuer result properties cannot override it. */
226
676
  user?: OidcVaultUserProfile;
227
677
  }
678
+ /** JSON body for opt-in `POST <basePath>/login`; proof/key and recognition signals belong in their configured headers, not the body. */
679
+ interface OidcVaultLoginInitiationInput {
680
+ /** Optional destination on the configured frontend origin. Query-string returnTo is ignored by POST login. */
681
+ returnTo?: string;
682
+ }
683
+ /**
684
+ * Exact successful POST-login JSON. Navigate to this URL after storing a proof
685
+ * key when DPoP is enabled. Use credentials: 'include' even with body session
686
+ * transport: initiation sets a temporary HttpOnly transaction cookie.
687
+ */
688
+ interface OidcVaultLoginInitiationResult {
689
+ authorizationUrl: string;
690
+ }
228
691
  type OidcVaultSessionTransport = 'body' | 'cookie';
229
692
  type OidcVaultCookieDeploymentMode = 'same-origin' | 'same-site' | 'cross-site';
230
693
  type OidcVaultCookieSameSite = 'lax' | 'strict' | 'none';
694
+ /**
695
+ * Temporary browser-binding cookie for POST login -> headerless callback ->
696
+ * guarded exchange. Always host-only, Path=/, HttpOnly, and Secure on HTTPS;
697
+ * no Domain/path/security opt-outs. One pending flow per cookie name: a fresh
698
+ * initiation replaces it. Use distinct names for multiple vault mounts.
699
+ */
700
+ interface OidcVaultTransactionCookieOptions {
701
+ /**
702
+ * HTTP cookie token, distinct from the session cookie. HTTPS defaults to
703
+ * __Host-oidc_vault_transaction; loopback HTTP to oidc_vault_transaction.
704
+ * __Host-/__Secure- prefixes require HTTPS.
705
+ */
706
+ name?: string;
707
+ /** Default lax permits the provider's top-level GET callback. Explicit none requires HTTPS for cross-site SPAs. */
708
+ sameSite?: 'lax' | 'none';
709
+ }
231
710
  interface OidcVaultCookieOptions {
711
+ /**
712
+ * Session cookie name. Must be a valid HTTP cookie name (printable ASCII
713
+ * token). Names with the `__Secure-` prefix require an effectively Secure
714
+ * cookie; names with the `__Host-` prefix additionally require no
715
+ * `cookie.domain` and `cookie.path: '/'`. Checked against effective
716
+ * serialized values, so `sameSite: 'none'` counts as Secure.
717
+ */
232
718
  name?: string;
233
719
  deploymentMode?: OidcVaultCookieDeploymentMode;
234
720
  sameSite?: OidcVaultCookieSameSite;
721
+ /**
722
+ * Explicit `Secure` override. When omitted, defaults to `true` for HTTPS
723
+ * `backendOrigin`, `sameSite: 'none'`, or `deploymentMode: 'cross-site'`;
724
+ * otherwise `false` as an intentional HTTP local-development policy
725
+ * (plaintext backends must opt into `secure: true` when served over HTTPS
726
+ * behind a proxy that reports an `http` origin). `SameSite=None` is always
727
+ * serialized with `Secure` regardless of this flag because browsers reject
728
+ * `SameSite=None` without it.
729
+ */
235
730
  secure?: boolean;
731
+ /**
732
+ * Optional cookie `Domain`. Must be a valid cookie domain when set. Must be
733
+ * omitted for `__Host-` prefixed names (host-only requirement).
734
+ */
236
735
  domain?: string;
736
+ /**
737
+ * Cookie `Path`. Must start with `/` and contain only header-safe printable
738
+ * ASCII (no CTLs, DEL, non-ASCII/Unicode, or `;`). `__Host-` prefixed names
739
+ * require exactly `'/'`.
740
+ */
237
741
  path?: string;
742
+ /**
743
+ * Must be `true` or omitted. Middleware creation rejects
744
+ * `httpOnly: false` for cookie session transport; there is no unsafe
745
+ * compatibility switch.
746
+ */
238
747
  httpOnly?: boolean;
239
748
  }
240
749
  interface OidcVaultOptions {
750
+ /** Public router mount; default /auth/oidc. DPoP vault proxies must preserve this externally visible path. */
241
751
  basePath?: string;
752
+ /** Pinned public callback/proof origin. DPoP requires a static HTTPS origin, with loopback HTTP allowed for development. */
242
753
  backendOrigin: string;
754
+ /** Vault persistence and, when DPoP is enabled, atomic replay admission. All six stronger capabilities are required for either opt-in feature. */
243
755
  storeProvider: OidcVaultStoreProvider;
244
756
  config?: OidcVaultConfig;
757
+ /**
758
+ * Lifecycle hooks shared by reference (not cloned): pre-commit hooks run
759
+ * before durable state changes and can veto by throwing, while
760
+ * `onSessionCreated`/`onSessionRefreshed`/`onLogout` are post-commit
761
+ * notifications whose failures reach `onError` without undoing state.
762
+ * Replacing the caller options object after middleware creation has no
763
+ * effect; the `hooks` service object itself is retained live.
764
+ */
245
765
  hooks?: OidcVaultHooks;
766
+ /** Optional local issuer; when omitted, exchange/refresh return no local token fields. */
246
767
  tokenIssuer?: OidcVaultTokenIssuer;
768
+ /**
769
+ * Opt-in DPoP policy; absent preserves unbound bearer behavior. Present with
770
+ * no mode means `optional`. Requires all `OidcVaultDeviceBindingStoreProvider`
771
+ * capabilities and a static HTTPS `backendOrigin` (loopback HTTP development
772
+ * is allowed). Bound exchange/refresh/logout require a fresh original-key
773
+ * DPoP header before code consumption, upstream refresh, hooks or revocation;
774
+ * vault POSTs need no Authorization access token/ath. A supplied optional
775
+ * unbound proof validates/reserves but never enrolls or rebinds. Construction
776
+ * snapshots/freezes plain options and algorithms and copies nonce bytes.
777
+ */
778
+ deviceBinding?: OidcVaultDeviceBindingOptions;
779
+ /**
780
+ * Separate opt-in recognition policy; omission means no signal capture/check.
781
+ * Enables JSON POST login and its temporary HttpOnly cookie in both session
782
+ * transports, even without DPoP; requires all guarded-store capabilities.
783
+ * POST login alone enrolls a supplied signal in reserved metadata. Enrolled
784
+ * exchange/refresh compare before proof admission/code consume/upstream use:
785
+ * missing/mismatch returns 403 OIDC_VAULT_FINGERPRINT_REAUTH_REQUIRED without
786
+ * mutation. Legacy/GET/absent-signal sessions remain unenrolled. Refresh never
787
+ * enrolls/rotates recognition; changes require fresh POST login. Logout,
788
+ * rotation aliases, signed backchannel logout and API recognition policy are
789
+ * unaffected. Disclose collection and transaction/session retention, including
790
+ * store cleanup/backups; hashing is not anonymization or theft prevention.
791
+ */
792
+ fingerprintRecognition?: OidcVaultFingerprintRecognitionOptions;
793
+ /**
794
+ * Default browser return target after backend callback completion. Required
795
+ * if login accepts a custom `returnTo`. Remains optional at middleware
796
+ * creation because non-callback routes do not need it; the callback route
797
+ * instead validates its destination (transaction `returnTo` or this value)
798
+ * before any provider call or durable session/code creation and fails with
799
+ * `500 OIDC_VAULT_MISSING_FRONTEND_REDIRECT_URI` when neither is configured.
800
+ */
247
801
  frontendRedirectUri?: string;
802
+ /**
803
+ * Optional provider-registered HTTP(S) URL used in the upstream end-session
804
+ * redirect. Only consulted for redirected logout (`redirect: true`); local
805
+ * logout never contacts the provider, and redirected logout treats upstream
806
+ * discovery failures or an absent endpoint as best-effort (local
807
+ * `200 { loggedOut: true }`, with `onLogout` still delivered for a live
808
+ * session). Discovery errors also reach `onError`.
809
+ */
248
810
  postLogoutRedirectUri?: string;
811
+ /** Default enabled when discovery/manual config has a UserInfo endpoint and the response supplies an access token; false disables it. */
249
812
  fetchUserInfo?: boolean;
813
+ /**
814
+ * Authorization transaction lifetime in milliseconds (default: 10 minutes).
815
+ * Must be a positive safe integer; construction and creation require
816
+ * `now() + TTL` to be an integer within JavaScript Date's epoch range.
817
+ */
250
818
  authorizationTransactionTtlMs?: number;
819
+ /**
820
+ * One-time exchange code lifetime in milliseconds (default: 30 seconds).
821
+ * Same positive-safe-integer and computed-epoch checks as
822
+ * `authorizationTransactionTtlMs`.
823
+ */
251
824
  exchangeCodeTtlMs?: number;
825
+ /**
826
+ * Opt-in absolute server-side session lifetime in milliseconds, measured
827
+ * from callback session creation, not login start. Must be a positive safe
828
+ * integer producing an integer epoch within JavaScript Date's range;
829
+ * checked at construction and again at creation using `now` (or Date.now).
830
+ *
831
+ * Sets `expiresAt` before `onBeforeSessionCreate` and caps it after the hook:
832
+ * hooks may shorten the expiry but cannot extend/remove the maximum (invalid
833
+ * timestamps also restore the cap). Refresh preserves the stored expiry.
834
+ * Independent of upstream OAuth `expires_in`. Unset retains application/
835
+ * store-owned lifetime behavior; no default session expiry is assigned.
836
+ */
837
+ sessionTtlMs?: number;
838
+ /** Default body: JSON sessionId. Cookie mode omits that handle from JSON; POST login/exchange need the temporary cookie in both modes. */
252
839
  sessionTransport?: OidcVaultSessionTransport;
253
840
  cookie?: OidcVaultCookieOptions;
841
+ /**
842
+ * Detached/frozen transaction-cookie settings, independent of session
843
+ * transport. POST login stores only SHA-256(cookie secret) and the verified
844
+ * initiating jkt when bound; callback authenticates and atomically spends the
845
+ * stored match. Guarded exchange needs this cookie in both session transports
846
+ * plus the original-key proof when bound. Success/authenticated terminal
847
+ * issuance failure clears it; mismatches and nonce challenges do not spend or
848
+ * clear. Cookie lifetime rounds down to the transaction/code deadline.
849
+ */
850
+ transactionCookie?: OidcVaultTransactionCookieOptions;
851
+ /**
852
+ * Allowed browser source origins: POST login uses this policy in both
853
+ * transports; guarded exchange uses the same policy (JSON or form), and
854
+ * cookie refresh/logout retain their source checks. Missing/null/untrusted
855
+ * sources fail closed; POST login/guarded exchange also reject raw source
856
+ * ambiguity. backendOrigin is included internally; a valid Referer is the
857
+ * fallback only when Origin is absent.
858
+ */
254
859
  trustedOrigins?: string[];
255
860
  requestBodyLimit?: string | number;
861
+ /**
862
+ * Overall deadline in milliseconds for a single upstream provider HTTP
863
+ * exchange, covering DNS/connect/TLS, response headers, and complete
864
+ * success/error body consumption. Cancellation is attempted promptly but
865
+ * never awaited: completion does not guarantee cleanup of custom streams.
866
+ * Defaults to 5000; must be a positive finite integer.
867
+ *
868
+ * Successful discovery metadata is shared across timeout policies, while
869
+ * in-flight fetches are isolated by `(issuer, providerRequestTimeoutMs)`.
870
+ * Remote JWKS resolvers are isolated by
871
+ * `(jwks_uri, providerRequestTimeoutMs)` because JOSE fixes the fetch
872
+ * timeout at resolver creation; the package JWKS transport additionally
873
+ * bounds JWKS bodies (1 MiB, 100 keys) that JOSE leaves unbounded.
874
+ * Network/reset failures are sanitized endpoint-specific 502s; original
875
+ * transport diagnostics are available privately via `hooks.onError`'s
876
+ * `error.cause`. JOSE timeouts retain `ERR_JWKS_TIMEOUT`.
877
+ */
256
878
  providerRequestTimeoutMs?: number;
879
+ /** Epoch-millisecond clock, also sampled during construction to validate TTLs. Defaults to Date.now. */
257
880
  now?: () => number;
258
881
  }
259
882
 
@@ -275,8 +898,20 @@ interface OidcVaultEnv {
275
898
  OIDC_SCOPES?: string;
276
899
  }
277
900
  /**
278
- * Resolve OIDC configuration from application input. If an issuer URL is
279
- * present, explicit endpoint fields are ignored and discovery mode wins.
901
+ * Resolve OIDC configuration from application input.
902
+ *
903
+ * Issuer discovery mode is selected only when an issuer is configured without
904
+ * any nonempty manual endpoint fields. Any such field, including optional
905
+ * `userInfoEndpoint` or `endSessionEndpoint`, selects manual mode: discovery
906
+ * is not used, and `issuer`, `authorizationEndpoint`, `tokenEndpoint`, and
907
+ * `jwksUri` are all required. Blank strings are treated as absent after trimming.
908
+ * `issuer` keeps ID tokens and logout tokens issuer-bound.
909
+ *
910
+ * The issuer identifier is syntax-validated (absolute http/https URL without
911
+ * userinfo, query, or fragment) but otherwise preserved exactly as configured
912
+ * after surrounding-whitespace trimming: no trailing slash is added and path
913
+ * variants such as `/tenant`, `/tenant/`, and `/tenant//` remain distinct.
914
+ * Endpoint URLs are normalized separately with `URL.toString()`.
280
915
  */
281
916
  declare function resolveOidcVaultConfig(config?: OidcVaultConfig): OidcVaultResolvedConfig;
282
917
  /**
@@ -291,19 +926,66 @@ declare const DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT = "16kb";
291
926
  declare const OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT = 16;
292
927
  declare const OIDC_VAULT_ROUTE_PATHS: Record<OidcVaultRouteName, string>;
293
928
 
929
+ /**
930
+ * API authentication from package-root named imports. With deviceBinding
931
+ * enabled, requires validateWithRequest at construction for BOTH schemes and
932
+ * verifies a single raw DPoP proof, pinned method/URL, original confirmation,
933
+ * API ath, nonce and shared replay BEFORE attaching req.auth or invoking hooks.
934
+ * Bound tokens require Authorization: DPoP; unbound DPoP presentation is invalid.
935
+ * Omitted binding retains exactly validate(token) for legacy Bearer adapters,
936
+ * but refuses any known bound credential. Construction snapshots configuration,
937
+ * callbacks/validator methods, URLs, algorithms and secret bytes; the replay
938
+ * store remains the shared atomic service. Responses carry no-store and fixed
939
+ * errors; onError privately observes originals and cannot override failures.
940
+ */
294
941
  declare function createOidcVaultAccessTokenMiddleware(options: OidcVaultAccessTokenMiddlewareOptions): RequestHandler;
295
942
 
296
- declare function createOidcVaultJwtAccessTokenValidator(options: OidcVaultJwtAccessTokenValidatorOptions): {
297
- validate(token: string): Promise<OidcVaultAccessTokenValidationResult>;
298
- };
943
+ /**
944
+ * Verify local JWTs with both legacy validate(token) and validateWithRequest.
945
+ * Raw verified cnf is captured before mapClaims; mapping cannot forge or strip
946
+ * the sender constraint. The request-aware result always has confirmation
947
+ * (null only for genuinely unbound JWTs). Legacy unbound results omit it;
948
+ * bound results report it so binding-disabled vault middleware refuses downgrade.
949
+ * Proof/target/ath/nonce/replay enforcement belongs to the API middleware.
950
+ * Construction captures options and copies secret/JWK/allowlist containers.
951
+ */
952
+ declare function createOidcVaultJwtAccessTokenValidator(options: OidcVaultJwtAccessTokenValidatorOptions): OidcVaultRequestAwareAccessTokenValidator;
299
953
 
300
954
  /**
301
955
  * Normalize the mounted base path for the OIDC router.
302
956
  */
303
957
  declare function normalizeOidcVaultBasePath(value?: string): string;
304
958
  /**
305
- * Create the core OIDC vault middleware.
959
+ * Create the OIDC lifecycle router using a named package-root import.
960
+ * The default mount is /auth/oidc and default transport is body. Configure
961
+ * issuer/clientId (or complete manual endpoints), a pinned backendOrigin,
962
+ * frontendRedirectUri for callback completion, and a storeProvider.
963
+ *
964
+ * With deviceBinding enabled, JSON POST login selects a verified persistent
965
+ * browser key, callback authenticates the temporary HttpOnly cookie, and
966
+ * exchange/refresh/logout enforce the original key before credential work.
967
+ * A local tokenIssuer must emit exact DPoP and matching cnf.jkt from its verified
968
+ * input. Protect every accepting API separately with
969
+ * createOidcVaultAccessTokenMiddleware and a request-aware validator/replay store.
970
+ * Vault refresh/logout need no access token or ath and work after JWT expiry.
971
+ * All vault responses carry no-store; logout does not revoke stateless JWTs.
972
+ *
973
+ * Construction takes an internal resolved snapshot of `options` without
974
+ * mutating the caller object: normalized values (such as
975
+ * `frontendRedirectUri`) are stored on the snapshot, plain-data containers
976
+ * (`cookie`, `trustedOrigins`, `config`) are shallow-copied, `transactionCookie`
977
+ * is resolved into a detached/frozen security configuration, and service
978
+ * references (`storeProvider`, `hooks`, `tokenIssuer`, `now`) are retained
979
+ * by reference, never deep-cloned. Mutating or replacing the caller options
980
+ * after this call has no effect on the created router. Post-commit hooks
981
+ * (`onSessionCreated`, `onSessionRefreshed`, `onLogout`) are notifications
982
+ * whose failures are reported to `onError` without undoing committed state;
983
+ * other hooks run pre-commit and can veto the operation by throwing.
984
+ * Opt-in `deviceBinding` policy/algorithm containers are frozen snapshots;
985
+ * an enabled nonce secret is copied into private owned storage. Separate opt-in
986
+ * `fingerprintRecognition` settings are detached/frozen; recognition enrolls
987
+ * only at POST login and checks exchange/refresh before credential mutation.
306
988
  */
307
989
  declare function createOidcVaultMiddleware(options: OidcVaultOptions): Router;
308
990
 
309
- export { type AuthorizationTransaction, type AuthorizationTransactionInput, type ConsumeBackchannelLogoutTokenJtiInput, DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS, DEFAULT_EXCHANGE_CODE_TTL_MS, DEFAULT_OIDC_SCOPES, DEFAULT_OIDC_VAULT_BASE_PATH, DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT, type DeleteSessionsByLogicalSessionIdInput, type DeleteSessionsByProviderSessionIdInput, type DeleteSessionsBySubjectInput, type ExchangeCodeRecord, type ExchangeCodeRecordInput, type IssueTokenInput, OIDC_VAULT_ROUTE_PATHS, OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT, type OidcVaultAccessTokenMiddlewareOptions, type OidcVaultAccessTokenValidationResult, type OidcVaultAccessTokenValidator, type OidcVaultAuthContext, type OidcVaultAuthenticatedRequest, type OidcVaultBackchannelLogoutResult, type OidcVaultConfig, type OidcVaultConfigMode, type OidcVaultCookieDeploymentMode, type OidcVaultCookieOptions, type OidcVaultCookieSameSite, type OidcVaultEnv, type OidcVaultErrorContext, type OidcVaultExchangeResult, type OidcVaultHookContext, type OidcVaultHooks, type OidcVaultJwtAccessTokenValidatorOptions, type OidcVaultLogoutResult, type OidcVaultOptions, type OidcVaultProviderMetadata, type OidcVaultResolvedConfig, type OidcVaultRouteName, type OidcVaultSession, type OidcVaultSessionInput, type OidcVaultSessionTransport, OidcVaultStoreConflictError, type OidcVaultStoreProvider, type OidcVaultTokenIssueResult, type OidcVaultTokenIssuer, type OidcVaultUserProfile, type RotateSessionInput, createOidcVaultAccessTokenMiddleware, createOidcVaultJwtAccessTokenValidator, createOidcVaultMiddleware, normalizeOidcVaultBasePath, resolveOidcVaultConfig, resolveOidcVaultConfigFromEnv };
991
+ export { type AuthorizationTransaction, type AuthorizationTransactionInput, type ConsumeAuthorizationTransactionIfMatchesInput, type ConsumeBackchannelLogoutTokenJtiInput, type ConsumeExchangeCodeIfMatchesInput, DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS, DEFAULT_EXCHANGE_CODE_TTL_MS, DEFAULT_OIDC_SCOPES, DEFAULT_OIDC_VAULT_BASE_PATH, DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT, type DeleteSessionsByLogicalSessionIdInput, type DeleteSessionsByProviderSessionIdInput, type DeleteSessionsBySubjectInput, type ExchangeCodeRecord, type ExchangeCodeRecordInput, type IssueTokenInput, OIDC_VAULT_ROUTE_PATHS, OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT, type OidcVaultAccessTokenConfirmation, type OidcVaultAccessTokenMiddlewareErrorContext, type OidcVaultAccessTokenMiddlewareOptions, type OidcVaultAccessTokenRequestInput, type OidcVaultAccessTokenValidationResult, type OidcVaultAccessTokenValidator, type OidcVaultApiDeviceBindingOptions, type OidcVaultAuthContext, type OidcVaultAuthenticatedRequest, type OidcVaultBackchannelLogoutResult, type OidcVaultConfig, type OidcVaultConfigMode, type OidcVaultCookieDeploymentMode, type OidcVaultCookieOptions, type OidcVaultCookieSameSite, type OidcVaultDeviceBindingMode, type OidcVaultDeviceBindingOptions, type OidcVaultDeviceBindingStoreProvider, type OidcVaultDpopAlgorithm, type OidcVaultDpopBinding, type OidcVaultDpopNonceOptions, type OidcVaultDpopProofOptions, OidcVaultDpopReplayCapacityError, type OidcVaultDpopReplayStore, type OidcVaultEnv, type OidcVaultErrorContext, type OidcVaultExchangeResult, type OidcVaultFingerprintRecognitionOptions, type OidcVaultHookContext, type OidcVaultHooks, type OidcVaultJwtAccessTokenValidatorOptions, type OidcVaultLoginInitiationInput, type OidcVaultLoginInitiationResult, type OidcVaultLogoutResult, type OidcVaultOptions, type OidcVaultProviderMetadata, type OidcVaultRecordBindingMatch, type OidcVaultRequestAwareAccessTokenValidationResult, type OidcVaultRequestAwareAccessTokenValidator, type OidcVaultResolvedConfig, type OidcVaultRouteName, type OidcVaultSession, type OidcVaultSessionInput, type OidcVaultSessionRevocationContext, type OidcVaultSessionTransport, OidcVaultStoreConflictError, type OidcVaultStoreProvider, type OidcVaultTokenIssueResult, type OidcVaultTokenIssuer, type OidcVaultTransactionCookieOptions, type OidcVaultUserProfile, type OidcVaultVerifiedDpopBinding, type ReserveDpopProofInput, type RotateSessionInput, createOidcVaultAccessTokenMiddleware, createOidcVaultJwtAccessTokenValidator, createOidcVaultMiddleware, normalizeOidcVaultBasePath, resolveOidcVaultConfig, resolveOidcVaultConfigFromEnv };