@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.mjs CHANGED
@@ -1,119 +1,7 @@
1
1
  // src/index.ts
2
- import { createHash, randomBytes } from "crypto";
2
+ import { createHash as createHash6, randomBytes as randomBytes3 } from "crypto";
3
3
  import express from "express";
4
4
 
5
- // src/config.ts
6
- var DEFAULT_OIDC_SCOPES = "openid email profile";
7
- var MANUAL_CONFIG_REQUIREMENTS = ["issuer", "authorizationEndpoint", "tokenEndpoint", "jwksUri"];
8
- var normalizeOptionalString = (value) => {
9
- const trimmed = value?.trim();
10
- return trimmed ? trimmed : void 0;
11
- };
12
- var normalizeHttpUrl = (value, optionName) => {
13
- if (value === void 0) {
14
- return void 0;
15
- }
16
- let url;
17
- try {
18
- url = new URL(value);
19
- } catch {
20
- throw new Error(`${optionName} must be an absolute HTTP(S) URL.`);
21
- }
22
- if (url.protocol !== "https:" && url.protocol !== "http:" || url.origin === "null") {
23
- throw new Error(`${optionName} must use http or https.`);
24
- }
25
- return url.toString();
26
- };
27
- var withOptionalValue = (target, key, value) => {
28
- if (value !== void 0) {
29
- target[key] = value;
30
- }
31
- };
32
- function resolveOidcVaultConfig(config = {}) {
33
- const issuer = normalizeHttpUrl(normalizeOptionalString(config.issuer), "config.issuer");
34
- const clientId = normalizeOptionalString(config.clientId);
35
- const clientSecret = normalizeOptionalString(config.clientSecret);
36
- const scopes = normalizeOptionalString(config.scopes) ?? DEFAULT_OIDC_SCOPES;
37
- const hasManualEndpoints = MANUAL_CONFIG_REQUIREMENTS.some(
38
- (key) => key !== "issuer" && normalizeOptionalString(config[key])
39
- );
40
- if (!clientId) {
41
- throw new Error("OIDC clientId is required.");
42
- }
43
- if (issuer && !hasManualEndpoints) {
44
- const resolved2 = {
45
- mode: "issuer",
46
- issuer,
47
- scopes
48
- };
49
- withOptionalValue(resolved2, "clientId", clientId);
50
- withOptionalValue(resolved2, "clientSecret", clientSecret);
51
- return resolved2;
52
- }
53
- const resolved = {
54
- mode: "manual",
55
- scopes
56
- };
57
- withOptionalValue(resolved, "issuer", issuer);
58
- withOptionalValue(
59
- resolved,
60
- "authorizationEndpoint",
61
- normalizeHttpUrl(normalizeOptionalString(config.authorizationEndpoint), "config.authorizationEndpoint")
62
- );
63
- withOptionalValue(
64
- resolved,
65
- "tokenEndpoint",
66
- normalizeHttpUrl(normalizeOptionalString(config.tokenEndpoint), "config.tokenEndpoint")
67
- );
68
- withOptionalValue(
69
- resolved,
70
- "userInfoEndpoint",
71
- normalizeHttpUrl(normalizeOptionalString(config.userInfoEndpoint), "config.userInfoEndpoint")
72
- );
73
- withOptionalValue(resolved, "jwksUri", normalizeHttpUrl(normalizeOptionalString(config.jwksUri), "config.jwksUri"));
74
- withOptionalValue(
75
- resolved,
76
- "endSessionEndpoint",
77
- normalizeHttpUrl(normalizeOptionalString(config.endSessionEndpoint), "config.endSessionEndpoint")
78
- );
79
- withOptionalValue(resolved, "clientId", clientId);
80
- withOptionalValue(resolved, "clientSecret", clientSecret);
81
- const missing = MANUAL_CONFIG_REQUIREMENTS.filter((key) => !resolved[key]);
82
- if (missing.length > 0) {
83
- throw new Error(`Missing required OIDC configuration for manual mode: ${missing.join(", ")}`);
84
- }
85
- return resolved;
86
- }
87
- function resolveOidcVaultConfigFromEnv(env = process.env) {
88
- return resolveOidcVaultConfig({
89
- issuer: env.OIDC_ISSUER,
90
- authorizationEndpoint: env.OIDC_AUTHORIZATION_ENDPOINT,
91
- tokenEndpoint: env.OIDC_TOKEN_ENDPOINT,
92
- userInfoEndpoint: env.OIDC_USERINFO_ENDPOINT,
93
- jwksUri: env.OIDC_JWKS_URI,
94
- endSessionEndpoint: env.OIDC_END_SESSION_ENDPOINT,
95
- clientId: env.OIDC_CLIENT_ID,
96
- clientSecret: env.OIDC_CLIENT_SECRET,
97
- scopes: env.OIDC_SCOPES
98
- });
99
- }
100
-
101
- // src/constants.ts
102
- var DEFAULT_OIDC_VAULT_BASE_PATH = "/auth/oidc";
103
- var DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS = 10 * 60 * 1e3;
104
- var DEFAULT_EXCHANGE_CODE_TTL_MS = 30 * 1e3;
105
- var DEFAULT_SESSION_COOKIE_NAME = "oidc_vault_session";
106
- var DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT = "16kb";
107
- var OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT = 16;
108
- var OIDC_VAULT_ROUTE_PATHS = {
109
- login: "/login",
110
- callback: "/callback",
111
- exchange: "/exchange",
112
- refresh: "/refresh",
113
- logout: "/logout",
114
- "backchannel-logout": "/backchannel-logout"
115
- };
116
-
117
5
  // src/errors.ts
118
6
  var OidcVaultHttpError = class extends Error {
119
7
  constructor(status, code, message, clientMessage = message) {
@@ -193,40 +81,33 @@ var getRequiredFiniteNonNegativeInteger = (value, message, code, status = 502) =
193
81
  return value;
194
82
  };
195
83
 
84
+ // src/fingerprint-recognition.ts
85
+ import { createHash as createHash3 } from "crypto";
86
+
87
+ // src/constants.ts
88
+ var DEFAULT_OIDC_VAULT_BASE_PATH = "/auth/oidc";
89
+ var DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS = 10 * 60 * 1e3;
90
+ var DEFAULT_EXCHANGE_CODE_TTL_MS = 30 * 1e3;
91
+ var DEFAULT_SESSION_COOKIE_NAME = "oidc_vault_session";
92
+ var DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT = "16kb";
93
+ var OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT = 16;
94
+ var OIDC_VAULT_ROUTE_PATHS = {
95
+ login: "/login",
96
+ callback: "/callback",
97
+ exchange: "/exchange",
98
+ refresh: "/refresh",
99
+ logout: "/logout",
100
+ "backchannel-logout": "/backchannel-logout"
101
+ };
102
+
196
103
  // src/cookies.ts
197
104
  var getSessionTransport = (options) => options.sessionTransport ?? "body";
198
105
  var usesCookieTransport = (options) => getSessionTransport(options) === "cookie";
199
- var parseCookieHeader = (headerValue) => {
200
- if (!headerValue) {
201
- return {};
202
- }
203
- return headerValue.split(";").reduce((cookies, part) => {
204
- const separatorIndex = part.indexOf("=");
205
- if (separatorIndex === -1) {
206
- return cookies;
207
- }
208
- const name = part.slice(0, separatorIndex).trim();
209
- const value = part.slice(separatorIndex + 1).trim();
210
- if (!name) {
211
- return cookies;
212
- }
213
- try {
214
- cookies[name] = decodeURIComponent(value);
215
- } catch {
216
- throw new OidcVaultHttpError(
217
- 400,
218
- "OIDC_VAULT_MALFORMED_SESSION_COOKIE",
219
- "OIDC vault session cookie is malformed."
220
- );
221
- }
222
- return cookies;
223
- }, {});
224
- };
225
106
  var resolveCookieOptions = (options) => {
226
107
  const cookieOptions = options.cookie ?? {};
227
108
  const deploymentMode = cookieOptions.deploymentMode ?? "same-origin";
228
109
  const sameSite = cookieOptions.sameSite ?? (deploymentMode === "cross-site" ? "none" : "lax");
229
- const secure = cookieOptions.secure ?? (sameSite === "none" || deploymentMode === "cross-site");
110
+ const secure = cookieOptions.secure ?? (sameSite === "none" || deploymentMode === "cross-site" || isHttpsBackendOrigin(options));
230
111
  return {
231
112
  name: cookieOptions.name ?? DEFAULT_SESSION_COOKIE_NAME,
232
113
  sameSite,
@@ -236,12 +117,25 @@ var resolveCookieOptions = (options) => {
236
117
  httpOnly: cookieOptions.httpOnly ?? true
237
118
  };
238
119
  };
120
+ var isEffectivelySecureCookie = (cookieOptions) => cookieOptions.secure || cookieOptions.sameSite === "none";
121
+ var isHttpsBackendOrigin = (options) => {
122
+ try {
123
+ const raw = options.backendOrigin;
124
+ if (typeof raw !== "string") {
125
+ return false;
126
+ }
127
+ return new URL(raw.trim()).protocol === "https:";
128
+ } catch {
129
+ return false;
130
+ }
131
+ };
239
132
  var COOKIE_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
133
+ var isValidCookieName = (value) => typeof value === "string" && COOKIE_NAME_PATTERN.test(value) && !hasUnsafeCookieValueCharacters(value);
240
134
  var COOKIE_DOMAIN_PATTERN = /^\.?[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
241
135
  var hasUnsafeCookieValueCharacters = (value) => {
242
136
  for (let index = 0; index < value.length; index += 1) {
243
137
  const code = value.charCodeAt(index);
244
- if (code <= 31 || code === 127 || value[index] === ";") {
138
+ if (code < 32 || code > 126 || value[index] === ";") {
245
139
  return true;
246
140
  }
247
141
  }
@@ -252,7 +146,7 @@ var validateCookieOptions = (options) => {
252
146
  return;
253
147
  }
254
148
  const cookieOptions = resolveCookieOptions(options);
255
- if (!COOKIE_NAME_PATTERN.test(cookieOptions.name)) {
149
+ if (!isValidCookieName(cookieOptions.name)) {
256
150
  throw new Error("cookie.name must be a valid HTTP cookie name.");
257
151
  }
258
152
  if (hasUnsafeCookieValueCharacters(cookieOptions.path) || !cookieOptions.path.startsWith("/")) {
@@ -264,6 +158,22 @@ var validateCookieOptions = (options) => {
264
158
  if (!cookieOptions.httpOnly) {
265
159
  throw new Error("cookie.httpOnly must be true for cookie session transport.");
266
160
  }
161
+ const effectiveSecure = isEffectivelySecureCookie(cookieOptions);
162
+ if (cookieOptions.name.startsWith("__Host-")) {
163
+ if (!effectiveSecure) {
164
+ throw new Error("cookie.name with the __Host- prefix must be Secure.");
165
+ }
166
+ if (cookieOptions.domain !== void 0) {
167
+ throw new Error("cookie.name with the __Host- prefix must not set cookie.domain.");
168
+ }
169
+ if (cookieOptions.path !== "/") {
170
+ throw new Error('cookie.name with the __Host- prefix must use cookie.path "/".');
171
+ }
172
+ } else if (cookieOptions.name.startsWith("__Secure-")) {
173
+ if (!effectiveSecure) {
174
+ throw new Error("cookie.name with the __Secure- prefix must be Secure.");
175
+ }
176
+ }
267
177
  };
268
178
  var usesCrossSiteCookieTransport = (options) => usesCookieTransport(options) && resolveCookieOptions(options).sameSite === "none";
269
179
  var serializeCookie = (name, value, options, overrides) => {
@@ -284,26 +194,878 @@ var serializeCookie = (name, value, options, overrides) => {
284
194
  if (overrides?.expires) {
285
195
  parts.push(`Expires=${overrides.expires.toUTCString()}`);
286
196
  }
287
- return parts.join("; ");
288
- };
289
- var setSessionCookie = (res, options, sessionId) => {
290
- const cookieOptions = resolveCookieOptions(options);
291
- res.append("Set-Cookie", serializeCookie(cookieOptions.name, sessionId, cookieOptions));
292
- };
293
- var clearSessionCookie = (res, options) => {
294
- const cookieOptions = resolveCookieOptions(options);
295
- res.append(
296
- "Set-Cookie",
297
- serializeCookie(cookieOptions.name, "", cookieOptions, {
298
- maxAge: 0,
299
- expires: /* @__PURE__ */ new Date(0)
300
- })
197
+ return parts.join("; ");
198
+ };
199
+ var setSessionCookie = (res, options, sessionId) => {
200
+ const cookieOptions = resolveCookieOptions(options);
201
+ res.append("Set-Cookie", serializeCookie(cookieOptions.name, sessionId, cookieOptions));
202
+ };
203
+ var clearSessionCookie = (res, options) => {
204
+ const cookieOptions = resolveCookieOptions(options);
205
+ res.append(
206
+ "Set-Cookie",
207
+ serializeCookie(cookieOptions.name, "", cookieOptions, {
208
+ maxAge: 0,
209
+ expires: /* @__PURE__ */ new Date(0)
210
+ })
211
+ );
212
+ };
213
+ var getSessionIdFromCookie = (req, options) => {
214
+ const raw = parseSelectedCookieValue(req.headers.cookie, resolveCookieOptions(options).name);
215
+ if (raw === void 0) {
216
+ return void 0;
217
+ }
218
+ try {
219
+ return decodeURIComponent(raw);
220
+ } catch {
221
+ throw new OidcVaultHttpError(400, "OIDC_VAULT_MALFORMED_SESSION_COOKIE", "OIDC vault session cookie is malformed.");
222
+ }
223
+ };
224
+ var parseSelectedCookieValue = (headerValue, selectedName) => {
225
+ if (!headerValue) {
226
+ return void 0;
227
+ }
228
+ const segments = headerValue.split(";");
229
+ for (const segment of segments) {
230
+ const separatorIndex = segment.indexOf("=");
231
+ if (separatorIndex === -1) {
232
+ continue;
233
+ }
234
+ const name = segment.slice(0, separatorIndex).trim();
235
+ if (!name || name !== selectedName) {
236
+ continue;
237
+ }
238
+ return segment.slice(separatorIndex + 1).trim();
239
+ }
240
+ return void 0;
241
+ };
242
+
243
+ // src/device-binding-policy.ts
244
+ import { isIP } from "net";
245
+ var isPlainRecord = (value) => {
246
+ if (typeof value !== "object" || value === null) return false;
247
+ const prototype = Object.getPrototypeOf(value);
248
+ return prototype === Object.prototype || prototype === null;
249
+ };
250
+ var isDpopAlgorithm = (value) => value === "ES256" || value === "PS256" || value === "RS256";
251
+ var resolveInteger = (value, fallback, min, max, name) => {
252
+ const resolved = value === void 0 ? fallback : value;
253
+ if (typeof resolved !== "number" || !Number.isInteger(resolved) || resolved < min || resolved > max) {
254
+ throw new Error(`${name} must be an integer from ${min} through ${max}.`);
255
+ }
256
+ return resolved;
257
+ };
258
+ var resolveDeviceBindingOptions = (value) => {
259
+ if (value === void 0) return void 0;
260
+ if (!isPlainRecord(value)) throw new Error("deviceBinding must be a plain options object.");
261
+ const { mode, algorithms, proofMaxAgeSeconds, clockSkewSeconds, nonce } = value;
262
+ if (mode !== void 0 && mode !== "optional" && mode !== "required") {
263
+ throw new Error("deviceBinding.mode must be optional or required.");
264
+ }
265
+ const configuredAlgorithms = algorithms === void 0 ? ["ES256"] : algorithms;
266
+ if (!Array.isArray(configuredAlgorithms) || configuredAlgorithms.length === 0) {
267
+ throw new Error("deviceBinding.algorithms must be a nonempty allowlist of ES256, PS256, or RS256.");
268
+ }
269
+ const allowlist = Array.from(configuredAlgorithms);
270
+ if (!allowlist.every(isDpopAlgorithm)) {
271
+ throw new Error("deviceBinding.algorithms must be a nonempty allowlist of ES256, PS256, or RS256.");
272
+ }
273
+ let resolvedNonce = false;
274
+ if (nonce !== void 0 && nonce !== false) {
275
+ if (!isPlainRecord(nonce)) throw new Error("deviceBinding.nonce must be false or a plain nonce options object.");
276
+ const { secret, lifetimeSeconds } = nonce;
277
+ if (!(secret instanceof Uint8Array) || secret.byteLength < 32) {
278
+ throw new Error("deviceBinding.nonce.secret must be a Uint8Array of at least 32 bytes.");
279
+ }
280
+ resolvedNonce = Object.freeze({
281
+ // Uint8Array.from also copies Buffer/subarray inputs instead of retaining
282
+ // a shared view. Nonempty typed arrays cannot be Object.freeze'd; these
283
+ // owned bytes stay private while the plain nonce container is frozen.
284
+ secret: Uint8Array.from(secret),
285
+ lifetimeSeconds: resolveInteger(lifetimeSeconds, 60, 1, 300, "deviceBinding.nonce.lifetimeSeconds")
286
+ });
287
+ }
288
+ return Object.freeze({
289
+ mode: mode ?? "optional",
290
+ algorithms: Object.freeze([...new Set(allowlist)]),
291
+ proofMaxAgeSeconds: resolveInteger(proofMaxAgeSeconds, 60, 1, 300, "deviceBinding.proofMaxAgeSeconds"),
292
+ clockSkewSeconds: resolveInteger(clockSkewSeconds, 5, 0, 30, "deviceBinding.clockSkewSeconds"),
293
+ nonce: resolvedNonce
294
+ });
295
+ };
296
+ var DEVICE_BINDING_STORE_CAPABILITIES = [
297
+ "getAuthorizationTransaction",
298
+ "consumeAuthorizationTransactionIfMatches",
299
+ "getExchangeCode",
300
+ "consumeExchangeCodeIfMatches",
301
+ "getSessionRevocationContext",
302
+ "reserveDpopProof"
303
+ ];
304
+ function assertDeviceBindingStoreCapabilities(store, feature = "deviceBinding") {
305
+ for (const name of DEVICE_BINDING_STORE_CAPABILITIES) {
306
+ if (typeof store?.[name] !== "function") {
307
+ throw new Error(`${feature} requires storeProvider.${name}.`);
308
+ }
309
+ }
310
+ }
311
+ var resolveDeviceBindingOrigin = (value, name = "backendOrigin") => {
312
+ if (typeof value !== "string") throw new Error(`${name} must be a static HTTP(S) origin for deviceBinding.`);
313
+ const raw = value.trim();
314
+ let url;
315
+ try {
316
+ url = new URL(raw);
317
+ } catch {
318
+ throw new Error(`${name} must be a static HTTP(S) origin for deviceBinding.`);
319
+ }
320
+ if (!/^https?:\/\/[^/\\?#]+\/?$/i.test(raw) || /\s/.test(raw) || raw.includes("@") || url.username !== "" || url.password !== "" || url.pathname !== "/" || url.origin === "null") {
321
+ throw new Error(
322
+ `${name} must be a static HTTP(S) origin without userinfo, path, query, or fragment for deviceBinding.`
323
+ );
324
+ }
325
+ const loopback = url.hostname === "localhost" || url.hostname === "[::1]" || isIP(url.hostname) === 4 && url.hostname.startsWith("127.");
326
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
327
+ throw new Error(`${name} must use HTTPS except for loopback HTTP development when deviceBinding is enabled.`);
328
+ }
329
+ return url.origin;
330
+ };
331
+ var isCanonicalDpopJkt = (value) => typeof value === "string" && value.length === 43 && /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/.test(value);
332
+ var invalidBinding = (diagnostic) => new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_DPOP_PROOF", diagnostic, "DPoP proof validation failed.");
333
+ var snapshotDpopBinding = (value) => {
334
+ if (value === void 0) return void 0;
335
+ if (!isPlainRecord(value)) throw invalidBinding("Stored device binding must be a plain dpop binding object.");
336
+ const { type, jkt } = value;
337
+ if (type !== "dpop" || !isCanonicalDpopJkt(jkt)) {
338
+ throw invalidBinding("Stored device binding must contain type dpop and a canonical SHA-256 JWK thumbprint.");
339
+ }
340
+ return Object.freeze({ type, jkt });
341
+ };
342
+ var assertDeviceBindingPolicy = (policy, binding) => {
343
+ const stored = snapshotDpopBinding(binding);
344
+ if (stored !== void 0 && policy === void 0) {
345
+ throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
346
+ }
347
+ if (stored === void 0 && policy?.mode === "required") {
348
+ throw new OidcVaultHttpError(401, "OIDC_VAULT_DEVICE_BINDING_REQUIRED", "A device-bound login is required.");
349
+ }
350
+ };
351
+ var resolveVerifiedDeviceBinding = (policy, binding, verified) => {
352
+ const stored = snapshotDpopBinding(binding);
353
+ assertDeviceBindingPolicy(policy, stored);
354
+ if (verified === void 0) {
355
+ if (stored !== void 0) {
356
+ throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
357
+ }
358
+ return void 0;
359
+ }
360
+ const proofBinding = snapshotDpopBinding(verified);
361
+ const alg = verified.alg;
362
+ if (proofBinding === void 0 || !isDpopAlgorithm(alg) || !policy?.algorithms.includes(alg)) {
363
+ throw invalidBinding("Verified DPoP context must use a currently allowed asymmetric proof algorithm.");
364
+ }
365
+ if (stored === void 0) return void 0;
366
+ if (stored.jkt !== proofBinding.jkt) throw invalidBinding("Verified DPoP key does not match the original binding.");
367
+ return Object.freeze({ type: "dpop", jkt: stored.jkt, alg });
368
+ };
369
+ var withSessionDeviceBinding = (session, binding) => {
370
+ const restored = {};
371
+ for (const key of Reflect.ownKeys(session)) {
372
+ if (key !== "deviceBinding" && Object.prototype.propertyIsEnumerable.call(session, key))
373
+ Object.defineProperty(restored, key, {
374
+ value: Reflect.get(session, key),
375
+ enumerable: true,
376
+ configurable: true,
377
+ writable: true
378
+ });
379
+ }
380
+ const stored = snapshotDpopBinding(binding);
381
+ if (stored !== void 0) restored.deviceBinding = stored;
382
+ return restored;
383
+ };
384
+
385
+ // src/dpop-proof.ts
386
+ import { createHash as createHash2 } from "crypto";
387
+ import { calculateJwkThumbprint, compactVerify, importJWK } from "jose";
388
+
389
+ // src/dpop-nonce.ts
390
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
391
+ var NONCE_VERSION = "v1";
392
+ var HMAC_DOMAIN = "@web-ts-toolkit/express-oidc-vault/dpop-nonce/v1\0";
393
+ var DPOP_NONCE_MAX_BYTES = 512;
394
+ var isEpochMs = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
395
+ var decodeBase64url = (value, byteLength) => {
396
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) return null;
397
+ const bytes = Buffer.from(value, "base64url");
398
+ if (bytes.toString("base64url") !== value || byteLength !== void 0 && bytes.length !== byteLength) return null;
399
+ return bytes;
400
+ };
401
+ var createDpopNoncePolicy = (options) => {
402
+ const { secret: configuredSecret, lifetimeSeconds: configuredLifetime, namespace } = options;
403
+ if (!(configuredSecret instanceof Uint8Array) || configuredSecret.byteLength < 32) {
404
+ throw new TypeError("DPoP nonce secret must be a Uint8Array of at least 32 bytes.");
405
+ }
406
+ const lifetimeSeconds = configuredLifetime === void 0 ? 60 : configuredLifetime;
407
+ if (!Number.isInteger(lifetimeSeconds) || lifetimeSeconds < 1 || lifetimeSeconds > 300) {
408
+ throw new TypeError("DPoP nonce lifetime must be an integer from 1 through 300 seconds.");
409
+ }
410
+ if (typeof namespace !== "string" || namespace.length === 0) {
411
+ throw new TypeError("DPoP nonce requires a resolved protection-space namespace.");
412
+ }
413
+ const secret = Uint8Array.from(configuredSecret);
414
+ const namespaceDigest = createHash("sha256").update(namespace, "utf8").digest("base64url");
415
+ const lifetimeMs = lifetimeSeconds * 1e3;
416
+ const sign = (encodedPayload) => createHmac("sha256", secret).update(HMAC_DOMAIN, "utf8").update(`${NONCE_VERSION}.${encodedPayload}`, "ascii").digest();
417
+ return Object.freeze({
418
+ issue(jkt, now) {
419
+ const expiresAt = now + lifetimeMs;
420
+ if (!isCanonicalDpopJkt(jkt) || !isEpochMs(now) || !isEpochMs(expiresAt) || expiresAt <= now) {
421
+ throw new TypeError("DPoP nonce issuance requires a canonical key thumbprint and a safe live epoch window.");
422
+ }
423
+ const payload = JSON.stringify([randomBytes(16).toString("base64url"), now, expiresAt, namespaceDigest, jkt]);
424
+ const encodedPayload = Buffer.from(payload, "utf8").toString("base64url");
425
+ const nonce = `${NONCE_VERSION}.${encodedPayload}.${sign(encodedPayload).toString("base64url")}`;
426
+ if (Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES) {
427
+ throw new TypeError("DPoP nonce issuance exceeded its fixed wire bound.");
428
+ }
429
+ return nonce;
430
+ },
431
+ verify(nonce, jkt, now) {
432
+ if (!isCanonicalDpopJkt(jkt) || !isEpochMs(now) || typeof nonce !== "string" || nonce.length > DPOP_NONCE_MAX_BYTES || Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES)
433
+ return false;
434
+ const [version, encodedPayload, encodedMac, extra] = nonce.split(".");
435
+ if (version !== NONCE_VERSION || !encodedPayload || !encodedMac || extra !== void 0) return false;
436
+ const mac = decodeBase64url(encodedMac, 32);
437
+ const bytes = decodeBase64url(encodedPayload);
438
+ if (!mac || !bytes || !timingSafeEqual(mac, sign(encodedPayload))) return false;
439
+ let payload;
440
+ try {
441
+ payload = JSON.parse(bytes.toString("utf8"));
442
+ } catch {
443
+ return false;
444
+ }
445
+ if (!Array.isArray(payload) || payload.length !== 5 || !bytes.equals(Buffer.from(JSON.stringify(payload), "utf8")))
446
+ return false;
447
+ const [random, issuedAt, expiresAt, context, key] = payload;
448
+ if (typeof random !== "string" || !decodeBase64url(random, 16) || !isEpochMs(issuedAt) || !isEpochMs(expiresAt) || context !== namespaceDigest || key !== jkt)
449
+ return false;
450
+ const declaredLifetime = expiresAt - issuedAt;
451
+ return declaredLifetime >= 1e3 && declaredLifetime <= lifetimeMs && declaredLifetime % 1e3 === 0 && issuedAt <= now && now < expiresAt;
452
+ }
453
+ });
454
+ };
455
+
456
+ // src/dpop-target.ts
457
+ var invalidTarget = () => new OidcVaultHttpError(
458
+ 401,
459
+ "OIDC_VAULT_INVALID_DPOP_PROOF",
460
+ "DPoP target must be a valid pinned HTTP(S) URL/path.",
461
+ "DPoP proof validation failed."
462
+ );
463
+ var hasControl = (value) => {
464
+ for (let index = 0; index < value.length; index++) {
465
+ const code = value.charCodeAt(index);
466
+ if (code < 32 || code === 127) return true;
467
+ }
468
+ return false;
469
+ };
470
+ var normalizePath = (origin, path) => {
471
+ if (!path.startsWith("/") || /[\s\\]/.test(path) || hasControl(path) || /%(?![0-9a-f]{2})/i.test(path))
472
+ throw invalidTarget();
473
+ const decoded = path.replace(/%([0-9a-f]{2})/gi, (_, hex) => {
474
+ const character = String.fromCharCode(Number.parseInt(hex, 16));
475
+ return /^[A-Za-z0-9._~-]$/.test(character) ? character : `%${hex.toUpperCase()}`;
476
+ });
477
+ return new URL(`${origin}${decoded}`).pathname;
478
+ };
479
+ var createDpopRequestTargetResolver = (options) => {
480
+ const { publicOrigin, publicPathPrefix } = options;
481
+ const origin = resolveDeviceBindingOrigin(publicOrigin, "publicOrigin");
482
+ let prefix = "";
483
+ if (publicPathPrefix !== void 0 && publicPathPrefix !== "") {
484
+ if (typeof publicPathPrefix !== "string" || /[?#]/.test(publicPathPrefix)) {
485
+ throw new TypeError("deviceBinding.publicPathPrefix must be an absolute path prefix without query or fragment.");
486
+ }
487
+ try {
488
+ prefix = normalizePath(origin, publicPathPrefix).replace(/\/+$/g, "");
489
+ } catch {
490
+ throw new TypeError("deviceBinding.publicPathPrefix must be a valid absolute path prefix.");
491
+ }
492
+ }
493
+ return (originalUrl) => {
494
+ if (typeof originalUrl !== "string") throw invalidTarget();
495
+ const path = originalUrl.split(/[?#]/, 1)[0];
496
+ if (!path.startsWith("/")) throw invalidTarget();
497
+ return `${origin}${normalizePath(origin, `${prefix}${path}`)}`;
498
+ };
499
+ };
500
+ var normalizeDpopProofTarget = (value) => {
501
+ if (typeof value !== "string" || /[\s\\]/.test(value) || hasControl(value) || !/^https?:\/\/[^/\\?#]+/i.test(value))
502
+ throw invalidTarget();
503
+ let url;
504
+ try {
505
+ url = new URL(value);
506
+ } catch {
507
+ throw invalidTarget();
508
+ }
509
+ const queryFree = value.split(/[?#]/, 1)[0];
510
+ const authority = queryFree.slice(queryFree.indexOf("://") + 3).split("/", 1)[0];
511
+ if (url.username || url.password || url.origin === "null" || authority.includes("@")) throw invalidTarget();
512
+ const authorityEnd = queryFree.indexOf("/", queryFree.indexOf("://") + 3);
513
+ const path = authorityEnd === -1 ? "/" : queryFree.slice(authorityEnd);
514
+ return `${url.origin}${normalizePath(url.origin, path)}`;
515
+ };
516
+
517
+ // src/dpop-proof.ts
518
+ var DPOP_PROOF_MAX_BYTES = 8192;
519
+ var DPOP_PROTECTED_HEADER_MAX_BYTES = 2048;
520
+ var readDpopRawHeader = (req, name) => {
521
+ const normalizedName = name.toLowerCase();
522
+ let count = 0;
523
+ let value;
524
+ for (let index = 0; index < req.rawHeaders.length; index += 2) {
525
+ if (req.rawHeaders[index].toLowerCase() !== normalizedName) continue;
526
+ if (++count === 1) value = req.rawHeaders[index + 1];
527
+ if (count === 2) break;
528
+ }
529
+ return Object.freeze({ count, value });
530
+ };
531
+ var invalidProof = (diagnostic, cause) => {
532
+ const error = new OidcVaultHttpError(
533
+ 401,
534
+ "OIDC_VAULT_INVALID_DPOP_PROOF",
535
+ diagnostic,
536
+ "DPoP proof validation failed."
537
+ );
538
+ if (cause !== void 0) Object.defineProperty(error, "cause", { value: cause, configurable: true });
539
+ return error;
540
+ };
541
+ var extractDpopProof = (header) => {
542
+ if (header.count === 0) return void 0;
543
+ const { value } = header;
544
+ if (header.count !== 1 || typeof value !== "string" || value.length === 0 || value.length > DPOP_PROOF_MAX_BYTES || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value)) {
545
+ throw invalidProof("DPoP header must contain one bounded compact signed proof.");
546
+ }
547
+ return value;
548
+ };
549
+ var decodeBase64url2 = (value, maxBytes) => {
550
+ if (!value || value.length > Math.ceil(maxBytes * 4 / 3) || !/^[A-Za-z0-9_-]+$/.test(value)) {
551
+ throw invalidProof("DPoP base64url input exceeds its bound or has invalid encoding.");
552
+ }
553
+ const bytes = Buffer.from(value, "base64url");
554
+ if (bytes.length > maxBytes || bytes.toString("base64url") !== value)
555
+ throw invalidProof("DPoP base64url input is not canonical.");
556
+ return bytes;
557
+ };
558
+ var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
559
+ var parseObject = (bytes) => {
560
+ try {
561
+ const json = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
562
+ const value = JSON.parse(json);
563
+ if (!isPlainObject(value)) throw new TypeError("DPoP JSON must be an object.");
564
+ const stack = [];
565
+ for (let index = 0; index < json.length; index++) {
566
+ const character = json[index];
567
+ if (character === "{" || character === "[") {
568
+ stack.push({ keys: character === "{" ? /* @__PURE__ */ new Set() : null, expectsKey: character === "{" });
569
+ } else if (character === "}" || character === "]") {
570
+ stack.pop();
571
+ } else if (character === ",") {
572
+ const container = stack[stack.length - 1];
573
+ if (container?.keys) container.expectsKey = true;
574
+ } else if (character === '"') {
575
+ const start = index++;
576
+ for (; index < json.length; index++) {
577
+ if (json[index] === "\\") index++;
578
+ else if (json[index] === '"') break;
579
+ }
580
+ const container = stack[stack.length - 1];
581
+ if (container?.keys && container.expectsKey) {
582
+ const key = JSON.parse(json.slice(start, index + 1));
583
+ if (container.keys.has(key)) throw new TypeError("DPoP JSON has duplicate member names.");
584
+ container.keys.add(key);
585
+ container.expectsKey = false;
586
+ }
587
+ }
588
+ }
589
+ return value;
590
+ } catch (cause) {
591
+ throw invalidProof("DPoP JSON encoding or object shape is invalid.", cause);
592
+ }
593
+ };
594
+ var publicJwk = (value, alg) => {
595
+ if (!isPlainObject(value) || Buffer.byteLength(JSON.stringify(value), "utf8") > DPOP_PROTECTED_HEADER_MAX_BYTES) {
596
+ throw invalidProof("DPoP JWK must be a bounded public object.");
597
+ }
598
+ const allowed = alg === "ES256" ? ["kty", "crv", "x", "y", "alg", "use", "key_ops", "kid"] : ["kty", "n", "e", "alg", "use", "key_ops", "kid"];
599
+ if (Object.keys(value).some((key) => !allowed.includes(key)) || value.alg !== void 0 && value.alg !== alg || value.use !== void 0 && value.use !== "sig" || value.kid !== void 0 && typeof value.kid !== "string" || value.key_ops !== void 0 && (!Array.isArray(value.key_ops) || value.key_ops.length !== 1 || value.key_ops[0] !== "verify")) {
600
+ throw invalidProof("DPoP JWK contains private, remote, unsupported or incompatible key parameters.");
601
+ }
602
+ if (alg === "ES256") {
603
+ if (value.kty !== "EC" || value.crv !== "P-256" || typeof value.x !== "string" || typeof value.y !== "string" || decodeBase64url2(value.x, 32).length !== 32 || decodeBase64url2(value.y, 32).length !== 32) {
604
+ throw invalidProof("DPoP ES256 requires a public P-256 key with canonical coordinates.");
605
+ }
606
+ } else {
607
+ if (value.kty !== "RSA" || typeof value.n !== "string" || typeof value.e !== "string") {
608
+ throw invalidProof("DPoP RSA requires public modulus and exponent.");
609
+ }
610
+ const modulus = decodeBase64url2(value.n, 512);
611
+ const exponent = decodeBase64url2(value.e, 512);
612
+ const bits = (modulus.length - 1) * 8 + 32 - Math.clz32(modulus[0]);
613
+ if (modulus[0] === 0 || bits < 2048 || bits > 4096 || exponent[0] === 0 || (exponent[exponent.length - 1] & 1) === 0 || exponent.length === 1 && exponent[0] < 3 || exponent.length > modulus.length || exponent.length === modulus.length && Buffer.compare(exponent, modulus) >= 0) {
614
+ throw invalidProof(
615
+ "DPoP RSA public parameters must use a 2048\u20134096-bit modulus and a valid bounded odd exponent."
616
+ );
617
+ }
618
+ }
619
+ return value;
620
+ };
621
+ var verifyDpopProof = async (input) => {
622
+ const { proof, method, targetUrl, proofOptions, expectedJkt, accessToken } = input;
623
+ const algorithms = [...proofOptions.algorithms];
624
+ extractDpopProof({ count: 1, value: proof });
625
+ const [encodedHeader, encodedPayload, encodedSignature] = proof.split(".");
626
+ const header = parseObject(decodeBase64url2(encodedHeader, DPOP_PROTECTED_HEADER_MAX_BYTES));
627
+ decodeBase64url2(encodedPayload, DPOP_PROOF_MAX_BYTES);
628
+ const signature = decodeBase64url2(encodedSignature, 512);
629
+ const { alg } = header;
630
+ if (header.typ !== "dpop+jwt" || !isDpopAlgorithm(alg) || !algorithms.includes(alg) || ["crit", "b64", "jku", "x5u", "x5c", "x5t", "x5t#S256"].some((key) => Object.hasOwn(header, key))) {
631
+ throw invalidProof("DPoP protected header has an unsupported type, algorithm or key/critical extension.");
632
+ }
633
+ const jwk = publicJwk(header.jwk, alg);
634
+ const signatureBytes = alg === "ES256" ? 64 : decodeBase64url2(jwk.n, 512).length;
635
+ if (signature.length !== signatureBytes)
636
+ throw invalidProof("DPoP signature length does not match the allowed key profile.");
637
+ let payload;
638
+ try {
639
+ const key = await importJWK(jwk, alg);
640
+ ({ payload } = await compactVerify(proof, key, { algorithms }));
641
+ } catch (cause) {
642
+ throw invalidProof("DPoP public-key import or signature verification failed.", cause);
643
+ }
644
+ const claims = parseObject(payload);
645
+ const { htm, htu, iat, jti, nonce, ath } = claims;
646
+ if (typeof iat !== "number" || !Number.isSafeInteger(iat) || iat < 0 || typeof jti !== "string" || jti.length < 1 || jti.length > 128 || /[^\x20-\x7e]/.test(jti)) {
647
+ throw invalidProof("DPoP signed iat/JTI must be nonnegative integer time and bounded printable ASCII.");
648
+ }
649
+ if (htm !== method || typeof method !== "string" || method.length === 0 || normalizeDpopProofTarget(htu) !== targetUrl) {
650
+ throw invalidProof("DPoP signed method or normalized public target does not match this request.");
651
+ }
652
+ if (typeof nonce === "string" && (nonce.length > DPOP_NONCE_MAX_BYTES || Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES)) {
653
+ throw invalidProof("DPoP signed nonce exceeds its byte bound.");
654
+ }
655
+ if (accessToken !== void 0 && (typeof accessToken !== "string" || !/^[\x21-\x7e]+$/.test(accessToken) || !isCanonicalDpopJkt(ath) || ath !== createHash2("sha256").update(accessToken, "ascii").digest("base64url"))) {
656
+ throw invalidProof("DPoP signed access-token hash does not match the presented API token.");
657
+ }
658
+ const jkt = await calculateJwkThumbprint(jwk, "sha256");
659
+ if (expectedJkt !== void 0 && (!isCanonicalDpopJkt(expectedJkt) || jkt !== expectedJkt)) {
660
+ throw invalidProof("DPoP verified public key does not match the original credential confirmation.");
661
+ }
662
+ return Object.freeze({
663
+ binding: Object.freeze({ type: "dpop", jkt, alg }),
664
+ iat,
665
+ jti,
666
+ ...nonce === void 0 ? {} : { nonce }
667
+ });
668
+ };
669
+
670
+ // src/fingerprint-recognition.ts
671
+ var FINGERPRINT_RECOGNITION_METADATA_KEY = "oidcVaultFingerprintRecognition";
672
+ var isPlainRecord2 = (value) => typeof value === "object" && value !== null && [Object.prototype, null].includes(Object.getPrototypeOf(value));
673
+ var COLLIDING_HEADERS = /* @__PURE__ */ new Set([
674
+ "authorization",
675
+ "proxy-authorization",
676
+ "www-authenticate",
677
+ "proxy-authenticate",
678
+ "authentication-info",
679
+ "proxy-authentication-info",
680
+ "dpop",
681
+ "dpop-nonce",
682
+ "cookie",
683
+ "set-cookie",
684
+ "origin",
685
+ "referer",
686
+ "host",
687
+ "forwarded",
688
+ "accept",
689
+ "accept-charset",
690
+ "accept-encoding",
691
+ "accept-language",
692
+ "connection",
693
+ "keep-alive",
694
+ "te",
695
+ "trailer",
696
+ "transfer-encoding",
697
+ "upgrade"
698
+ ]);
699
+ var resolveFingerprintRecognitionOptions = (value) => {
700
+ if (value === void 0) return void 0;
701
+ if (!isPlainRecord2(value)) throw new Error("fingerprintRecognition must be a plain options object.");
702
+ const { headerName } = value;
703
+ const name = headerName === void 0 ? "X-Device-Fingerprint" : headerName;
704
+ if (!isValidCookieName(name)) throw new Error("fingerprintRecognition.headerName must be a valid HTTP field name.");
705
+ const normalized = name.toLowerCase();
706
+ if (COLLIDING_HEADERS.has(normalized) || ["content-", "x-forwarded-", "access-control-", "sec-fetch-"].some((prefix) => normalized.startsWith(prefix))) {
707
+ throw new Error(
708
+ "fingerprintRecognition.headerName must not collide with authentication, cookie, origin, content, or transport headers."
709
+ );
710
+ }
711
+ return Object.freeze({ headerName: name });
712
+ };
713
+ var invalidFingerprint = () => new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_FINGERPRINT", "Fingerprint signal is invalid.");
714
+ var captureFingerprintRecognition = (req, policy) => {
715
+ if (policy === void 0) return void 0;
716
+ const { count, value } = readDpopRawHeader(req, policy.headerName);
717
+ if (count === 0) return void 0;
718
+ if (count !== 1 || typeof value !== "string" || value.length === 0 || value.length > 256) throw invalidFingerprint();
719
+ for (let index = 0; index < value.length; index++) {
720
+ const byte = value.charCodeAt(index);
721
+ if (byte < 32 || byte > 126) throw invalidFingerprint();
722
+ }
723
+ return Object.freeze({ version: 1, hash: createHash3("sha256").update(value, "ascii").digest("base64url") });
724
+ };
725
+ var snapshotFingerprintRecognition = (metadata) => {
726
+ if (metadata === void 0 || !Object.hasOwn(metadata, FINGERPRINT_RECOGNITION_METADATA_KEY)) return void 0;
727
+ const value = metadata?.[FINGERPRINT_RECOGNITION_METADATA_KEY];
728
+ if (value === void 0) return void 0;
729
+ if (!isPlainRecord2(value)) throw new TypeError("Stored fingerprint recognition metadata is invalid.");
730
+ const { version, hash } = value;
731
+ if (Object.keys(value).length !== 2 || !Object.hasOwn(value, "version") || !Object.hasOwn(value, "hash") || version !== 1 || !isCanonicalDpopJkt(hash)) {
732
+ throw new TypeError("Stored fingerprint recognition metadata is invalid.");
733
+ }
734
+ return Object.freeze({ version, hash });
735
+ };
736
+ var fingerprintRecognitionMatches = (actual, original) => actual?.hash === original?.hash;
737
+ var assertFingerprintRecognition = (policy, original, supplied) => {
738
+ if (policy !== void 0 && original !== void 0 && !fingerprintRecognitionMatches(supplied, original)) {
739
+ throw new OidcVaultHttpError(
740
+ 403,
741
+ "OIDC_VAULT_FINGERPRINT_REAUTH_REQUIRED",
742
+ "Browser recognition changed; sign in again."
743
+ );
744
+ }
745
+ };
746
+ var withFingerprintRecognitionMetadata = (metadata, original) => {
747
+ if (metadata === void 0 && original === void 0) return void 0;
748
+ if (original === void 0 && metadata !== void 0 && !Object.hasOwn(metadata, FINGERPRINT_RECOGNITION_METADATA_KEY))
749
+ return metadata;
750
+ const restored = {};
751
+ for (const name of Object.keys(metadata ?? {})) {
752
+ if (name !== FINGERPRINT_RECOGNITION_METADATA_KEY)
753
+ Object.defineProperty(restored, name, {
754
+ value: Reflect.get(metadata, name),
755
+ enumerable: true,
756
+ configurable: true,
757
+ writable: true
758
+ });
759
+ }
760
+ if (original !== void 0) restored[FINGERPRINT_RECOGNITION_METADATA_KEY] = { version: 1, hash: original.hash };
761
+ return restored;
762
+ };
763
+
764
+ // src/lifetime-policy.ts
765
+ var MAX_EPOCH_MS = 864e13;
766
+ var isUsableEpochMs = (value) => typeof value === "number" && Number.isSafeInteger(value) && Math.abs(value) <= MAX_EPOCH_MS;
767
+ var computeExpiresAt = (now, ttlMs, name) => {
768
+ if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
769
+ throw new Error(`${name} must be a positive safe integer in milliseconds.`);
770
+ }
771
+ const expiresAt = now + ttlMs;
772
+ if (!isUsableEpochMs(now) || !isUsableEpochMs(expiresAt) || expiresAt <= now) {
773
+ throw new Error(`${name} must produce a usable epoch-millisecond expiry after now.`);
774
+ }
775
+ return expiresAt;
776
+ };
777
+ var validateLifetimeOptions = (options, now) => {
778
+ computeExpiresAt(
779
+ now,
780
+ options.authorizationTransactionTtlMs === void 0 ? DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS : options.authorizationTransactionTtlMs,
781
+ "authorizationTransactionTtlMs"
782
+ );
783
+ computeExpiresAt(
784
+ now,
785
+ options.exchangeCodeTtlMs === void 0 ? DEFAULT_EXCHANGE_CODE_TTL_MS : options.exchangeCodeTtlMs,
786
+ "exchangeCodeTtlMs"
787
+ );
788
+ if (options.sessionTtlMs !== void 0) {
789
+ computeExpiresAt(now, options.sessionTtlMs, "sessionTtlMs");
790
+ }
791
+ };
792
+
793
+ // src/transaction-cookie.ts
794
+ import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
795
+ var invalidBrowserBinding = () => new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_BROWSER_BINDING", "Login browser binding validation failed.");
796
+ var resolveTransactionCookieOptions = (options, backendOrigin) => {
797
+ const value = options.transactionCookie;
798
+ if (value !== void 0 && (typeof value !== "object" || value === null || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))) {
799
+ throw new Error("transactionCookie must be a plain options object.");
800
+ }
801
+ if (value !== void 0 && Object.keys(value).some((key) => key !== "name" && key !== "sameSite")) {
802
+ throw new Error(
803
+ "transactionCookie supports only name and sameSite; browser-binding cookie security cannot be overridden."
804
+ );
805
+ }
806
+ const { name, sameSite } = value ?? {};
807
+ const secure = new URL(backendOrigin).protocol === "https:";
808
+ const resolvedName = name === void 0 ? secure ? "__Host-oidc_vault_transaction" : "oidc_vault_transaction" : name;
809
+ if (!isValidCookieName(resolvedName)) throw new Error("transactionCookie.name must be a valid HTTP cookie name.");
810
+ if (sameSite !== void 0 && sameSite !== "lax" && sameSite !== "none") {
811
+ throw new Error("transactionCookie.sameSite must be lax or none.");
812
+ }
813
+ if (!secure && sameSite === "none") throw new Error("transactionCookie.sameSite none requires HTTPS.");
814
+ if (!secure && (resolvedName.startsWith("__Host-") || resolvedName.startsWith("__Secure-"))) {
815
+ throw new Error("transactionCookie.name with the __Host- or __Secure- prefix requires HTTPS.");
816
+ }
817
+ if ((options.deviceBinding !== void 0 || options.fingerprintRecognition !== void 0 || value !== void 0) && resolvedName === resolveCookieOptions(options).name) {
818
+ throw new Error("transactionCookie.name must be distinct from cookie.name.");
819
+ }
820
+ return Object.freeze({ name: resolvedName, sameSite: sameSite ?? "lax", secure, path: "/", httpOnly: true });
821
+ };
822
+ var hashSecret = (secret) => createHash4("sha256").update(secret, "ascii").digest("base64url");
823
+ var createTransactionBrowserBinding = () => {
824
+ const secret = randomBytes2(32).toString("base64url");
825
+ return Object.freeze({ secret, browserBindingHash: hashSecret(secret) });
826
+ };
827
+ var parseTransactionCookie = (header, selectedName) => {
828
+ if (header === void 0) return void 0;
829
+ if (typeof header !== "string") throw invalidBrowserBinding();
830
+ let selected;
831
+ for (let start = 0; start < header.length; ) {
832
+ const delimiter = header.indexOf(";", start);
833
+ const end = delimiter === -1 ? header.length : delimiter;
834
+ let separator = start;
835
+ while (separator < end && header[separator] !== "=") separator += 1;
836
+ const hasValue = separator < end;
837
+ let nameStart = start;
838
+ let nameEnd = hasValue ? separator : end;
839
+ while (nameStart < nameEnd && (header[nameStart] === " " || header[nameStart] === " ")) nameStart += 1;
840
+ while (nameEnd > nameStart && (header[nameEnd - 1] === " " || header[nameEnd - 1] === " ")) nameEnd -= 1;
841
+ if (nameEnd - nameStart === selectedName.length && header.startsWith(selectedName, nameStart)) {
842
+ if (selected !== void 0 || !hasValue) throw invalidBrowserBinding();
843
+ if (end - separator - 1 !== 43) throw invalidBrowserBinding();
844
+ const value = header.slice(separator + 1, end);
845
+ if (!isCanonicalDpopJkt(value)) throw invalidBrowserBinding();
846
+ selected = value;
847
+ }
848
+ start = end + 1;
849
+ }
850
+ return selected;
851
+ };
852
+ var snapshotRecordBindingMatch = (record) => {
853
+ const { deviceBinding, browserBindingHash } = record;
854
+ const binding = snapshotDpopBinding(deviceBinding);
855
+ if (binding !== void 0 && Object.keys(deviceBinding).length !== 2) {
856
+ throw new OidcVaultHttpError(
857
+ 401,
858
+ "OIDC_VAULT_INVALID_DPOP_PROOF",
859
+ "Stored device binding contains unsupported fields.",
860
+ "DPoP proof validation failed."
861
+ );
862
+ }
863
+ if (browserBindingHash !== void 0 && !isCanonicalDpopJkt(browserBindingHash) || binding !== void 0 && browserBindingHash === void 0)
864
+ throw invalidBrowserBinding();
865
+ return Object.freeze({ deviceBinding: binding ?? null, browserBindingHash: browserBindingHash ?? null });
866
+ };
867
+ var recordBindingMatches = (actual, expected) => actual.deviceBinding?.jkt === expected.deviceBinding?.jkt && actual.browserBindingHash === expected.browserBindingHash;
868
+ var authenticateTransactionCookie = (header, cookie, match) => {
869
+ if (match.browserBindingHash === null) return void 0;
870
+ const secret = parseTransactionCookie(header, cookie.name);
871
+ if (secret === void 0 || hashSecret(secret) !== match.browserBindingHash) throw invalidBrowserBinding();
872
+ return secret;
873
+ };
874
+ var setTransactionCookie = (res, cookie, secret, expiresAt, now) => {
875
+ if (!isCanonicalDpopJkt(secret) || !isUsableEpochMs(expiresAt) || !isUsableEpochMs(now)) {
876
+ throw new TypeError("OIDC vault transaction cookie requires a canonical secret and usable deadline/clock.");
877
+ }
878
+ res.append(
879
+ "Set-Cookie",
880
+ serializeCookie(cookie.name, secret, cookie, {
881
+ maxAge: Math.max(0, Math.floor((expiresAt - now) / 1e3)),
882
+ expires: new Date(Math.floor(expiresAt / 1e3) * 1e3)
883
+ })
884
+ );
885
+ };
886
+ var clearTransactionCookie = (res, cookie) => {
887
+ res.append("Set-Cookie", serializeCookie(cookie.name, "", cookie, { maxAge: 0, expires: /* @__PURE__ */ new Date(0) }));
888
+ };
889
+
890
+ // src/authorization-transaction.ts
891
+ var PROVIDER_METADATA_KEY = "oidcVaultTransactionProvider";
892
+ var invalidAuthorizationState = () => new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_STATE", "OIDC state is invalid or expired.");
893
+ var createTransactionProviderMetadata = (config) => ({
894
+ [PROVIDER_METADATA_KEY]: { issuer: config.issuer, clientId: config.clientId }
895
+ });
896
+ var snapshotAuthorizationTransaction = (record, state, now) => {
897
+ const { state: storedState, nonce, pkceVerifier, codeChallenge, returnTo, createdAt, expiresAt, metadata } = record;
898
+ if (storedState !== state || typeof nonce !== "string" || typeof pkceVerifier !== "string" || typeof codeChallenge !== "string" || returnTo !== void 0 && typeof returnTo !== "string" || !isUsableEpochMs(createdAt) || !isUsableEpochMs(expiresAt) || !isUsableEpochMs(now) || expiresAt <= now) {
899
+ throw invalidAuthorizationState();
900
+ }
901
+ const match = snapshotRecordBindingMatch(record);
902
+ let fingerprintRecognition;
903
+ try {
904
+ fingerprintRecognition = snapshotFingerprintRecognition(metadata);
905
+ } catch {
906
+ throw invalidAuthorizationState();
907
+ }
908
+ if (fingerprintRecognition !== void 0 && match.browserBindingHash === null) throw invalidAuthorizationState();
909
+ const value = metadata?.[PROVIDER_METADATA_KEY];
910
+ let provider;
911
+ if (value !== void 0) {
912
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalidAuthorizationState();
913
+ const { issuer, clientId } = value;
914
+ if (issuer !== void 0 && typeof issuer !== "string" || clientId !== void 0 && typeof clientId !== "string") {
915
+ throw invalidAuthorizationState();
916
+ }
917
+ provider = Object.freeze({
918
+ ...issuer === void 0 ? {} : { issuer },
919
+ ...clientId === void 0 ? {} : { clientId }
920
+ });
921
+ }
922
+ return Object.freeze({
923
+ state,
924
+ nonce,
925
+ pkceVerifier,
926
+ codeChallenge,
927
+ returnTo,
928
+ createdAt,
929
+ expiresAt,
930
+ ...metadata === void 0 ? {} : {
931
+ metadata: Object.freeze({
932
+ ...withFingerprintRecognitionMetadata(metadata, fingerprintRecognition),
933
+ ...provider === void 0 ? {} : { [PROVIDER_METADATA_KEY]: provider },
934
+ ...fingerprintRecognition === void 0 ? {} : { [FINGERPRINT_RECOGNITION_METADATA_KEY]: fingerprintRecognition }
935
+ })
936
+ },
937
+ ...match.deviceBinding === null ? {} : { deviceBinding: match.deviceBinding },
938
+ ...match.browserBindingHash === null ? {} : { browserBindingHash: match.browserBindingHash },
939
+ match,
940
+ ...provider === void 0 ? {} : { provider },
941
+ ...fingerprintRecognition === void 0 ? {} : { fingerprintRecognition }
942
+ });
943
+ };
944
+ var assertAuthorizationTransactionIdentity = (transaction, config) => {
945
+ if (transaction.provider?.issuer !== void 0 && transaction.provider.issuer !== config.issuer || transaction.provider?.clientId !== void 0 && transaction.provider.clientId !== config.clientId) {
946
+ throw invalidAuthorizationState();
947
+ }
948
+ };
949
+ var assertAuthorizationTransactionMatches = (actual, original) => {
950
+ if (actual.state !== original.state || actual.nonce !== original.nonce || actual.pkceVerifier !== original.pkceVerifier || actual.codeChallenge !== original.codeChallenge || actual.returnTo !== original.returnTo || actual.createdAt !== original.createdAt || actual.expiresAt !== original.expiresAt || actual.provider?.issuer !== original.provider?.issuer || actual.provider?.clientId !== original.provider?.clientId || !fingerprintRecognitionMatches(actual.fingerprintRecognition, original.fingerprintRecognition) || !recordBindingMatches(actual.match, original.match))
951
+ throw invalidAuthorizationState();
952
+ };
953
+
954
+ // src/config.ts
955
+ var DEFAULT_OIDC_SCOPES = "openid email profile";
956
+ var MANUAL_CONFIG_REQUIREMENTS = ["issuer", "authorizationEndpoint", "tokenEndpoint", "jwksUri"];
957
+ var MANUAL_CONFIG_SELECTORS = ["authorizationEndpoint", "tokenEndpoint", "jwksUri", "userInfoEndpoint", "endSessionEndpoint"];
958
+ var normalizeOptionalString = (value) => {
959
+ const trimmed = value?.trim();
960
+ return trimmed ? trimmed : void 0;
961
+ };
962
+ var normalizeHttpUrl = (value, optionName) => {
963
+ if (value === void 0) {
964
+ return void 0;
965
+ }
966
+ let url;
967
+ try {
968
+ url = new URL(value);
969
+ } catch {
970
+ throw new Error(`${optionName} must be an absolute HTTP(S) URL.`);
971
+ }
972
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.origin === "null") {
973
+ throw new Error(`${optionName} must use http or https.`);
974
+ }
975
+ return url.toString();
976
+ };
977
+ var normalizeIssuerIdentifier = (value, optionName) => {
978
+ if (value === void 0) {
979
+ return void 0;
980
+ }
981
+ let url;
982
+ try {
983
+ url = new URL(value);
984
+ } catch {
985
+ throw new Error(`${optionName} must be an absolute HTTP(S) URL.`);
986
+ }
987
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.origin === "null") {
988
+ throw new Error(`${optionName} must use http or https.`);
989
+ }
990
+ if (url.username !== "" || url.password !== "") {
991
+ throw new Error(`${optionName} must not include userinfo.`);
992
+ }
993
+ if (url.search !== "" || url.hash !== "") {
994
+ throw new Error(`${optionName} must not include query or fragment.`);
995
+ }
996
+ return value;
997
+ };
998
+ var withOptionalValue = (target, key, value) => {
999
+ if (value !== void 0) {
1000
+ target[key] = value;
1001
+ }
1002
+ };
1003
+ function resolveOidcVaultConfig(config = {}) {
1004
+ const issuer = normalizeIssuerIdentifier(normalizeOptionalString(config.issuer), "config.issuer");
1005
+ const clientId = normalizeOptionalString(config.clientId);
1006
+ const clientSecret = normalizeOptionalString(config.clientSecret);
1007
+ const scopes = normalizeOptionalString(config.scopes) ?? DEFAULT_OIDC_SCOPES;
1008
+ const hasManualEndpoints = MANUAL_CONFIG_SELECTORS.some((key) => normalizeOptionalString(config[key]));
1009
+ if (!clientId) {
1010
+ throw new Error("OIDC clientId is required.");
1011
+ }
1012
+ if (issuer && !hasManualEndpoints) {
1013
+ const resolved2 = {
1014
+ mode: "issuer",
1015
+ issuer,
1016
+ scopes
1017
+ };
1018
+ withOptionalValue(resolved2, "clientId", clientId);
1019
+ withOptionalValue(resolved2, "clientSecret", clientSecret);
1020
+ return resolved2;
1021
+ }
1022
+ const resolved = {
1023
+ mode: "manual",
1024
+ scopes
1025
+ };
1026
+ withOptionalValue(resolved, "issuer", issuer);
1027
+ withOptionalValue(
1028
+ resolved,
1029
+ "authorizationEndpoint",
1030
+ normalizeHttpUrl(normalizeOptionalString(config.authorizationEndpoint), "config.authorizationEndpoint")
301
1031
  );
302
- };
303
- var getSessionIdFromCookie = (req, options) => {
304
- const cookies = parseCookieHeader(req.headers.cookie);
305
- return cookies[resolveCookieOptions(options).name];
306
- };
1032
+ withOptionalValue(
1033
+ resolved,
1034
+ "tokenEndpoint",
1035
+ normalizeHttpUrl(normalizeOptionalString(config.tokenEndpoint), "config.tokenEndpoint")
1036
+ );
1037
+ withOptionalValue(
1038
+ resolved,
1039
+ "userInfoEndpoint",
1040
+ normalizeHttpUrl(normalizeOptionalString(config.userInfoEndpoint), "config.userInfoEndpoint")
1041
+ );
1042
+ withOptionalValue(resolved, "jwksUri", normalizeHttpUrl(normalizeOptionalString(config.jwksUri), "config.jwksUri"));
1043
+ withOptionalValue(
1044
+ resolved,
1045
+ "endSessionEndpoint",
1046
+ normalizeHttpUrl(normalizeOptionalString(config.endSessionEndpoint), "config.endSessionEndpoint")
1047
+ );
1048
+ withOptionalValue(resolved, "clientId", clientId);
1049
+ withOptionalValue(resolved, "clientSecret", clientSecret);
1050
+ const missing = MANUAL_CONFIG_REQUIREMENTS.filter((key) => !resolved[key]);
1051
+ if (missing.length > 0) {
1052
+ throw new Error(`Missing required OIDC configuration for manual mode: ${missing.join(", ")}`);
1053
+ }
1054
+ return resolved;
1055
+ }
1056
+ function resolveOidcVaultConfigFromEnv(env = process.env) {
1057
+ return resolveOidcVaultConfig({
1058
+ issuer: env.OIDC_ISSUER,
1059
+ authorizationEndpoint: env.OIDC_AUTHORIZATION_ENDPOINT,
1060
+ tokenEndpoint: env.OIDC_TOKEN_ENDPOINT,
1061
+ userInfoEndpoint: env.OIDC_USERINFO_ENDPOINT,
1062
+ jwksUri: env.OIDC_JWKS_URI,
1063
+ endSessionEndpoint: env.OIDC_END_SESSION_ENDPOINT,
1064
+ clientId: env.OIDC_CLIENT_ID,
1065
+ clientSecret: env.OIDC_CLIENT_SECRET,
1066
+ scopes: env.OIDC_SCOPES
1067
+ });
1068
+ }
307
1069
 
308
1070
  // src/utils.ts
309
1071
  var isRecord = (value) => typeof value === "object" && value !== null;
@@ -387,32 +1149,70 @@ var getRequestSourceOrigin = (req) => {
387
1149
  }
388
1150
  };
389
1151
  var assertTrustedOrigin = (req, options, trustedOrigins, action) => {
390
- if (!usesCookieTransport(options)) {
391
- return;
1152
+ captureTrustedOriginGuard(req, options, trustedOrigins, action)();
1153
+ };
1154
+ var captureTrustedOriginGuard = (req, options, trustedOrigins, action) => {
1155
+ const guarded = action === "login" || action === "exchange";
1156
+ if (!guarded && !usesCookieTransport(options)) {
1157
+ return () => {
1158
+ };
392
1159
  }
393
- const origin = getRequestSourceOrigin(req);
394
- if (!origin || !trustedOrigins.has(origin)) {
395
- throw new OidcVaultHttpError(
396
- 403,
397
- "OIDC_VAULT_UNTRUSTED_ORIGIN",
398
- `${action === "refresh" ? "Refresh" : "Logout"} request origin is not trusted.`
399
- );
1160
+ const origin = guarded ? getGuardedRequestSourceOrigin(req) : getRequestSourceOrigin(req);
1161
+ return () => {
1162
+ if (!origin || !trustedOrigins.has(origin)) {
1163
+ throw new OidcVaultHttpError(
1164
+ 403,
1165
+ "OIDC_VAULT_UNTRUSTED_ORIGIN",
1166
+ `${action.charAt(0).toUpperCase()}${action.slice(1)} request origin is not trusted.`
1167
+ );
1168
+ }
1169
+ };
1170
+ };
1171
+ var getGuardedRequestSourceOrigin = (req) => {
1172
+ const origin = readDpopRawHeader(req, "origin");
1173
+ if (origin.count !== 0) return origin.count === 1 && isString(origin.value) ? origin.value : void 0;
1174
+ const referer = readDpopRawHeader(req, "referer");
1175
+ if (referer.count !== 1 || !isString(referer.value)) return void 0;
1176
+ if (!/^https?:\/\//i.test(referer.value) || /[\s\\]/.test(referer.value)) return void 0;
1177
+ try {
1178
+ const url = new URL(referer.value);
1179
+ return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password ? url.origin : void 0;
1180
+ } catch {
1181
+ return void 0;
400
1182
  }
401
1183
  };
402
1184
 
403
1185
  // src/provider-client.ts
404
- import { createRemoteJWKSet } from "jose";
1186
+ import { createRemoteJWKSet, customFetch } from "jose";
405
1187
  var DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 5e3;
406
1188
  var DEFAULT_DISCOVERY_CACHE_TTL_MS = 10 * 6e4;
407
1189
  var DEFAULT_DISCOVERY_CACHE_CAPACITY = 32;
408
1190
  var DEFAULT_JWKS_CACHE_CAPACITY = 32;
409
1191
  var DEFAULT_PROVIDER_ERROR_BODY_LIMIT = 1024;
410
1192
  var DEFAULT_PROVIDER_JSON_BODY_LIMIT = 1024 * 1024;
1193
+ var DEFAULT_JWKS_BODY_LIMIT = 1024 * 1024;
1194
+ var DEFAULT_JWKS_MAX_KEYS = 100;
411
1195
  var discoveryCache = /* @__PURE__ */ new Map();
412
1196
  var jwksCache = /* @__PURE__ */ new Map();
413
- var normalizeIssuerForDiscoveryComparison = (issuer) => issuer.replace(/\/+$/g, "");
414
1197
  var assertDiscoveredIssuerMatchesConfig = (configuredIssuer, discoveredIssuer) => {
415
- if (normalizeIssuerForDiscoveryComparison(configuredIssuer) !== normalizeIssuerForDiscoveryComparison(discoveredIssuer)) {
1198
+ let discoveredUrl;
1199
+ try {
1200
+ discoveredUrl = new URL(discoveredIssuer);
1201
+ } catch {
1202
+ throw new OidcVaultHttpError(
1203
+ 502,
1204
+ "OIDC_VAULT_DISCOVERY_INVALID",
1205
+ "OIDC discovery response issuer must be an absolute HTTP(S) URL."
1206
+ );
1207
+ }
1208
+ if (discoveredUrl.protocol !== "https:" && discoveredUrl.protocol !== "http:" || discoveredUrl.origin === "null" || discoveredUrl.username !== "" || discoveredUrl.password !== "" || discoveredUrl.search !== "" || discoveredUrl.hash !== "") {
1209
+ throw new OidcVaultHttpError(
1210
+ 502,
1211
+ "OIDC_VAULT_DISCOVERY_INVALID",
1212
+ "OIDC discovery response issuer must be an http(s) URL without userinfo, query, or fragment."
1213
+ );
1214
+ }
1215
+ if (configuredIssuer !== discoveredIssuer) {
416
1216
  throw new OidcVaultHttpError(
417
1217
  502,
418
1218
  "OIDC_VAULT_DISCOVERY_INVALID",
@@ -440,6 +1240,20 @@ var validateDiscoveredHttpUrl = (value, fieldName) => {
440
1240
  }
441
1241
  return url.toString();
442
1242
  };
1243
+ var validateOptionalDiscoveredHttpUrl = (value, fieldName) => {
1244
+ if (value === void 0) {
1245
+ return void 0;
1246
+ }
1247
+ return validateDiscoveredHttpUrl(
1248
+ getRequiredString(
1249
+ value,
1250
+ `OIDC discovery response ${fieldName} must be a non-empty string when present.`,
1251
+ "OIDC_VAULT_DISCOVERY_INVALID",
1252
+ 502
1253
+ ),
1254
+ fieldName
1255
+ );
1256
+ };
443
1257
  var buildWellKnownUrl = (issuer) => {
444
1258
  const normalizedIssuer = issuer.endsWith("/") ? issuer : `${issuer}/`;
445
1259
  return new URL(".well-known/openid-configuration", normalizedIssuer);
@@ -464,23 +1278,47 @@ var evictOldestEntry = (cache, capacity) => {
464
1278
  cache.delete(oldestKey);
465
1279
  }
466
1280
  };
1281
+ var isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
1282
+ var createProviderTimeoutError = (errorCode) => new OidcVaultHttpError(502, errorCode, "OIDC provider request timed out.");
1283
+ var createProviderTransportError = (errorCode, cause) => {
1284
+ const error = new OidcVaultHttpError(502, errorCode, "OIDC provider request failed.");
1285
+ Object.defineProperty(error, "cause", { value: cause, configurable: true, writable: true });
1286
+ return error;
1287
+ };
1288
+ var cancelBestEffort = (cancel) => {
1289
+ try {
1290
+ void cancel()?.catch(() => {
1291
+ });
1292
+ } catch {
1293
+ }
1294
+ };
467
1295
  var fetchProvider = async (input, init, errorCode, options) => {
1296
+ const timeoutMs = getProviderRequestTimeoutMs(options);
468
1297
  const controller = new AbortController();
469
- const timeout = setTimeout(() => controller.abort(), getProviderRequestTimeoutMs(options));
1298
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1299
+ const done = () => clearTimeout(timeout);
470
1300
  try {
471
- return await fetch(input, {
1301
+ const response = await fetch(input, {
472
1302
  ...init,
473
1303
  redirect: "manual",
474
1304
  signal: controller.signal
475
1305
  });
1306
+ return { response, signal: controller.signal, done };
476
1307
  } catch (error) {
477
- if (error instanceof Error && error.name === "AbortError") {
478
- throw new OidcVaultHttpError(502, errorCode, "OIDC provider request timed out.");
1308
+ done();
1309
+ if (controller.signal.aborted || isAbortError(error)) {
1310
+ throw createProviderTimeoutError(errorCode);
479
1311
  }
480
- throw error;
481
- } finally {
482
- clearTimeout(timeout);
1312
+ throw createProviderTransportError(errorCode, error);
1313
+ }
1314
+ };
1315
+ var throwBoundedReadTimeout = (errorCode, timeoutAsTimeoutError) => {
1316
+ if (timeoutAsTimeoutError) {
1317
+ const timeoutError = new Error("JSON Web Key Set request timed out.");
1318
+ timeoutError.name = "TimeoutError";
1319
+ throw timeoutError;
483
1320
  }
1321
+ throw createProviderTimeoutError(errorCode);
484
1322
  };
485
1323
  var readBoundedResponseText = async (response, limit, errorCode, options = {}) => {
486
1324
  if (!response.body) {
@@ -488,18 +1326,43 @@ var readBoundedResponseText = async (response, limit, errorCode, options = {}) =
488
1326
  }
489
1327
  const reader = response.body.getReader();
490
1328
  const decoder = new TextDecoder();
1329
+ const { signal } = options;
491
1330
  let size = 0;
492
1331
  let text = "";
1332
+ let onAbort;
1333
+ let abortPromise;
1334
+ if (signal) {
1335
+ if (signal.aborted) {
1336
+ cancelBestEffort(() => reader.cancel());
1337
+ reader.releaseLock();
1338
+ throwBoundedReadTimeout(errorCode, options.timeoutAsTimeoutError === true);
1339
+ }
1340
+ abortPromise = new Promise((_resolve, reject) => {
1341
+ onAbort = () => {
1342
+ reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
1343
+ };
1344
+ signal.addEventListener("abort", onAbort, { once: true });
1345
+ });
1346
+ abortPromise.catch(() => {
1347
+ });
1348
+ }
493
1349
  try {
494
1350
  for (; ; ) {
495
- const result = await reader.read();
1351
+ let result;
1352
+ try {
1353
+ result = abortPromise === void 0 ? await reader.read() : await Promise.race([reader.read(), abortPromise]);
1354
+ } catch (error) {
1355
+ if (signal?.aborted === true || isAbortError(error)) {
1356
+ throwBoundedReadTimeout(errorCode, options.timeoutAsTimeoutError === true);
1357
+ }
1358
+ throw createProviderTransportError(options.transportErrorCode ?? errorCode, error);
1359
+ }
496
1360
  if (result.done) {
497
1361
  break;
498
1362
  }
499
1363
  size += result.value.byteLength;
500
1364
  if (size > limit) {
501
- await reader.cancel();
502
- if (options.truncate) {
1365
+ if (options.truncate === true) {
503
1366
  return `${text.slice(0, limit)}...`;
504
1367
  }
505
1368
  throw new OidcVaultHttpError(502, errorCode, "OIDC provider response is too large.");
@@ -507,123 +1370,219 @@ var readBoundedResponseText = async (response, limit, errorCode, options = {}) =
507
1370
  text += decoder.decode(result.value, { stream: true });
508
1371
  }
509
1372
  } finally {
1373
+ if (signal !== void 0 && onAbort !== void 0) {
1374
+ signal.removeEventListener("abort", onAbort);
1375
+ }
1376
+ cancelBestEffort(() => reader.cancel());
510
1377
  reader.releaseLock();
511
1378
  }
512
1379
  return `${text}${decoder.decode()}`;
513
1380
  };
1381
+ var createBoundedJwksFetch = (timeoutMs) => {
1382
+ void timeoutMs;
1383
+ return async (url, { headers, method, redirect, signal }) => {
1384
+ let response;
1385
+ try {
1386
+ response = await fetch(url, { method, headers, redirect, signal });
1387
+ } catch (error) {
1388
+ if (signal.aborted || isAbortError(error)) {
1389
+ const timeoutError = new Error("JSON Web Key Set request timed out.");
1390
+ timeoutError.name = "TimeoutError";
1391
+ throw timeoutError;
1392
+ }
1393
+ throw createProviderTransportError("OIDC_VAULT_JWKS_FAILED", error);
1394
+ }
1395
+ if (response.status !== 200) {
1396
+ cancelBestEffort(() => response.body?.cancel());
1397
+ return response;
1398
+ }
1399
+ const text = await readBoundedResponseText(response, DEFAULT_JWKS_BODY_LIMIT, "OIDC_VAULT_JWKS_FAILED", {
1400
+ signal,
1401
+ timeoutAsTimeoutError: true
1402
+ });
1403
+ try {
1404
+ const parsed = JSON.parse(text);
1405
+ if (Array.isArray(parsed.keys) && parsed.keys.length > DEFAULT_JWKS_MAX_KEYS) {
1406
+ throw new OidcVaultHttpError(502, "OIDC_VAULT_JWKS_FAILED", "JSON Web Key Set contains too many keys.");
1407
+ }
1408
+ } catch (error) {
1409
+ if (error instanceof OidcVaultHttpError) {
1410
+ throw error;
1411
+ }
1412
+ }
1413
+ return new Response(text, {
1414
+ status: 200,
1415
+ headers: { "content-type": "application/json" }
1416
+ });
1417
+ };
1418
+ };
514
1419
  var resolveJwks = (jwksUri, options = {}) => {
515
- let jwks = jwksCache.get(jwksUri);
1420
+ const timeoutMs = getProviderRequestTimeoutMs(options);
1421
+ const cacheKey = `${jwksUri}\0${timeoutMs}`;
1422
+ let jwks = jwksCache.get(cacheKey);
516
1423
  if (!jwks) {
517
1424
  jwks = createRemoteJWKSet(new URL(jwksUri), {
518
- timeoutDuration: getProviderRequestTimeoutMs(options),
519
- cacheMaxAge: DEFAULT_DISCOVERY_CACHE_TTL_MS
1425
+ timeoutDuration: timeoutMs,
1426
+ cacheMaxAge: DEFAULT_DISCOVERY_CACHE_TTL_MS,
1427
+ [customFetch]: createBoundedJwksFetch(timeoutMs)
520
1428
  });
521
- jwksCache.set(jwksUri, jwks);
1429
+ jwksCache.set(cacheKey, jwks);
522
1430
  evictOldestEntry(jwksCache, DEFAULT_JWKS_CACHE_CAPACITY);
523
1431
  }
524
1432
  return jwks;
525
1433
  };
526
1434
  var formEncodeClientCredential = (value) => new URLSearchParams([[value, ""]]).toString().slice(0, -1);
527
1435
  var toBasicAuthorization = (clientId, clientSecret) => `Basic ${Buffer.from(`${formEncodeClientCredential(clientId)}:${formEncodeClientCredential(clientSecret)}`).toString("base64")}`;
528
- var readJsonResponse = async (response, errorCode, invalidJsonMessage = "OIDC provider returned invalid JSON.") => {
529
- const text = await readBoundedResponseText(response, DEFAULT_PROVIDER_JSON_BODY_LIMIT, errorCode);
1436
+ var isProviderJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1437
+ var readJsonResponse = async (response, errorCode, invalidJsonMessage = "OIDC provider returned invalid JSON.", readOptions = {}) => {
1438
+ const text = await readBoundedResponseText(response, DEFAULT_PROVIDER_JSON_BODY_LIMIT, errorCode, {
1439
+ signal: readOptions.signal,
1440
+ transportErrorCode: readOptions.transportErrorCode
1441
+ });
530
1442
  if (!text) {
531
1443
  return {};
532
1444
  }
1445
+ let parsed;
533
1446
  try {
534
- return JSON.parse(text);
1447
+ parsed = JSON.parse(text);
535
1448
  } catch {
536
- throw new OidcVaultHttpError(
537
- response.ok ? 502 : response.status,
538
- errorCode,
539
- invalidJsonMessage,
540
- invalidJsonMessage
541
- );
1449
+ throw new OidcVaultHttpError(502, errorCode, invalidJsonMessage, invalidJsonMessage);
1450
+ }
1451
+ if (!isProviderJsonObject(parsed)) {
1452
+ throw new OidcVaultHttpError(502, errorCode, invalidJsonMessage, invalidJsonMessage);
1453
+ }
1454
+ return parsed;
1455
+ };
1456
+ var drainProviderErrorBody = async (response, signal, errorCode) => {
1457
+ try {
1458
+ await readBoundedResponseText(response, DEFAULT_PROVIDER_JSON_BODY_LIMIT, errorCode, { signal });
1459
+ } catch (error) {
1460
+ if (error instanceof OidcVaultHttpError) {
1461
+ throw error;
1462
+ }
1463
+ throw error;
542
1464
  }
543
1465
  };
544
1466
  async function discoverIssuerMetadata(issuer, options) {
1467
+ const callerTimeoutMs = getProviderRequestTimeoutMs(options);
1468
+ const policyKey = `${issuer}\0${callerTimeoutMs}`;
545
1469
  const now = Date.now();
546
- const cached = discoveryCache.get(issuer);
547
- let discoveryPromise = cached && cached.expiresAt > now ? cached.promise : void 0;
548
- if (cached && !discoveryPromise) {
549
- discoveryCache.delete(issuer);
550
- }
551
- if (!discoveryPromise) {
1470
+ const policyCached = discoveryCache.get(policyKey);
1471
+ let discoveryPromise = policyCached !== void 0 && policyCached.expiresAt > now ? policyCached.promise : void 0;
1472
+ if (policyCached !== void 0 && discoveryPromise === void 0) {
1473
+ discoveryCache.delete(policyKey);
1474
+ }
1475
+ if (discoveryPromise === void 0) {
1476
+ const sharedCached = discoveryCache.get(issuer);
1477
+ const sharedPromise = sharedCached !== void 0 && sharedCached.expiresAt > now ? sharedCached.promise : void 0;
1478
+ if (sharedCached !== void 0 && sharedPromise === void 0) {
1479
+ discoveryCache.delete(issuer);
1480
+ }
1481
+ if (sharedPromise !== void 0) {
1482
+ return sharedPromise;
1483
+ }
552
1484
  discoveryPromise = (async () => {
553
1485
  const discoveryUrl = buildWellKnownUrl(issuer);
554
- const response = await fetchProvider(discoveryUrl, void 0, "OIDC_VAULT_DISCOVERY_FAILED", options);
555
- if (!response.ok) {
556
- const errorText = await readBoundedResponseText(
1486
+ const { response, signal, done } = await fetchProvider(
1487
+ discoveryUrl,
1488
+ void 0,
1489
+ "OIDC_VAULT_DISCOVERY_FAILED",
1490
+ options
1491
+ );
1492
+ try {
1493
+ if (!response.ok) {
1494
+ const errorText = await readBoundedResponseText(
1495
+ response,
1496
+ DEFAULT_PROVIDER_ERROR_BODY_LIMIT,
1497
+ "OIDC_VAULT_DISCOVERY_FAILED",
1498
+ {
1499
+ truncate: true,
1500
+ signal
1501
+ }
1502
+ );
1503
+ throw new OidcVaultHttpError(
1504
+ 502,
1505
+ "OIDC_VAULT_DISCOVERY_FAILED",
1506
+ errorText || `OIDC discovery failed with status ${response.status}.`,
1507
+ "OIDC discovery failed."
1508
+ );
1509
+ }
1510
+ const discovered = await readJsonResponse(
557
1511
  response,
558
- DEFAULT_PROVIDER_ERROR_BODY_LIMIT,
559
- "OIDC_VAULT_DISCOVERY_FAILED",
560
- {
561
- truncate: true
562
- }
1512
+ "OIDC_VAULT_DISCOVERY_INVALID",
1513
+ "OIDC discovery response is malformed JSON.",
1514
+ { signal, transportErrorCode: "OIDC_VAULT_DISCOVERY_FAILED" }
563
1515
  );
564
- throw new OidcVaultHttpError(
565
- 502,
566
- "OIDC_VAULT_DISCOVERY_FAILED",
567
- errorText || `OIDC discovery failed with status ${response.status}.`,
568
- "OIDC discovery failed."
1516
+ const authorizationEndpoint = validateDiscoveredHttpUrl(
1517
+ getRequiredString(
1518
+ discovered.authorization_endpoint,
1519
+ "OIDC discovery response is missing authorization_endpoint.",
1520
+ "OIDC_VAULT_DISCOVERY_INVALID",
1521
+ 502
1522
+ ),
1523
+ "authorization_endpoint"
569
1524
  );
570
- }
571
- const discovered = await readJsonResponse(
572
- response,
573
- "OIDC_VAULT_DISCOVERY_INVALID",
574
- "OIDC discovery response is malformed JSON."
575
- );
576
- const authorizationEndpoint = validateDiscoveredHttpUrl(
577
- getRequiredString(
578
- discovered.authorization_endpoint,
579
- "OIDC discovery response is missing authorization_endpoint.",
580
- "OIDC_VAULT_DISCOVERY_INVALID",
581
- 502
582
- ),
583
- "authorization_endpoint"
584
- );
585
- const tokenEndpoint = validateDiscoveredHttpUrl(
586
- getRequiredString(
587
- discovered.token_endpoint,
588
- "OIDC discovery response is missing token_endpoint.",
589
- "OIDC_VAULT_DISCOVERY_INVALID",
590
- 502
591
- ),
592
- "token_endpoint"
593
- );
594
- const jwksUri = validateDiscoveredHttpUrl(
595
- getRequiredString(
596
- discovered.jwks_uri,
597
- "OIDC discovery response is missing jwks_uri.",
1525
+ const tokenEndpoint = validateDiscoveredHttpUrl(
1526
+ getRequiredString(
1527
+ discovered.token_endpoint,
1528
+ "OIDC discovery response is missing token_endpoint.",
1529
+ "OIDC_VAULT_DISCOVERY_INVALID",
1530
+ 502
1531
+ ),
1532
+ "token_endpoint"
1533
+ );
1534
+ const jwksUri = validateDiscoveredHttpUrl(
1535
+ getRequiredString(
1536
+ discovered.jwks_uri,
1537
+ "OIDC discovery response is missing jwks_uri.",
1538
+ "OIDC_VAULT_DISCOVERY_INVALID",
1539
+ 502
1540
+ ),
1541
+ "jwks_uri"
1542
+ );
1543
+ const discoveredIssuer = getRequiredString(
1544
+ discovered.issuer,
1545
+ "OIDC discovery response is missing issuer.",
598
1546
  "OIDC_VAULT_DISCOVERY_INVALID",
599
1547
  502
600
- ),
601
- "jwks_uri"
602
- );
603
- const discoveredIssuer = getRequiredString(
604
- discovered.issuer,
605
- "OIDC discovery response is missing issuer.",
606
- "OIDC_VAULT_DISCOVERY_INVALID",
607
- 502
608
- );
609
- assertDiscoveredIssuerMatchesConfig(issuer, discoveredIssuer);
610
- return {
611
- issuer: discoveredIssuer,
612
- authorizationEndpoint,
613
- tokenEndpoint,
614
- jwksUri,
615
- userInfoEndpoint: typeof discovered.userinfo_endpoint === "string" ? validateDiscoveredHttpUrl(discovered.userinfo_endpoint, "userinfo_endpoint") : void 0,
616
- endSessionEndpoint: typeof discovered.end_session_endpoint === "string" ? validateDiscoveredHttpUrl(discovered.end_session_endpoint, "end_session_endpoint") : void 0
617
- };
1548
+ );
1549
+ assertDiscoveredIssuerMatchesConfig(issuer, discoveredIssuer);
1550
+ return {
1551
+ issuer: discoveredIssuer,
1552
+ authorizationEndpoint,
1553
+ tokenEndpoint,
1554
+ jwksUri,
1555
+ userInfoEndpoint: validateOptionalDiscoveredHttpUrl(discovered.userinfo_endpoint, "userinfo_endpoint"),
1556
+ endSessionEndpoint: validateOptionalDiscoveredHttpUrl(
1557
+ discovered.end_session_endpoint,
1558
+ "end_session_endpoint"
1559
+ )
1560
+ };
1561
+ } finally {
1562
+ done();
1563
+ cancelBestEffort(() => response.body?.cancel());
1564
+ }
618
1565
  })();
619
- discoveryCache.set(issuer, {
1566
+ discoveryPromise.then(
1567
+ () => {
1568
+ const fetchedAt = Date.now();
1569
+ discoveryCache.set(issuer, {
1570
+ expiresAt: fetchedAt + DEFAULT_DISCOVERY_CACHE_TTL_MS,
1571
+ promise: discoveryPromise
1572
+ });
1573
+ evictOldestEntry(discoveryCache, DEFAULT_DISCOVERY_CACHE_CAPACITY);
1574
+ },
1575
+ () => {
1576
+ }
1577
+ );
1578
+ discoveryCache.set(policyKey, {
620
1579
  expiresAt: now + DEFAULT_DISCOVERY_CACHE_TTL_MS,
621
1580
  promise: discoveryPromise
622
1581
  });
623
1582
  evictOldestEntry(discoveryCache, DEFAULT_DISCOVERY_CACHE_CAPACITY);
624
1583
  discoveryPromise.catch(() => {
625
- if (discoveryCache.get(issuer)?.promise === discoveryPromise) {
626
- discoveryCache.delete(issuer);
1584
+ if (discoveryCache.get(policyKey)?.promise === discoveryPromise) {
1585
+ discoveryCache.delete(policyKey);
627
1586
  }
628
1587
  });
629
1588
  }
@@ -674,7 +1633,7 @@ async function requestToken(metadata, params, options = {}) {
674
1633
  if (metadata.clientSecret) {
675
1634
  headers.authorization = toBasicAuthorization(clientId, metadata.clientSecret);
676
1635
  }
677
- const response = await fetchProvider(
1636
+ const { response, signal, done } = await fetchProvider(
678
1637
  metadata.tokenEndpoint,
679
1638
  {
680
1639
  method: "POST",
@@ -684,17 +1643,33 @@ async function requestToken(metadata, params, options = {}) {
684
1643
  "OIDC_VAULT_TOKEN_REQUEST_FAILED",
685
1644
  options
686
1645
  );
687
- const json = await readJsonResponse(response, "OIDC_VAULT_TOKEN_REQUEST_FAILED");
688
- if (!response.ok) {
1646
+ try {
1647
+ if (!response.ok) {
1648
+ await drainProviderErrorBody(response, signal, "OIDC_VAULT_TOKEN_REQUEST_FAILED");
1649
+ throw new OidcVaultHttpError(
1650
+ 502,
1651
+ "OIDC_VAULT_TOKEN_REQUEST_FAILED",
1652
+ "OIDC token request failed.",
1653
+ "OIDC token request failed."
1654
+ );
1655
+ }
1656
+ const json = await readJsonResponse(response, "OIDC_VAULT_TOKEN_REQUEST_FAILED", void 0, { signal });
1657
+ return json;
1658
+ } finally {
1659
+ done();
1660
+ cancelBestEffort(() => response.body?.cancel());
1661
+ }
1662
+ }
1663
+ var assertPresentTokenStringField = (tokenResponse, field) => {
1664
+ const value = tokenResponse[field];
1665
+ if (value !== void 0 && (typeof value !== "string" || value.length === 0)) {
689
1666
  throw new OidcVaultHttpError(
690
1667
  502,
691
- "OIDC_VAULT_TOKEN_REQUEST_FAILED",
692
- typeof json.error_description === "string" ? json.error_description : `OIDC token request failed with status ${response.status}.`,
693
- "OIDC token request failed."
1668
+ "OIDC_VAULT_INVALID_TOKEN_RESPONSE",
1669
+ `OIDC token response ${field} must be a non-empty string when present.`
694
1670
  );
695
1671
  }
696
- return json;
697
- }
1672
+ };
698
1673
  function validateTokenResponse(tokenResponse) {
699
1674
  if (typeof tokenResponse.token_type !== "string" || tokenResponse.token_type.toLowerCase() !== "bearer") {
700
1675
  throw new OidcVaultHttpError(
@@ -710,12 +1685,38 @@ function validateTokenResponse(tokenResponse) {
710
1685
  "OIDC_VAULT_INVALID_TOKEN_RESPONSE"
711
1686
  );
712
1687
  }
1688
+ assertPresentTokenStringField(tokenResponse, "access_token");
1689
+ assertPresentTokenStringField(tokenResponse, "id_token");
1690
+ assertPresentTokenStringField(tokenResponse, "refresh_token");
1691
+ if (tokenResponse.scope !== void 0 && typeof tokenResponse.scope !== "string") {
1692
+ throw new OidcVaultHttpError(
1693
+ 502,
1694
+ "OIDC_VAULT_INVALID_TOKEN_RESPONSE",
1695
+ "OIDC token response scope must be a string when present."
1696
+ );
1697
+ }
1698
+ }
1699
+ function validateCallbackTokenResponse(tokenResponse) {
1700
+ validateTokenResponse(tokenResponse);
1701
+ if (typeof tokenResponse.id_token !== "string" || tokenResponse.id_token.length === 0) {
1702
+ throw new OidcVaultHttpError(502, "OIDC_VAULT_MISSING_ID_TOKEN", "OIDC token response is missing id_token.");
1703
+ }
1704
+ if (typeof tokenResponse.refresh_token !== "string" || tokenResponse.refresh_token.length === 0) {
1705
+ throw new OidcVaultHttpError(
1706
+ 502,
1707
+ "OIDC_VAULT_MISSING_REFRESH_TOKEN",
1708
+ "OIDC token response is missing refresh_token."
1709
+ );
1710
+ }
1711
+ }
1712
+ function validateRefreshTokenResponse(tokenResponse) {
1713
+ validateTokenResponse(tokenResponse);
713
1714
  }
714
1715
  async function fetchUserInfo(metadata, accessToken, options = {}) {
715
1716
  if (!metadata.userInfoEndpoint) {
716
1717
  return {};
717
1718
  }
718
- const response = await fetchProvider(
1719
+ const { response, signal, done } = await fetchProvider(
719
1720
  metadata.userInfoEndpoint,
720
1721
  {
721
1722
  headers: {
@@ -725,20 +1726,261 @@ async function fetchUserInfo(metadata, accessToken, options = {}) {
725
1726
  "OIDC_VAULT_USERINFO_FAILED",
726
1727
  options
727
1728
  );
728
- const json = await readJsonResponse(response, "OIDC_VAULT_USERINFO_FAILED");
729
- if (!response.ok) {
730
- throw new OidcVaultHttpError(
731
- 502,
732
- "OIDC_VAULT_USERINFO_FAILED",
733
- `OIDC userinfo request failed with status ${response.status}.`,
734
- "OIDC userinfo request failed."
735
- );
1729
+ try {
1730
+ if (!response.ok) {
1731
+ await drainProviderErrorBody(response, signal, "OIDC_VAULT_USERINFO_FAILED");
1732
+ throw new OidcVaultHttpError(
1733
+ 502,
1734
+ "OIDC_VAULT_USERINFO_FAILED",
1735
+ "OIDC userinfo request failed.",
1736
+ "OIDC userinfo request failed."
1737
+ );
1738
+ }
1739
+ const json = await readJsonResponse(response, "OIDC_VAULT_USERINFO_FAILED", void 0, { signal });
1740
+ return json;
1741
+ } finally {
1742
+ done();
1743
+ cancelBestEffort(() => response.body?.cancel());
736
1744
  }
737
- return json;
738
1745
  }
739
1746
 
1747
+ // src/token-issuance.ts
1748
+ import { decodeJwt, decodeProtectedHeader } from "jose";
1749
+
1750
+ // src/vault-session.ts
1751
+ var invalidVaultSession = () => new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
1752
+ var invalidExchangeCode = () => new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_EXCHANGE_CODE", "Exchange code is invalid or expired.");
1753
+ var isPlainObject2 = (value) => typeof value === "object" && value !== null && [Object.prototype, null].includes(Object.getPrototypeOf(value));
1754
+ var copyPortableData = (value, omitRecognition = false) => {
1755
+ const copies = /* @__PURE__ */ new Map();
1756
+ const pending = [];
1757
+ const copy = (item) => {
1758
+ if (!Array.isArray(item) && !isPlainObject2(item)) return item;
1759
+ const existing = copies.get(item);
1760
+ if (existing) return existing;
1761
+ const target = Array.isArray(item) ? new Array(item.length) : Object.create(Object.getPrototypeOf(item));
1762
+ copies.set(item, target);
1763
+ pending.push({ source: item, target });
1764
+ return target;
1765
+ };
1766
+ const result = copy(value);
1767
+ for (let index = 0; index < pending.length; index++) {
1768
+ const { source, target } = pending[index];
1769
+ for (const key of Object.keys(source)) {
1770
+ if (omitRecognition && key === FINGERPRINT_RECOGNITION_METADATA_KEY) continue;
1771
+ Object.defineProperty(target, key, {
1772
+ value: copy(Reflect.get(source, key)),
1773
+ enumerable: true,
1774
+ configurable: true,
1775
+ writable: true
1776
+ });
1777
+ }
1778
+ }
1779
+ return result;
1780
+ };
1781
+ var copyVaultSession = (session, omitRecognition = false) => copyPortableData(session, omitRecognition);
1782
+ var copyVaultUserProfile = (user) => copyPortableData(user, true);
1783
+ var snapshotProvider = (value) => {
1784
+ if (value === void 0) return void 0;
1785
+ if (!isPlainObject2(value)) throw invalidVaultSession();
1786
+ const { issuer, clientId } = value;
1787
+ if (issuer !== void 0 && typeof issuer !== "string" || clientId !== void 0 && typeof clientId !== "string") {
1788
+ throw invalidVaultSession();
1789
+ }
1790
+ return Object.freeze({
1791
+ ...issuer === void 0 ? {} : { issuer },
1792
+ ...clientId === void 0 ? {} : { clientId }
1793
+ });
1794
+ };
1795
+ var assertVaultSessionIdentity = (session, config) => {
1796
+ if (session.provider?.issuer !== void 0 && session.provider.issuer !== config.issuer || session.provider?.clientId !== void 0 && session.provider.clientId !== config.clientId)
1797
+ throw invalidVaultSession();
1798
+ };
1799
+ var snapshotVaultSession = (record, expectedSessionId, now) => {
1800
+ const {
1801
+ sessionId,
1802
+ logicalSessionId,
1803
+ subject,
1804
+ providerSessionId,
1805
+ provider: rawProvider,
1806
+ deviceBinding: rawBinding,
1807
+ refreshToken,
1808
+ idToken,
1809
+ accessToken,
1810
+ scope,
1811
+ expiresAt,
1812
+ createdAt,
1813
+ updatedAt,
1814
+ user,
1815
+ metadata
1816
+ } = record;
1817
+ if (sessionId !== expectedSessionId || typeof sessionId !== "string" || !sessionId || typeof subject !== "string" || !subject || logicalSessionId !== void 0 && (typeof logicalSessionId !== "string" || !logicalSessionId) || providerSessionId !== void 0 && typeof providerSessionId !== "string" || typeof refreshToken !== "string" || typeof idToken !== "string" || accessToken !== void 0 && typeof accessToken !== "string" || scope !== void 0 && typeof scope !== "string" || !isUsableEpochMs(createdAt) || !isUsableEpochMs(updatedAt) || !isUsableEpochMs(now) || expiresAt !== void 0 && (!isUsableEpochMs(expiresAt) || expiresAt <= now))
1818
+ throw invalidVaultSession();
1819
+ const provider = snapshotProvider(rawProvider);
1820
+ const binding = snapshotDpopBinding(rawBinding);
1821
+ if (binding !== void 0 && Object.keys(rawBinding).length !== 2) throw invalidVaultSession();
1822
+ return Object.freeze({
1823
+ sessionId,
1824
+ logicalSessionId,
1825
+ subject,
1826
+ providerSessionId,
1827
+ provider,
1828
+ ...binding === void 0 ? {} : { deviceBinding: binding },
1829
+ refreshToken,
1830
+ idToken,
1831
+ accessToken,
1832
+ scope,
1833
+ expiresAt,
1834
+ createdAt,
1835
+ updatedAt,
1836
+ ...user === void 0 ? {} : { user: copyPortableData(user) },
1837
+ ...metadata === void 0 ? {} : { metadata: copyPortableData(metadata) }
1838
+ });
1839
+ };
1840
+ var assertVaultSessionMatches = (actual, original) => {
1841
+ if (actual.sessionId !== original.sessionId || actual.logicalSessionId !== original.logicalSessionId || actual.subject !== original.subject || actual.providerSessionId !== original.providerSessionId || actual.provider?.issuer !== original.provider?.issuer || actual.provider?.clientId !== original.provider?.clientId || actual.deviceBinding?.jkt !== original.deviceBinding?.jkt || actual.expiresAt !== original.expiresAt || actual.createdAt !== original.createdAt || actual.updatedAt !== original.updatedAt || actual.refreshToken !== original.refreshToken || actual.idToken !== original.idToken || actual.accessToken !== original.accessToken || actual.scope !== original.scope)
1842
+ throw invalidVaultSession();
1843
+ };
1844
+ var snapshotVaultExchangeCode = (record, expectedCode, now) => {
1845
+ const { code, sessionId, returnTo, createdAt, expiresAt } = record;
1846
+ if (code !== expectedCode || typeof sessionId !== "string" || !sessionId || returnTo !== void 0 && typeof returnTo !== "string" || !isUsableEpochMs(createdAt) || !isUsableEpochMs(expiresAt) || !isUsableEpochMs(now) || expiresAt <= now)
1847
+ throw invalidExchangeCode();
1848
+ const match = snapshotRecordBindingMatch(record);
1849
+ return Object.freeze({
1850
+ code,
1851
+ sessionId,
1852
+ returnTo,
1853
+ createdAt,
1854
+ expiresAt,
1855
+ match,
1856
+ ...match.deviceBinding === null ? {} : { deviceBinding: match.deviceBinding },
1857
+ ...match.browserBindingHash === null ? {} : { browserBindingHash: match.browserBindingHash }
1858
+ });
1859
+ };
1860
+ var assertVaultExchangeCodeMatches = (actual, original) => {
1861
+ if (actual.code !== original.code || actual.sessionId !== original.sessionId || actual.returnTo !== original.returnTo || actual.createdAt !== original.createdAt || actual.expiresAt !== original.expiresAt || !recordBindingMatches(actual.match, original.match))
1862
+ throw invalidExchangeCode();
1863
+ };
1864
+ var assertVaultExchangeSessionBinding = (code, session) => {
1865
+ if (code.sessionId !== session.sessionId || code.deviceBinding?.jkt !== session.deviceBinding?.jkt)
1866
+ throw invalidVaultSession();
1867
+ };
1868
+ var snapshotVaultRevocationContext = (context) => {
1869
+ const { logicalSessionId, provider, deviceBinding } = context;
1870
+ if (typeof logicalSessionId !== "string" || !logicalSessionId) throw invalidVaultSession();
1871
+ const identity = snapshotProvider(provider);
1872
+ const binding = snapshotDpopBinding(deviceBinding);
1873
+ if (binding !== void 0 && Object.keys(deviceBinding).length !== 2) throw invalidVaultSession();
1874
+ return Object.freeze({
1875
+ logicalSessionId,
1876
+ ...identity === void 0 ? {} : { provider: identity },
1877
+ ...binding === void 0 ? {} : { deviceBinding: binding }
1878
+ });
1879
+ };
1880
+ var assertVaultRevocationContextMatches = (actual, original) => {
1881
+ if (actual.logicalSessionId !== original.logicalSessionId || actual.provider?.issuer !== original.provider?.issuer || actual.provider?.clientId !== original.provider?.clientId || actual.deviceBinding?.jkt !== original.deviceBinding?.jkt) {
1882
+ throw invalidVaultSession();
1883
+ }
1884
+ };
1885
+
1886
+ // src/token-issuance.ts
1887
+ var createExchangeResponse = (options, session, issuedToken) => ({
1888
+ sessionId: usesCookieTransport(options) ? void 0 : session.sessionId,
1889
+ user: copyVaultUserProfile(session.user),
1890
+ accessToken: issuedToken.accessToken,
1891
+ expiresIn: issuedToken.expiresIn,
1892
+ tokenType: issuedToken.tokenType
1893
+ });
1894
+ var isCompactSignedJwt = (token) => {
1895
+ const parts = token.split(".");
1896
+ return parts.length === 3 && parts.every(
1897
+ (part) => /^[A-Za-z0-9_-]+$/.test(part) ? Buffer.from(part, "base64url").toString("base64url") === part : false
1898
+ );
1899
+ };
1900
+ var assertIssuedTokenConfirmation = (accessToken, jkt) => {
1901
+ if (!isCompactSignedJwt(accessToken)) {
1902
+ throw new TypeError("tokenIssuer.issue accessToken must be a compact signed JWT for bound issuance.");
1903
+ }
1904
+ let header;
1905
+ let claims;
1906
+ try {
1907
+ header = decodeProtectedHeader(accessToken);
1908
+ claims = decodeJwt(accessToken);
1909
+ } catch {
1910
+ throw new TypeError("tokenIssuer.issue accessToken must be a compact signed JWT for bound issuance.");
1911
+ }
1912
+ if (typeof header.alg !== "string" || header.alg.length === 0 || header.alg === "none" || header.b64 === false) {
1913
+ throw new TypeError("tokenIssuer.issue accessToken must use a signed JWT algorithm for bound issuance.");
1914
+ }
1915
+ const cnf = claims.cnf;
1916
+ if (typeof cnf !== "object" || cnf === null || Array.isArray(cnf) || cnf.jkt !== jkt) {
1917
+ throw new TypeError("tokenIssuer.issue accessToken cnf.jkt must match the verified device binding.");
1918
+ }
1919
+ };
1920
+ var withIssuedToken = async (req, res, options, session, verifiedBinding) => {
1921
+ const binding = resolveVerifiedDeviceBinding(options.deviceBinding, session.deviceBinding, verifiedBinding);
1922
+ if (!options.tokenIssuer) return {};
1923
+ const logicalSessionId = session.logicalSessionId ?? session.sessionId;
1924
+ try {
1925
+ const issuerSession = withSessionDeviceBinding(copyVaultSession(session, true), binding);
1926
+ const result = await options.tokenIssuer.issue({
1927
+ req,
1928
+ res,
1929
+ session: issuerSession,
1930
+ ...binding === void 0 ? {} : { deviceBinding: Object.freeze({ ...binding }) }
1931
+ });
1932
+ if (typeof result !== "object" || result === null || Array.isArray(result)) {
1933
+ throw new TypeError("tokenIssuer.issue must return a token result object.");
1934
+ }
1935
+ const { accessToken, expiresIn, tokenType } = result;
1936
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
1937
+ throw new TypeError("tokenIssuer.issue accessToken must be a nonempty string.");
1938
+ }
1939
+ if (typeof expiresIn !== "number" || !Number.isSafeInteger(expiresIn) || expiresIn < 0) {
1940
+ throw new TypeError("tokenIssuer.issue expiresIn must be a finite nonnegative safe integer.");
1941
+ }
1942
+ if (tokenType !== void 0 && tokenType !== "Bearer" && tokenType !== "DPoP") {
1943
+ throw new TypeError("tokenIssuer.issue tokenType must be the exact Bearer or DPoP literal when provided.");
1944
+ }
1945
+ if (binding === void 0) {
1946
+ if (tokenType !== void 0 && tokenType !== "Bearer") {
1947
+ throw new TypeError("tokenIssuer.issue tokenType must be Bearer when provided for unbound issuance.");
1948
+ }
1949
+ } else {
1950
+ if (tokenType !== "DPoP") throw new TypeError("tokenIssuer.issue tokenType must be DPoP for bound issuance.");
1951
+ assertIssuedTokenConfirmation(accessToken, binding.jkt);
1952
+ }
1953
+ return { accessToken, expiresIn, ...tokenType === void 0 ? {} : { tokenType } };
1954
+ } catch (error) {
1955
+ await options.storeProvider.deleteSessionsByLogicalSessionId({ logicalSessionId });
1956
+ if (usesCookieTransport(options)) clearSessionCookie(res, options);
1957
+ throw error;
1958
+ }
1959
+ };
1960
+
740
1961
  // src/token-validation.ts
741
1962
  import { jwtVerify } from "jose";
1963
+
1964
+ // src/access-token-confirmation.ts
1965
+ var invalidAccessToken = (diagnostic) => new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_ACCESS_TOKEN", diagnostic, "Access token validation failed.");
1966
+ var snapshotJkt = (value) => {
1967
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1968
+ throw invalidAccessToken("Access token confirmation must be a plain jkt object.");
1969
+ }
1970
+ const prototype = Object.getPrototypeOf(value);
1971
+ const keys = Reflect.ownKeys(value);
1972
+ if (prototype !== Object.prototype && prototype !== null || keys.length !== 1 || keys[0] !== "jkt") {
1973
+ throw invalidAccessToken("Access token confirmation contains unsupported fields.");
1974
+ }
1975
+ const { jkt } = value;
1976
+ if (!isCanonicalDpopJkt(jkt))
1977
+ throw invalidAccessToken("Access token confirmation is not a canonical SHA-256 thumbprint.");
1978
+ return Object.freeze({ jkt });
1979
+ };
1980
+ var snapshotVerifiedJwtConfirmation = (claims) => Object.hasOwn(claims, "cnf") ? snapshotJkt(claims.cnf) : null;
1981
+ var snapshotAccessTokenConfirmation = (value) => value === null ? null : snapshotJkt(value);
1982
+
1983
+ // src/token-validation.ts
742
1984
  var BACKCHANNEL_LOGOUT_EVENT_CLAIM = "http://schemas.openid.net/event/backchannel-logout";
743
1985
  var validateBackchannelLogoutTokenTyp = (typ) => {
744
1986
  if (typ !== void 0 && (typeof typ !== "string" || typ.toLowerCase() !== "logout+jwt")) {
@@ -764,6 +2006,9 @@ var defaultJwtClaimsMapper = (claims) => {
764
2006
  };
765
2007
  };
766
2008
  var assertUserInfoSubject = (userInfo, subject) => {
2009
+ if (typeof userInfo !== "object" || userInfo === null || Array.isArray(userInfo)) {
2010
+ throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_USERINFO", "OIDC userinfo response is invalid.");
2011
+ }
767
2012
  if (!isString(userInfo.sub)) {
768
2013
  throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_USERINFO", "OIDC userinfo response is missing sub.");
769
2014
  }
@@ -776,6 +2021,12 @@ var mergeUserProfile = (sub, idTokenClaims, userInfo) => ({
776
2021
  ...userInfo ?? {},
777
2022
  sub
778
2023
  });
2024
+ var composeRefreshedUserProfile = (sub, currentUser, freshIdClaims, freshUserInfo) => {
2025
+ if (freshIdClaims === void 0) {
2026
+ return mergeUserProfile(sub, { ...currentUser ?? { sub } }, freshUserInfo);
2027
+ }
2028
+ return mergeUserProfile(sub, { ...freshIdClaims }, freshUserInfo);
2029
+ };
779
2030
  async function verifyIdToken(metadata, idToken, nonce, options = {}) {
780
2031
  if (!metadata.jwksUri) {
781
2032
  throw new OidcVaultHttpError(500, "OIDC_VAULT_MISSING_JWKS_URI", "OIDC jwksUri is required to validate id_token.");
@@ -877,17 +2128,35 @@ async function verifyBackchannelLogoutToken(metadata, logoutToken, options = {})
877
2128
  return claims;
878
2129
  }
879
2130
  function createOidcVaultJwtAccessTokenValidator(options) {
880
- return {
2131
+ const {
2132
+ key: configuredKey,
2133
+ issuer,
2134
+ audience: configuredAudience,
2135
+ algorithms: configuredAlgorithms,
2136
+ mapClaims
2137
+ } = options;
2138
+ const key = configuredKey instanceof Uint8Array ? Uint8Array.from(configuredKey) : "kty" in configuredKey ? { ...configuredKey, ...configuredKey.key_ops ? { key_ops: [...configuredKey.key_ops] } : {} } : configuredKey;
2139
+ const audience = Array.isArray(configuredAudience) ? [...configuredAudience] : configuredAudience;
2140
+ const algorithms = configuredAlgorithms === void 0 ? void 0 : [...configuredAlgorithms];
2141
+ const validateJwt = async (token) => {
2142
+ const result = await jwtVerify(token, key, { issuer, audience, algorithms });
2143
+ const claims = result.payload;
2144
+ const confirmation = snapshotVerifiedJwtConfirmation(claims);
2145
+ const mapped = mapClaims ? mapClaims(claims) : defaultJwtClaimsMapper(claims);
2146
+ const { subject, sessionId, scope, claims: mappedClaims } = mapped;
2147
+ return { subject, sessionId, scope, claims: mappedClaims, confirmation };
2148
+ };
2149
+ return Object.freeze({
881
2150
  async validate(token) {
882
- const result = await jwtVerify(token, options.key, {
883
- issuer: options.issuer,
884
- audience: options.audience,
885
- algorithms: options.algorithms
886
- });
887
- const claims = result.payload;
888
- return options.mapClaims ? options.mapClaims(claims) : defaultJwtClaimsMapper(claims);
2151
+ const { confirmation, ...mapped } = await validateJwt(token);
2152
+ return confirmation === null ? mapped : { ...mapped, confirmation };
2153
+ },
2154
+ async validateWithRequest({
2155
+ token
2156
+ }) {
2157
+ return validateJwt(token);
889
2158
  }
890
- };
2159
+ });
891
2160
  }
892
2161
 
893
2162
  // src/types.ts
@@ -897,6 +2166,235 @@ var OidcVaultStoreConflictError = class extends Error {
897
2166
  this.name = "OidcVaultStoreConflictError";
898
2167
  }
899
2168
  };
2169
+ var OidcVaultDpopReplayCapacityError = class extends Error {
2170
+ constructor(message = "OIDC vault DPoP replay store capacity is exhausted.") {
2171
+ super(message);
2172
+ this.name = "OidcVaultDpopReplayCapacityError";
2173
+ }
2174
+ };
2175
+
2176
+ // src/dpop-replay.ts
2177
+ import { createHash as createHash5 } from "crypto";
2178
+ var isPrintableIdentifier = (value) => typeof value === "string" && /^[\x20-\x7e]{1,128}$/.test(value);
2179
+ var normalizeNamespaceBasePath = (origin, value) => {
2180
+ if (value !== void 0 && typeof value !== "string")
2181
+ throw new TypeError("DPoP vault basePath must be a static path.");
2182
+ const mounted = !value || value === "/" ? DEFAULT_OIDC_VAULT_BASE_PATH : `/${value.replace(/^\/+|\/+$/g, "")}`;
2183
+ if (/[\s\\?#]/.test(mounted) || /%(?![0-9a-f]{2})/i.test(mounted)) {
2184
+ throw new TypeError("DPoP vault basePath must be a static path without query, fragment or malformed escapes.");
2185
+ }
2186
+ const path = mounted.replace(/%([0-9a-f]{2})/gi, (_, hex) => {
2187
+ const character = String.fromCharCode(Number.parseInt(hex, 16));
2188
+ return /^[A-Za-z0-9._~-]$/.test(character) ? character : `%${hex.toUpperCase()}`;
2189
+ });
2190
+ const normalized = new URL(`${origin}${path}`).pathname;
2191
+ return normalized.replace(/\/+$/g, "") || "/";
2192
+ };
2193
+ var resolveDpopEffectiveNamespace = (space) => {
2194
+ if (space?.type === "vault") {
2195
+ const { backendOrigin, basePath, issuer, clientId } = space;
2196
+ const origin = resolveDeviceBindingOrigin(backendOrigin);
2197
+ if (typeof issuer !== "string" || issuer.length === 0 || typeof clientId !== "string" || clientId.length === 0) {
2198
+ throw new TypeError("DPoP vault namespace requires resolved issuer and clientId.");
2199
+ }
2200
+ return Object.freeze(["vault", origin, normalizeNamespaceBasePath(origin, basePath), issuer, clientId]);
2201
+ }
2202
+ if (space?.type === "api") {
2203
+ const { publicOrigin, replayNamespace } = space;
2204
+ if (!isPrintableIdentifier(replayNamespace)) {
2205
+ throw new TypeError("DPoP replayNamespace must contain 1 through 128 printable ASCII bytes.");
2206
+ }
2207
+ return Object.freeze(["api", resolveDeviceBindingOrigin(publicOrigin, "publicOrigin"), replayNamespace]);
2208
+ }
2209
+ throw new TypeError("DPoP replay policy requires a vault or API protection space.");
2210
+ };
2211
+ var createDpopReplayKey = (effectiveNamespace, jkt, jti) => {
2212
+ if (!isCanonicalDpopJkt(jkt) || !isPrintableIdentifier(jti)) {
2213
+ throw new TypeError("DPoP replay key requires a canonical thumbprint and a bounded printable JTI.");
2214
+ }
2215
+ return `dpop:v1:${createHash5("sha256").update(JSON.stringify([effectiveNamespace, jkt, jti]), "utf8").digest("base64url")}`;
2216
+ };
2217
+ var getDpopReplayExpiresAt = (input) => {
2218
+ const { iat, now, proofMaxAgeSeconds: age, clockSkewSeconds: skew } = input;
2219
+ if (typeof iat !== "number" || !Number.isSafeInteger(iat) || iat < 0 || !Number.isSafeInteger(now) || now < 0 || !Number.isInteger(age) || age < 1 || age > 300 || !Number.isInteger(skew) || skew < 0 || skew > 30)
2220
+ return null;
2221
+ const issuedAt = iat * 1e3;
2222
+ const expiresAt = (iat + age + skew) * 1e3;
2223
+ if (!Number.isSafeInteger(issuedAt) || !Number.isSafeInteger(expiresAt) || issuedAt - now > skew * 1e3 || expiresAt <= now || expiresAt - now > 36e4)
2224
+ return null;
2225
+ return expiresAt;
2226
+ };
2227
+ var invalidProof2 = (diagnostic) => new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_DPOP_PROOF", diagnostic, "DPoP proof validation failed.");
2228
+ var replayUnavailable = (cause) => {
2229
+ const error = new OidcVaultHttpError(
2230
+ 503,
2231
+ "OIDC_VAULT_DPOP_REPLAY_UNAVAILABLE",
2232
+ "DPoP replay admission failed.",
2233
+ "DPoP replay protection is unavailable."
2234
+ );
2235
+ Object.defineProperty(error, "cause", { value: cause, configurable: true });
2236
+ return error;
2237
+ };
2238
+ var createDpopReplayPolicy = (options) => {
2239
+ const { protectionSpace, proofOptions, replayStore, now: configuredNow } = options;
2240
+ const policy = resolveDeviceBindingOptions(proofOptions);
2241
+ if (!policy) throw new TypeError("DPoP replay policy requires enabled proof options.");
2242
+ if (typeof replayStore?.reserveDpopProof !== "function")
2243
+ throw new TypeError("DPoP replay policy requires a replay store.");
2244
+ if (configuredNow !== void 0 && typeof configuredNow !== "function")
2245
+ throw new TypeError("DPoP replay clock must be callable.");
2246
+ const now = configuredNow ?? Date.now;
2247
+ const effectiveNamespace = resolveDpopEffectiveNamespace(protectionSpace);
2248
+ const noncePolicy = policy.nonce === false ? void 0 : createDpopNoncePolicy({
2249
+ ...policy.nonce,
2250
+ namespace: JSON.stringify(effectiveNamespace)
2251
+ });
2252
+ return Object.freeze({
2253
+ effectiveNamespace,
2254
+ async verifyAndReserve(verifyBeforeReplay) {
2255
+ const verified = await verifyBeforeReplay();
2256
+ if (typeof verified !== "object" || verified === null || Array.isArray(verified)) {
2257
+ throw invalidProof2("DPoP verifier did not return a proof verification result.");
2258
+ }
2259
+ const { binding: verifiedBinding, iat, jti, nonce } = verified;
2260
+ if (typeof verifiedBinding !== "object" || verifiedBinding === null || Array.isArray(verifiedBinding)) {
2261
+ throw invalidProof2("DPoP verifier did not return a verified key binding.");
2262
+ }
2263
+ const { type, jkt, alg } = verifiedBinding;
2264
+ if (type !== "dpop" || !isCanonicalDpopJkt(jkt) || !isDpopAlgorithm(alg) || !policy.algorithms.includes(alg) || !isPrintableIdentifier(jti)) {
2265
+ throw invalidProof2("DPoP verified key/algorithm or signed JTI shape is invalid.");
2266
+ }
2267
+ const binding = Object.freeze({ type, jkt, alg });
2268
+ let checkedNow;
2269
+ try {
2270
+ checkedNow = now();
2271
+ if (!Number.isSafeInteger(checkedNow) || checkedNow < 0) throw new TypeError("DPoP replay clock is invalid.");
2272
+ } catch (error) {
2273
+ throw replayUnavailable(error);
2274
+ }
2275
+ const expiresAt = getDpopReplayExpiresAt({ ...policy, iat, now: checkedNow });
2276
+ if (expiresAt === null) throw invalidProof2("DPoP signed iat is outside the bounded replay window.");
2277
+ if (noncePolicy) {
2278
+ if (!noncePolicy.verify(nonce, jkt, checkedNow)) {
2279
+ return Object.freeze({ type: "nonce-challenge", nonce: noncePolicy.issue(jkt, checkedNow) });
2280
+ }
2281
+ } else if (nonce !== void 0 && (typeof nonce !== "string" || nonce.length > DPOP_NONCE_MAX_BYTES || Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES)) {
2282
+ throw invalidProof2("DPoP signed nonce claim exceeds its string/byte bound.");
2283
+ }
2284
+ let reserved;
2285
+ try {
2286
+ reserved = await replayStore.reserveDpopProof({
2287
+ replayKey: createDpopReplayKey(effectiveNamespace, jkt, jti),
2288
+ expiresAt
2289
+ });
2290
+ if (typeof reserved !== "boolean")
2291
+ throw new TypeError("DPoP replay store returned a non-boolean admission result.");
2292
+ } catch (error) {
2293
+ throw replayUnavailable(error);
2294
+ }
2295
+ if (!reserved) throw invalidProof2("DPoP replay reservation was duplicate or no longer live.");
2296
+ return Object.freeze({ type: "accepted", binding, expiresAt });
2297
+ }
2298
+ });
2299
+ };
2300
+ var challenge = (error, algorithms) => {
2301
+ if (algorithms.length === 0 || !algorithms.every(isDpopAlgorithm))
2302
+ throw new TypeError("DPoP challenge requires a resolved algorithm allowlist.");
2303
+ return `DPoP error="${error}", algs="${[...new Set(algorithms)].join(" ")}"`;
2304
+ };
2305
+ var toDpopReplayErrorResponse = (error, algorithms) => {
2306
+ const { status, code, message } = toErrorPayload(error);
2307
+ return Object.freeze({
2308
+ status,
2309
+ body: Object.freeze({ code, message }),
2310
+ headers: Object.freeze({
2311
+ "Cache-Control": "no-store",
2312
+ ...status === 401 && (code === "OIDC_VAULT_INVALID_DPOP_PROOF" || code === "OIDC_VAULT_DPOP_REQUIRED") ? { "WWW-Authenticate": challenge("invalid_dpop_proof", algorithms) } : {}
2313
+ })
2314
+ });
2315
+ };
2316
+ var toDpopNonceChallengeResponse = (result, endpoint, algorithms) => {
2317
+ const nonce = result?.nonce;
2318
+ if (result?.type !== "nonce-challenge" || typeof nonce !== "string" || nonce.length > DPOP_NONCE_MAX_BYTES || !/^v1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{43}$/.test(nonce))
2319
+ throw new TypeError("DPoP nonce response requires a bounded server-issued challenge.");
2320
+ if (endpoint !== "vault-post" && endpoint !== "api")
2321
+ throw new TypeError("DPoP nonce response requires an API or vault POST endpoint.");
2322
+ return Object.freeze({
2323
+ status: endpoint === "api" ? 401 : 400,
2324
+ body: Object.freeze({ code: "OIDC_VAULT_USE_DPOP_NONCE", message: "A fresh DPoP nonce is required." }),
2325
+ headers: Object.freeze({
2326
+ "Cache-Control": "no-store",
2327
+ "DPoP-Nonce": nonce,
2328
+ ...endpoint === "api" ? { "WWW-Authenticate": challenge("use_dpop_nonce", algorithms) } : {}
2329
+ })
2330
+ });
2331
+ };
2332
+
2333
+ // src/vault-route-proof.ts
2334
+ var captureVaultRouteProof = (req) => Object.freeze({
2335
+ header: readDpopRawHeader(req, "dpop"),
2336
+ method: req.method,
2337
+ originalUrl: req.originalUrl
2338
+ });
2339
+ var VaultDpopNonceChallengeError = class extends OidcVaultHttpError {
2340
+ constructor(response) {
2341
+ super(400, "OIDC_VAULT_USE_DPOP_NONCE", "A fresh DPoP nonce is required.");
2342
+ this.response = response;
2343
+ }
2344
+ };
2345
+ var createVaultRouteProofVerifier = (options, config, backendOrigin, basePath) => {
2346
+ const policy = options.deviceBinding;
2347
+ if (policy === void 0) return void 0;
2348
+ assertDeviceBindingStoreCapabilities(options.storeProvider);
2349
+ const targetFor = createDpopRequestTargetResolver({ publicOrigin: backendOrigin });
2350
+ const replay = createDpopReplayPolicy({
2351
+ // The config resolver requires both identifiers in manual/discovery modes.
2352
+ protectionSpace: { type: "vault", backendOrigin, basePath, issuer: config.issuer, clientId: config.clientId },
2353
+ proofOptions: policy,
2354
+ replayStore: options.storeProvider,
2355
+ now: options.now
2356
+ });
2357
+ const admit = async (request, proof, expectedJkt) => {
2358
+ const targetUrl = targetFor(request.originalUrl);
2359
+ const result = await replay.verifyAndReserve(
2360
+ () => verifyDpopProof({
2361
+ proof,
2362
+ method: request.method,
2363
+ targetUrl,
2364
+ proofOptions: policy,
2365
+ expectedJkt
2366
+ })
2367
+ );
2368
+ if (result.type === "nonce-challenge") {
2369
+ throw new VaultDpopNonceChallengeError(toDpopNonceChallengeResponse(result, "vault-post", policy.algorithms));
2370
+ }
2371
+ return result.binding;
2372
+ };
2373
+ return Object.freeze({
2374
+ async initiate(request) {
2375
+ const proof = extractDpopProof(request.header);
2376
+ if (proof === void 0) {
2377
+ assertDeviceBindingPolicy(policy, void 0);
2378
+ return void 0;
2379
+ }
2380
+ return admit(request, proof);
2381
+ },
2382
+ async verify(request, originalBinding) {
2383
+ const binding = snapshotDpopBinding(originalBinding);
2384
+ assertDeviceBindingPolicy(policy, binding);
2385
+ const proof = extractDpopProof(request.header);
2386
+ const verified = proof === void 0 ? void 0 : await admit(request, proof, binding?.jkt);
2387
+ return resolveVerifiedDeviceBinding(policy, binding, verified);
2388
+ }
2389
+ });
2390
+ };
2391
+ var toVaultRouteErrorResponse = (error, policy) => error instanceof VaultDpopNonceChallengeError ? error.response : toDpopReplayErrorResponse(error, policy?.algorithms ?? ["ES256"]);
2392
+ var sendVaultRoutePolicyResponse = (res, response) => {
2393
+ res.removeHeader("WWW-Authenticate");
2394
+ res.removeHeader("DPoP-Nonce");
2395
+ for (const [name, value] of Object.entries(response.headers)) res.setHeader(name, value);
2396
+ res.status(response.status).json(response.body);
2397
+ };
900
2398
 
901
2399
  // src/access-token-middleware.ts
902
2400
  var extractBearerToken = (authorizationHeader) => {
@@ -913,36 +2411,212 @@ var extractBearerToken = (authorizationHeader) => {
913
2411
  }
914
2412
  return token;
915
2413
  };
916
- var setBearerChallengeHeader = (res) => {
917
- res.setHeader("WWW-Authenticate", "Bearer");
2414
+ var notifyMiddlewareError = async (onError, context) => {
2415
+ try {
2416
+ await onError?.(context);
2417
+ } catch {
2418
+ }
2419
+ };
2420
+ var schemeOf = (value) => {
2421
+ const scheme = value?.trim().split(/\s+/, 1)[0]?.toLowerCase();
2422
+ return scheme === "bearer" ? "Bearer" : scheme === "dpop" ? "DPoP" : void 0;
2423
+ };
2424
+ var mappedFields = (result) => {
2425
+ if (typeof result !== "object" || result === null || Array.isArray(result))
2426
+ throw invalidAccessToken("Access token validator result is invalid.");
2427
+ const { subject, sessionId, scope, claims } = result;
2428
+ if (typeof subject !== "string" || subject.length === 0)
2429
+ throw invalidAccessToken("Access token validator result is missing subject.");
2430
+ return { subject, sessionId, scope, claims };
918
2431
  };
919
2432
  function createOidcVaultAccessTokenMiddleware(options) {
2433
+ const { validator, deviceBinding: configuredBinding, onAuthContext, onError } = options;
2434
+ const policy = resolveDeviceBindingOptions(configuredBinding);
2435
+ const validate = validator.validate.bind(validator);
2436
+ const requestAware = policy ? validator.validateWithRequest : void 0;
2437
+ if (policy && typeof requestAware !== "function")
2438
+ throw new TypeError("deviceBinding requires validator.validateWithRequest.");
2439
+ const validateWithRequest = requestAware?.bind(validator);
2440
+ const apiOptions = policy ? (() => {
2441
+ const { publicOrigin, publicPathPrefix, replayNamespace, replayStore, now } = configuredBinding;
2442
+ return {
2443
+ publicOrigin: resolveDeviceBindingOrigin(publicOrigin, "publicOrigin"),
2444
+ publicPathPrefix,
2445
+ replayNamespace,
2446
+ replayStore,
2447
+ now
2448
+ };
2449
+ })() : void 0;
2450
+ const targetFor = apiOptions ? createDpopRequestTargetResolver(apiOptions) : void 0;
2451
+ const replay = policy ? createDpopReplayPolicy({
2452
+ protectionSpace: {
2453
+ type: "api",
2454
+ publicOrigin: apiOptions.publicOrigin,
2455
+ replayNamespace: apiOptions.replayNamespace
2456
+ },
2457
+ proofOptions: policy,
2458
+ replayStore: apiOptions.replayStore,
2459
+ now: apiOptions.now
2460
+ }) : void 0;
2461
+ const algorithms = policy?.algorithms ?? ["ES256"];
2462
+ const algs = algorithms.join(" ");
2463
+ const dpopChallenge = (error) => `DPoP${error ? ` error="${error}",` : ""} algs="${algs}"`;
2464
+ const proofChallenge = dpopChallenge("invalid_dpop_proof");
920
2465
  return async (req, res, next) => {
2466
+ let token;
2467
+ let scheme;
2468
+ let nonceResponse;
2469
+ const authenticatedRequest = req;
2470
+ delete authenticatedRequest.auth;
2471
+ if (!res.headersSent) res.setHeader("Cache-Control", "no-store");
2472
+ const prepareError = (error, hookFailure = false) => {
2473
+ const response = nonceResponse ?? (error instanceof OidcVaultHttpError ? toDpopReplayErrorResponse(error, algorithms) : {
2474
+ status: hookFailure ? 500 : 401,
2475
+ body: hookFailure ? { code: "OIDC_VAULT_AUTH_CONTEXT_FAILED", message: "Auth context hook failed." } : { code: "OIDC_VAULT_INVALID_ACCESS_TOKEN", message: "Access token validation failed." },
2476
+ headers: { "Cache-Control": "no-store" }
2477
+ });
2478
+ const headers = { ...response.headers };
2479
+ if (response.status === 401 && headers["WWW-Authenticate"] === void 0) {
2480
+ const code = response.body.code;
2481
+ const challenge2 = code === "OIDC_VAULT_DPOP_REQUIRED" || code === "OIDC_VAULT_INVALID_DPOP_PROOF" ? proofChallenge : !policy ? "Bearer" : code === "OIDC_VAULT_MISSING_ACCESS_TOKEN" ? policy.mode === "required" ? dpopChallenge() : `Bearer, ${dpopChallenge()}` : scheme === "DPoP" || !scheme && policy.mode === "required" ? dpopChallenge("invalid_token") : 'Bearer error="invalid_token"';
2482
+ headers["WWW-Authenticate"] = challenge2;
2483
+ }
2484
+ return Object.freeze({
2485
+ status: response.status,
2486
+ body: Object.freeze({ ...response.body }),
2487
+ headers: Object.freeze(headers)
2488
+ });
2489
+ };
2490
+ const sendError = (response) => {
2491
+ res.removeHeader("WWW-Authenticate");
2492
+ res.removeHeader("DPoP-Nonce");
2493
+ for (const [name, value] of Object.entries(response.headers)) res.setHeader(name, value);
2494
+ res.status(response.status).json(response.body);
2495
+ };
921
2496
  try {
922
- const token = extractBearerToken(req.get("authorization"));
923
- const validationResult = await options.validator.validate(token);
2497
+ let result;
2498
+ let confirmation;
2499
+ let deviceBinding;
2500
+ if (!policy) {
2501
+ token = extractBearerToken(req.get("authorization"));
2502
+ scheme = "Bearer";
2503
+ result = await validate(token);
2504
+ delete authenticatedRequest.auth;
2505
+ const reported = result?.confirmation;
2506
+ confirmation = reported === void 0 ? void 0 : snapshotAccessTokenConfirmation(reported);
2507
+ result = mappedFields(result);
2508
+ delete authenticatedRequest.auth;
2509
+ if (confirmation)
2510
+ throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
2511
+ } else {
2512
+ const authorization = readDpopRawHeader(req, "authorization");
2513
+ scheme = schemeOf(authorization.value);
2514
+ if (authorization.count === 0 || authorization.count === 1 && !authorization.value?.trim()) {
2515
+ throw new OidcVaultHttpError(401, "OIDC_VAULT_MISSING_ACCESS_TOKEN", "Missing access token.");
2516
+ }
2517
+ const credential = authorization.value?.trim().split(/\s+/);
2518
+ if (authorization.count !== 1 || !scheme || credential?.length !== 2 || !credential[1] || !/^[\x21-\x7e]+$/.test(credential[1]) || credential[1].includes(",")) {
2519
+ throw new OidcVaultHttpError(
2520
+ 401,
2521
+ "OIDC_VAULT_INVALID_AUTHORIZATION_HEADER",
2522
+ "Authorization header must use one Bearer or DPoP credential."
2523
+ );
2524
+ }
2525
+ token = credential[1];
2526
+ const proofHeader = readDpopRawHeader(req, "dpop");
2527
+ const { method, originalUrl } = req;
2528
+ result = await validateWithRequest(Object.freeze({ token, scheme, req }));
2529
+ delete authenticatedRequest.auth;
2530
+ confirmation = snapshotAccessTokenConfirmation(result?.confirmation);
2531
+ result = mappedFields(result);
2532
+ delete authenticatedRequest.auth;
2533
+ if (confirmation && scheme !== "DPoP")
2534
+ throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
2535
+ if (!confirmation && scheme === "DPoP")
2536
+ throw invalidAccessToken("DPoP presentation of a verified unbound token is invalid.");
2537
+ if (!confirmation && policy.mode === "required") {
2538
+ throw new OidcVaultHttpError(401, "OIDC_VAULT_DEVICE_BINDING_REQUIRED", "A device-bound login is required.");
2539
+ }
2540
+ const proof = extractDpopProof(proofHeader);
2541
+ if (!proof && confirmation)
2542
+ throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
2543
+ if (proof) {
2544
+ const targetUrl = targetFor(originalUrl);
2545
+ const admission = await replay.verifyAndReserve(
2546
+ () => verifyDpopProof({
2547
+ proof,
2548
+ method,
2549
+ targetUrl,
2550
+ proofOptions: policy,
2551
+ accessToken: token,
2552
+ expectedJkt: confirmation?.jkt
2553
+ })
2554
+ );
2555
+ if (admission.type === "nonce-challenge") {
2556
+ nonceResponse = toDpopNonceChallengeResponse(admission, "api", policy.algorithms);
2557
+ throw new OidcVaultHttpError(401, "OIDC_VAULT_USE_DPOP_NONCE", "A fresh DPoP nonce is required.");
2558
+ }
2559
+ if (confirmation) deviceBinding = admission.binding;
2560
+ }
2561
+ }
2562
+ const authenticatedToken = token;
924
2563
  const auth = {
925
- token,
926
- ...validationResult
2564
+ ...result,
2565
+ token: authenticatedToken,
2566
+ ...confirmation === void 0 ? {} : { confirmation },
2567
+ ...deviceBinding ? { deviceBinding } : {}
2568
+ };
2569
+ const restoreAuth = () => {
2570
+ const { subject, sessionId, scope, claims } = auth;
2571
+ return {
2572
+ subject,
2573
+ sessionId,
2574
+ scope,
2575
+ claims,
2576
+ token: authenticatedToken,
2577
+ ...confirmation === void 0 ? {} : { confirmation },
2578
+ ...deviceBinding ? { deviceBinding } : {}
2579
+ };
927
2580
  };
928
- const authenticatedRequest = req;
929
2581
  authenticatedRequest.auth = auth;
930
- await options.onAuthContext?.({ req: authenticatedRequest, res, auth });
2582
+ try {
2583
+ await onAuthContext?.({ req: authenticatedRequest, res, auth });
2584
+ authenticatedRequest.auth = restoreAuth();
2585
+ } catch (hookError) {
2586
+ const response = prepareError(hookError, true);
2587
+ delete authenticatedRequest.auth;
2588
+ let errorAuth;
2589
+ try {
2590
+ errorAuth = restoreAuth();
2591
+ } catch {
2592
+ errorAuth = {
2593
+ ...result,
2594
+ token: authenticatedToken,
2595
+ ...confirmation === void 0 ? {} : { confirmation },
2596
+ ...deviceBinding ? { deviceBinding } : {}
2597
+ };
2598
+ }
2599
+ await notifyMiddlewareError(onError, { error: hookError, req, res, token, auth: errorAuth });
2600
+ delete authenticatedRequest.auth;
2601
+ if (res.headersSent) {
2602
+ next(hookError);
2603
+ return;
2604
+ }
2605
+ sendError(response);
2606
+ return;
2607
+ }
2608
+ if (!res.headersSent) res.setHeader("Cache-Control", "no-store");
931
2609
  next();
932
2610
  } catch (error) {
933
- if (error instanceof OidcVaultHttpError) {
934
- setBearerChallengeHeader(res);
935
- res.status(error.status).json({
936
- code: error.code,
937
- message: error.clientMessage
938
- });
2611
+ const response = prepareError(error);
2612
+ delete authenticatedRequest.auth;
2613
+ await notifyMiddlewareError(onError, { error, req, res, ...token ? { token } : {} });
2614
+ delete authenticatedRequest.auth;
2615
+ if (res.headersSent) {
2616
+ next(error);
939
2617
  return;
940
2618
  }
941
- setBearerChallengeHeader(res);
942
- res.status(401).json({
943
- code: "OIDC_VAULT_INVALID_ACCESS_TOKEN",
944
- message: "Access token validation failed."
945
- });
2619
+ sendError(response);
946
2620
  }
947
2621
  };
948
2622
  }
@@ -955,26 +2629,60 @@ function normalizeOidcVaultBasePath(value) {
955
2629
  return `/${value.replace(/^\/+|\/+$/g, "")}`;
956
2630
  }
957
2631
  var getNow = (options) => (options.now ?? Date.now)();
958
- var createOpaqueId = (prefix) => `${prefix}_${randomBytes(16).toString("base64url")}`;
959
- var createPkceVerifier = () => randomBytes(32).toString("base64url");
960
- var createPkceChallenge = (verifier) => createHash("sha256").update(verifier).digest("base64url");
2632
+ var CREDENTIAL_RESPONSE_CACHE_CONTROL = "no-store";
2633
+ var applyCredentialResponseCachePolicy = (res) => {
2634
+ res.setHeader("Cache-Control", CREDENTIAL_RESPONSE_CACHE_CONTROL);
2635
+ };
2636
+ var createOpaqueId = (prefix) => `${prefix}_${randomBytes3(16).toString("base64url")}`;
2637
+ var createPkceVerifier = () => randomBytes3(32).toString("base64url");
2638
+ var createPkceChallenge = (verifier) => createHash6("sha256").update(verifier).digest("base64url");
961
2639
  var getCallbackUri = (backendOrigin, basePath) => `${backendOrigin}${basePath}${OIDC_VAULT_ROUTE_PATHS.callback}`;
962
2640
  var validateOidcVaultOptions = (options) => {
963
- const backendOrigin = resolveBackendOrigin(options);
2641
+ const {
2642
+ deviceBinding: configuredDeviceBinding,
2643
+ fingerprintRecognition: configuredFingerprintRecognition,
2644
+ transactionCookie: configuredTransactionCookie,
2645
+ ...otherOptions
2646
+ } = options;
2647
+ const cookie = otherOptions.cookie ? { ...otherOptions.cookie } : void 0;
2648
+ const cookieOptions = { ...otherOptions, cookie };
2649
+ validateLifetimeOptions(options, getNow(options));
2650
+ const deviceBinding = resolveDeviceBindingOptions(configuredDeviceBinding);
2651
+ const fingerprintRecognition = resolveFingerprintRecognitionOptions(configuredFingerprintRecognition);
2652
+ const backendOrigin = deviceBinding === void 0 ? resolveBackendOrigin(options) : resolveDeviceBindingOrigin(options.backendOrigin);
2653
+ if (deviceBinding !== void 0) assertDeviceBindingStoreCapabilities(options.storeProvider);
2654
+ if (fingerprintRecognition !== void 0)
2655
+ assertDeviceBindingStoreCapabilities(options.storeProvider, "fingerprintRecognition");
964
2656
  const frontendRedirectUri = resolveFrontendRedirectUri(options);
965
2657
  validatePostLogoutRedirectUri(options);
966
- validateCookieOptions(options);
967
- const config = resolveOidcVaultConfig(options.config);
2658
+ validateCookieOptions(cookieOptions);
2659
+ const transactionCookie = resolveTransactionCookieOptions(
2660
+ {
2661
+ ...cookieOptions,
2662
+ deviceBinding: configuredDeviceBinding,
2663
+ fingerprintRecognition,
2664
+ transactionCookie: configuredTransactionCookie
2665
+ },
2666
+ backendOrigin
2667
+ );
2668
+ const config = resolveOidcVaultConfig(options.config ? { ...options.config } : void 0);
968
2669
  const configuredTrustedOrigins = resolveTrustedOrigins(options);
969
- if (usesCrossSiteCookieTransport(options) && configuredTrustedOrigins.size === 0) {
2670
+ if (usesCrossSiteCookieTransport(cookieOptions) && configuredTrustedOrigins.size === 0) {
970
2671
  throw new Error("trustedOrigins is required when using cross-site cookie transport.");
971
2672
  }
972
2673
  const trustedOrigins = new Set(configuredTrustedOrigins);
973
2674
  trustedOrigins.add(backendOrigin);
974
- if (frontendRedirectUri) {
975
- options.frontendRedirectUri = frontendRedirectUri;
976
- }
977
- return { backendOrigin, config, trustedOrigins };
2675
+ const resolvedOptions = {
2676
+ ...otherOptions,
2677
+ deviceBinding,
2678
+ fingerprintRecognition,
2679
+ transactionCookie,
2680
+ ...frontendRedirectUri !== void 0 ? { frontendRedirectUri } : {},
2681
+ ...options.trustedOrigins ? { trustedOrigins: [...options.trustedOrigins] } : {},
2682
+ ...cookie === void 0 ? {} : { cookie },
2683
+ ...options.config ? { config: { ...options.config } } : {}
2684
+ };
2685
+ return { backendOrigin, config, trustedOrigins, resolvedOptions };
978
2686
  };
979
2687
  var isStoreConflictError = (error) => error instanceof OidcVaultStoreConflictError || typeof error === "object" && error !== null && "name" in error && error.name === "OidcVaultStoreConflictError";
980
2688
  var getSessionIdFromRequest = (req, options, action) => {
@@ -996,11 +2704,6 @@ var getSessionIdFromRequest = (req, options, action) => {
996
2704
  "OIDC_VAULT_MISSING_SESSION_ID"
997
2705
  );
998
2706
  };
999
- var createExchangeResponse = (options, session, issuedToken) => ({
1000
- sessionId: usesCookieTransport(options) ? void 0 : session.sessionId,
1001
- user: session.user,
1002
- ...issuedToken
1003
- });
1004
2707
  var appendQueryParam = (url, name, value) => {
1005
2708
  if (value) {
1006
2709
  url.searchParams.set(name, value);
@@ -1032,9 +2735,12 @@ var getLogoutTokenFromRequest = (req) => {
1032
2735
  "OIDC_VAULT_MISSING_LOGOUT_TOKEN"
1033
2736
  );
1034
2737
  };
2738
+ var buildBackchannelLogoutReplayKey = (input) => {
2739
+ const encode = (value) => Buffer.from(value, "utf8").toString("base64url");
2740
+ return `v2:${encode(input.issuer ?? "")}:${encode(input.clientId ?? "")}:${encode(input.jti)}`;
2741
+ };
1035
2742
  var createHookContext = (route, req, res, session, metadata) => ({ route, req, res, session, metadata });
1036
- var resolveReturnTo = (req, options) => {
1037
- const rawReturnTo = req.query.returnTo;
2743
+ var resolveLoginReturnTo = (rawReturnTo, options) => {
1038
2744
  if (!isString(rawReturnTo)) {
1039
2745
  return void 0;
1040
2746
  }
@@ -1062,17 +2768,12 @@ var resolveReturnTo = (req, options) => {
1062
2768
  }
1063
2769
  return resolvedUrl.toString();
1064
2770
  };
2771
+ var resolveReturnTo = (req, options) => resolveLoginReturnTo(req.query.returnTo, options);
1065
2772
  var appendCodeToRedirectUri = (redirectUri, code) => {
1066
2773
  const url = new URL(redirectUri);
1067
2774
  url.searchParams.set("code", code);
1068
2775
  return url.toString();
1069
2776
  };
1070
- var withIssuedToken = async (req, res, options, session) => {
1071
- if (!options.tokenIssuer) {
1072
- return {};
1073
- }
1074
- return options.tokenIssuer.issue({ req, res, session });
1075
- };
1076
2777
  var resolveFrontendRedirectUri2 = (transaction, options) => {
1077
2778
  const redirectUri = transaction.returnTo ?? options.frontendRedirectUri;
1078
2779
  if (!redirectUri) {
@@ -1108,6 +2809,7 @@ async function callPostCommitHook(route, options, hook, req, res, session, metad
1108
2809
  }
1109
2810
  }
1110
2811
  async function handleRouteError(route, req, res, next, options, error) {
2812
+ const response = toVaultRouteErrorResponse(error, options.deviceBinding);
1111
2813
  try {
1112
2814
  await options.hooks?.onError?.({
1113
2815
  ...createHookContext(route, req, res),
@@ -1119,11 +2821,7 @@ async function handleRouteError(route, req, res, next, options, error) {
1119
2821
  next(error);
1120
2822
  return;
1121
2823
  }
1122
- const payload = toErrorPayload(error);
1123
- res.status(payload.status).json({
1124
- code: payload.code,
1125
- message: payload.message
1126
- });
2824
+ sendVaultRoutePolicyResponse(res, response);
1127
2825
  }
1128
2826
  function createAsyncHandler(route, options, handler) {
1129
2827
  return async (req, res, next) => {
@@ -1140,6 +2838,7 @@ function createBodyParserErrorHandler() {
1140
2838
  next(error);
1141
2839
  return;
1142
2840
  }
2841
+ applyCredentialResponseCachePolicy(res);
1143
2842
  const payload = toBodyParserErrorPayload(error);
1144
2843
  res.status(payload.status).json({
1145
2844
  code: payload.code,
@@ -1148,6 +2847,7 @@ function createBodyParserErrorHandler() {
1148
2847
  };
1149
2848
  }
1150
2849
  var createLoginHandler = (options, config, backendOrigin, basePath) => createAsyncHandler("login", options, async (req, res) => {
2850
+ assertDeviceBindingPolicy(options.deviceBinding, void 0);
1151
2851
  const metadata = await resolveProviderMetadata(config, options);
1152
2852
  await callHook("login", options.hooks?.onLoginStart, req, res, void 0, { provider: metadata });
1153
2853
  const now = getNow(options);
@@ -1159,7 +2859,11 @@ var createLoginHandler = (options, config, backendOrigin, basePath) => createAsy
1159
2859
  codeChallenge: createPkceChallenge(pkceVerifier),
1160
2860
  returnTo: resolveReturnTo(req, options),
1161
2861
  createdAt: now,
1162
- expiresAt: now + (options.authorizationTransactionTtlMs ?? DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS)
2862
+ expiresAt: computeExpiresAt(
2863
+ now,
2864
+ options.authorizationTransactionTtlMs ?? DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS,
2865
+ "authorizationTransactionTtlMs"
2866
+ )
1163
2867
  };
1164
2868
  await options.storeProvider.createAuthorizationTransaction(transaction);
1165
2869
  const authorizationUrl = buildAuthorizationUrl(metadata, transaction, getCallbackUri(backendOrigin, basePath));
@@ -1169,140 +2873,330 @@ var createLoginHandler = (options, config, backendOrigin, basePath) => createAsy
1169
2873
  });
1170
2874
  res.redirect(302, authorizationUrl);
1171
2875
  });
1172
- var createCallbackHandler = (options, config, backendOrigin, basePath) => createAsyncHandler("callback", options, async (req, res) => {
1173
- if (isString(req.query.error)) {
1174
- throw new OidcVaultHttpError(400, "OIDC_VAULT_CALLBACK_ERROR", req.query.error);
2876
+ var createLoginInitiationHandler = (options, config, trustedOrigins, backendOrigin, basePath, proofVerifier) => createAsyncHandler("login", options, async (req, res) => {
2877
+ const capturedProof = captureVaultRouteProof(req);
2878
+ const fingerprintRecognition = captureFingerprintRecognition(req, options.fingerprintRecognition);
2879
+ assertLoginInitiationRequest(req, options, trustedOrigins);
2880
+ const body = getBody(req);
2881
+ if (Array.isArray(req.body) || body.returnTo !== void 0 && !isString(body.returnTo)) {
2882
+ throw new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_REQUEST_BODY", "Login initiation request body is invalid.");
2883
+ }
2884
+ const returnTo = resolveLoginReturnTo(body.returnTo, options);
2885
+ if (proofVerifier === void 0 && capturedProof.header.count !== 0) {
2886
+ throw new OidcVaultHttpError(
2887
+ 401,
2888
+ "OIDC_VAULT_INVALID_DPOP_PROOF",
2889
+ "DPoP login initiation is not enabled.",
2890
+ "DPoP proof validation failed."
2891
+ );
1175
2892
  }
1176
- const code = getRequiredString(
2893
+ const verifiedBinding = await proofVerifier?.initiate(capturedProof);
2894
+ const binding = snapshotDpopBinding(verifiedBinding);
2895
+ const metadata = await resolveProviderMetadata(config, options);
2896
+ await callHook("login", options.hooks?.onLoginStart, req, res, void 0, { provider: { ...metadata } });
2897
+ const now = getNow(options);
2898
+ const browserBinding = createTransactionBrowserBinding();
2899
+ const pkceVerifier = createPkceVerifier();
2900
+ const transaction = {
2901
+ state: createOpaqueId("state"),
2902
+ nonce: createOpaqueId("nonce"),
2903
+ pkceVerifier,
2904
+ codeChallenge: createPkceChallenge(pkceVerifier),
2905
+ returnTo,
2906
+ ...binding === void 0 ? {} : { deviceBinding: binding },
2907
+ browserBindingHash: browserBinding.browserBindingHash,
2908
+ createdAt: now,
2909
+ expiresAt: computeExpiresAt(
2910
+ now,
2911
+ options.authorizationTransactionTtlMs ?? DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS,
2912
+ "authorizationTransactionTtlMs"
2913
+ ),
2914
+ metadata: Object.freeze({
2915
+ ...createTransactionProviderMetadata(config),
2916
+ ...fingerprintRecognition === void 0 ? {} : { [FINGERPRINT_RECOGNITION_METADATA_KEY]: fingerprintRecognition }
2917
+ })
2918
+ };
2919
+ const authorizationUrl = buildAuthorizationUrl(metadata, transaction, getCallbackUri(backendOrigin, basePath));
2920
+ const { state, expiresAt } = transaction;
2921
+ await options.storeProvider.createAuthorizationTransaction(transaction);
2922
+ await callHook("login", options.hooks?.onAuthorizationUrl, req, res, void 0, { authorizationUrl, state });
2923
+ setTransactionCookie(res, options.transactionCookie, browserBinding.secret, expiresAt, getNow(options));
2924
+ applyCredentialResponseCachePolicy(res);
2925
+ res.status(200).json({ authorizationUrl });
2926
+ });
2927
+ var createCallbackHandler = (options, config, backendOrigin, basePath) => createAsyncHandler("callback", options, async (req, res) => {
2928
+ const providerError = isString(req.query.error) ? req.query.error : void 0;
2929
+ const code = providerError === void 0 ? getRequiredString(
1177
2930
  req.query.code,
1178
2931
  "OIDC callback is missing the authorization code.",
1179
2932
  "OIDC_VAULT_MISSING_CODE"
1180
- );
2933
+ ) : void 0;
1181
2934
  const state = getRequiredString(req.query.state, "OIDC callback is missing state.", "OIDC_VAULT_MISSING_STATE");
1182
- const transaction = await options.storeProvider.consumeAuthorizationTransaction(state);
1183
- if (!transaction) {
1184
- throw new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_STATE", "OIDC state is invalid or expired.");
1185
- }
1186
- const metadata = await resolveProviderMetadata(config, options);
1187
- const tokenResponse = await requestToken(
1188
- metadata,
1189
- {
1190
- grant_type: "authorization_code",
1191
- code,
1192
- code_verifier: transaction.pkceVerifier,
1193
- redirect_uri: getCallbackUri(backendOrigin, basePath)
1194
- },
1195
- options
1196
- );
1197
- validateTokenResponse(tokenResponse);
1198
- if (!isString(tokenResponse.id_token)) {
1199
- throw new OidcVaultHttpError(502, "OIDC_VAULT_MISSING_ID_TOKEN", "OIDC token response is missing id_token.");
2935
+ const cookieHeader = req.headers.cookie;
2936
+ const store = options.storeProvider;
2937
+ const preflight = typeof store.getAuthorizationTransaction === "function" ? await store.getAuthorizationTransaction(state) : await store.consumeAuthorizationTransaction(state);
2938
+ if (!preflight) throw invalidAuthorizationState();
2939
+ const transaction = snapshotAuthorizationTransaction(preflight, state, getNow(options));
2940
+ assertAuthorizationTransactionIdentity(transaction, config);
2941
+ assertDeviceBindingPolicy(options.deviceBinding, transaction.deviceBinding);
2942
+ const cookie = options.transactionCookie;
2943
+ const cookieSecret = authenticateTransactionCookie(cookieHeader, cookie, transaction.match);
2944
+ const frontendDestination = providerError === void 0 ? resolveFrontendRedirectUri2(transaction, options) : void 0;
2945
+ if (typeof store.getAuthorizationTransaction === "function") {
2946
+ let consumed;
2947
+ if (typeof store.consumeAuthorizationTransactionIfMatches === "function") {
2948
+ consumed = await store.consumeAuthorizationTransactionIfMatches({ state, match: transaction.match });
2949
+ } else {
2950
+ if (transaction.browserBindingHash !== void 0) {
2951
+ throw new TypeError("Guarded callbacks require storeProvider.consumeAuthorizationTransactionIfMatches.");
2952
+ }
2953
+ consumed = await store.consumeAuthorizationTransaction(state);
2954
+ }
2955
+ if (!consumed) throw invalidAuthorizationState();
2956
+ const returned = snapshotAuthorizationTransaction(consumed, state, getNow(options));
2957
+ assertAuthorizationTransactionIdentity(returned, config);
2958
+ assertDeviceBindingPolicy(options.deviceBinding, returned.deviceBinding);
2959
+ assertAuthorizationTransactionMatches(returned, transaction);
1200
2960
  }
1201
- if (!isString(tokenResponse.refresh_token)) {
1202
- throw new OidcVaultHttpError(
1203
- 502,
1204
- "OIDC_VAULT_MISSING_REFRESH_TOKEN",
1205
- "OIDC token response is missing refresh_token."
2961
+ try {
2962
+ if (providerError !== void 0) {
2963
+ throw new OidcVaultHttpError(400, "OIDC_VAULT_CALLBACK_ERROR", providerError, "OIDC callback failed.");
2964
+ }
2965
+ const metadata = await resolveProviderMetadata(config, options);
2966
+ const tokenResponse = await requestToken(
2967
+ metadata,
2968
+ {
2969
+ grant_type: "authorization_code",
2970
+ code,
2971
+ code_verifier: transaction.pkceVerifier,
2972
+ redirect_uri: getCallbackUri(backendOrigin, basePath)
2973
+ },
2974
+ options
1206
2975
  );
1207
- }
1208
- await callHook("callback", options.hooks?.onCallbackTokens, req, res, void 0, {
1209
- hasAccessToken: Boolean(tokenResponse.access_token),
1210
- hasRefreshToken: true,
1211
- hasIdToken: true
1212
- });
1213
- const claims = await verifyIdToken(metadata, tokenResponse.id_token, transaction.nonce, options);
1214
- const subject = getRequiredString(claims.sub, "OIDC id_token is missing sub.", "OIDC_VAULT_INVALID_ID_TOKEN", 502);
1215
- const userInfo = shouldFetchUserInfo(options, metadata) && isString(tokenResponse.access_token) ? await fetchUserInfo(metadata, tokenResponse.access_token, options) : void 0;
1216
- if (userInfo) {
1217
- assertUserInfoSubject(userInfo, subject);
1218
- }
1219
- if (userInfo) {
1220
- await callHook("callback", options.hooks?.onUserInfo, req, res, void 0, { subject });
1221
- }
1222
- const now = getNow(options);
1223
- const sessionId = createOpaqueId("sess");
1224
- const session = {
1225
- sessionId,
1226
- logicalSessionId: sessionId,
1227
- subject,
1228
- providerSessionId: typeof claims.sid === "string" ? claims.sid : void 0,
1229
- provider: {
2976
+ validateCallbackTokenResponse(tokenResponse);
2977
+ await callHook("callback", options.hooks?.onCallbackTokens, req, res, void 0, {
2978
+ hasAccessToken: Boolean(tokenResponse.access_token),
2979
+ hasRefreshToken: true,
2980
+ hasIdToken: true
2981
+ });
2982
+ const claims = await verifyIdToken(metadata, tokenResponse.id_token, transaction.nonce, options);
2983
+ const subject = getRequiredString(
2984
+ claims.sub,
2985
+ "OIDC id_token is missing sub.",
2986
+ "OIDC_VAULT_INVALID_ID_TOKEN",
2987
+ 502
2988
+ );
2989
+ const userInfo = shouldFetchUserInfo(options, metadata) && isString(tokenResponse.access_token) ? await fetchUserInfo(metadata, tokenResponse.access_token, options) : void 0;
2990
+ if (userInfo !== void 0) {
2991
+ assertUserInfoSubject(userInfo, subject);
2992
+ await callHook("callback", options.hooks?.onUserInfo, req, res, void 0, { subject });
2993
+ }
2994
+ const now = getNow(options);
2995
+ const sessionExpiresAt = options.sessionTtlMs === void 0 ? void 0 : computeExpiresAt(now, options.sessionTtlMs, "sessionTtlMs");
2996
+ const exchangeExpiresAt = computeExpiresAt(
2997
+ now,
2998
+ options.exchangeCodeTtlMs ?? DEFAULT_EXCHANGE_CODE_TTL_MS,
2999
+ "exchangeCodeTtlMs"
3000
+ );
3001
+ const sessionId = createOpaqueId("sess");
3002
+ const provider = Object.freeze({
1230
3003
  issuer: metadata.issuer ?? (typeof claims.iss === "string" ? claims.iss : void 0),
1231
3004
  clientId: metadata.clientId
1232
- },
1233
- refreshToken: tokenResponse.refresh_token,
1234
- idToken: tokenResponse.id_token,
1235
- accessToken: isString(tokenResponse.access_token) ? tokenResponse.access_token : void 0,
1236
- scope: typeof tokenResponse.scope === "string" ? tokenResponse.scope : metadata.scopes,
1237
- createdAt: now,
1238
- updatedAt: now,
1239
- user: mergeUserProfile(subject, claims, userInfo),
1240
- metadata: {
1241
- tokenType: tokenResponse.token_type
1242
- }
1243
- };
1244
- await callHook("callback", options.hooks?.onBeforeSessionCreate, req, res, session, { subject });
1245
- const createdSession = await options.storeProvider.createSession(session);
1246
- const exchangeCode = createOpaqueId("code");
1247
- try {
1248
- await options.storeProvider.createExchangeCode({
1249
- code: exchangeCode,
1250
- sessionId: createdSession.sessionId,
1251
- returnTo: transaction.returnTo,
1252
- createdAt: now,
1253
- expiresAt: now + (options.exchangeCodeTtlMs ?? DEFAULT_EXCHANGE_CODE_TTL_MS)
1254
3005
  });
3006
+ const sessionBinding = transaction.deviceBinding;
3007
+ const recognition = transaction.fingerprintRecognition;
3008
+ const session = {
3009
+ sessionId,
3010
+ logicalSessionId: sessionId,
3011
+ subject,
3012
+ providerSessionId: typeof claims.sid === "string" ? claims.sid : void 0,
3013
+ provider: { ...provider },
3014
+ ...sessionBinding === void 0 ? {} : { deviceBinding: { ...sessionBinding } },
3015
+ refreshToken: tokenResponse.refresh_token,
3016
+ idToken: tokenResponse.id_token,
3017
+ accessToken: isString(tokenResponse.access_token) ? tokenResponse.access_token : void 0,
3018
+ scope: typeof tokenResponse.scope === "string" ? tokenResponse.scope : metadata.scopes,
3019
+ createdAt: now,
3020
+ updatedAt: now,
3021
+ ...sessionExpiresAt === void 0 ? {} : { expiresAt: sessionExpiresAt },
3022
+ user: copyVaultUserProfile(mergeUserProfile(subject, claims, userInfo)),
3023
+ metadata: withFingerprintRecognitionMetadata({ tokenType: tokenResponse.token_type }, recognition)
3024
+ };
3025
+ await callHook("callback", options.hooks?.onBeforeSessionCreate, req, res, session, { subject });
3026
+ if (sessionExpiresAt !== void 0) {
3027
+ session.expiresAt = isUsableEpochMs(session.expiresAt) ? Math.min(session.expiresAt, sessionExpiresAt) : sessionExpiresAt;
3028
+ }
3029
+ const guarded = transaction.browserBindingHash !== void 0;
3030
+ const restoredBinding = guarded ? withSessionDeviceBinding(
3031
+ {
3032
+ ...copySessionApplicationFields(session),
3033
+ sessionId,
3034
+ logicalSessionId: sessionId,
3035
+ provider: { ...provider }
3036
+ },
3037
+ sessionBinding
3038
+ ) : withSessionDeviceBinding(session, sessionBinding);
3039
+ const restored = {
3040
+ ...restoredBinding,
3041
+ user: copyVaultUserProfile(restoredBinding.user),
3042
+ metadata: withFingerprintRecognitionMetadata(restoredBinding.metadata, recognition)
3043
+ };
3044
+ const createdSession = await store.createSession(restored);
3045
+ const createdSessionId = guarded ? sessionId : createdSession.sessionId;
3046
+ const logicalSessionId = guarded ? sessionId : createdSession.logicalSessionId ?? createdSessionId;
3047
+ const exchangeCode = createOpaqueId("code");
3048
+ try {
3049
+ const returnedBinding = snapshotDpopBinding(createdSession.deviceBinding);
3050
+ if (returnedBinding?.jkt !== sessionBinding?.jkt) throw invalidAuthorizationState();
3051
+ if (!fingerprintRecognitionMatches(snapshotFingerprintRecognition(createdSession.metadata), recognition))
3052
+ throw invalidAuthorizationState();
3053
+ if (guarded && (createdSession.sessionId !== sessionId || createdSession.logicalSessionId !== sessionId || createdSession.provider?.issuer !== provider.issuer || createdSession.provider?.clientId !== provider.clientId)) {
3054
+ throw invalidAuthorizationState();
3055
+ }
3056
+ await store.createExchangeCode({
3057
+ code: exchangeCode,
3058
+ sessionId: createdSessionId,
3059
+ returnTo: transaction.returnTo,
3060
+ ...sessionBinding === void 0 ? {} : { deviceBinding: sessionBinding },
3061
+ ...transaction.browserBindingHash === void 0 ? {} : { browserBindingHash: transaction.browserBindingHash },
3062
+ createdAt: now,
3063
+ expiresAt: exchangeExpiresAt
3064
+ });
3065
+ } catch (error) {
3066
+ await store.deleteSessionsByLogicalSessionId({ logicalSessionId });
3067
+ throw error;
3068
+ }
3069
+ await callPostCommitHook(
3070
+ "callback",
3071
+ options,
3072
+ options.hooks?.onSessionCreated,
3073
+ req,
3074
+ res,
3075
+ withSessionDeviceBinding(copyVaultSession(createdSession), sessionBinding),
3076
+ { subject }
3077
+ );
3078
+ if (cookieSecret !== void 0) {
3079
+ setTransactionCookie(res, cookie, cookieSecret, exchangeExpiresAt, getNow(options));
3080
+ }
3081
+ applyCredentialResponseCachePolicy(res);
3082
+ res.redirect(302, appendCodeToRedirectUri(frontendDestination, exchangeCode));
1255
3083
  } catch (error) {
1256
- await options.storeProvider.deleteSessionsByLogicalSessionId({
1257
- logicalSessionId: createdSession.logicalSessionId ?? createdSession.sessionId
1258
- });
3084
+ if (cookieSecret !== void 0) clearTransactionCookie(res, cookie);
1259
3085
  throw error;
1260
3086
  }
1261
- await callPostCommitHook("callback", options, options.hooks?.onSessionCreated, req, res, createdSession, {
1262
- subject
1263
- });
1264
- res.redirect(302, appendCodeToRedirectUri(resolveFrontendRedirectUri2(transaction, options), exchangeCode));
1265
3087
  });
1266
- var createExchangeHandler = (options) => createAsyncHandler("exchange", options, async (req, res) => {
1267
- const body = getBody(req);
1268
- const code = getRequiredString(body.code, "Exchange request is missing code.", "OIDC_VAULT_MISSING_EXCHANGE_CODE");
1269
- const exchangeRecord = await options.storeProvider.consumeExchangeCode(code);
1270
- if (!exchangeRecord) {
1271
- throw new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_EXCHANGE_CODE", "Exchange code is invalid or expired.");
3088
+ var assertLoginInitiationRequest = (req, options, trustedOrigins) => {
3089
+ assertTrustedOrigin(req, options, trustedOrigins, "login");
3090
+ if (!req.is("application/json")) {
3091
+ throw new OidcVaultHttpError(
3092
+ 415,
3093
+ "OIDC_VAULT_UNSUPPORTED_REQUEST_BODY_TYPE",
3094
+ "Login initiation requires a JSON request body."
3095
+ );
1272
3096
  }
1273
- const session = await options.storeProvider.getSession(exchangeRecord.sessionId);
1274
- if (!session) {
1275
- throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
3097
+ };
3098
+ var copySessionApplicationFields = (session) => {
3099
+ const result = {};
3100
+ for (const key of Object.keys(session)) {
3101
+ if (["deviceBinding", "browserBindingHash", "sessionId", "logicalSessionId", "provider"].includes(key)) continue;
3102
+ Object.defineProperty(result, key, {
3103
+ value: Reflect.get(session, key),
3104
+ enumerable: true,
3105
+ configurable: true,
3106
+ writable: true
3107
+ });
1276
3108
  }
1277
- let issuedToken;
3109
+ return result;
3110
+ };
3111
+ var createExchangeHandler = (options, config, trustedOrigins, proofVerifier) => createAsyncHandler("exchange", options, async (req, res) => {
3112
+ const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req);
3113
+ const capturedRecognition = captureFingerprintRecognition(req, options.fingerprintRecognition);
3114
+ const assertExchangeOrigin = captureTrustedOriginGuard(req, options, trustedOrigins, "exchange");
3115
+ const cookieHeader = req.headers.cookie;
3116
+ const body = getBody(req);
3117
+ const code = getRequiredString(body.code, "Exchange request is missing code.", "OIDC_VAULT_MISSING_EXCHANGE_CODE");
3118
+ const store = options.storeProvider;
3119
+ const canPreflight = typeof store.getExchangeCode === "function";
3120
+ if ((proofVerifier !== void 0 || options.fingerprintRecognition !== void 0) && (!canPreflight || typeof store.consumeExchangeCodeIfMatches !== "function")) {
3121
+ throw new TypeError("Device-bound exchange requires guarded store reads and atomic consumption.");
3122
+ }
3123
+ const rawCode = canPreflight ? await store.getExchangeCode(code) : await store.consumeExchangeCode(code);
3124
+ if (!rawCode) throw invalidExchangeCode();
3125
+ const record = snapshotVaultExchangeCode(rawCode, code, getNow(options));
3126
+ const guarded = record.browserBindingHash !== void 0;
3127
+ if (guarded) assertExchangeOrigin();
3128
+ assertDeviceBindingPolicy(options.deviceBinding, record.deviceBinding);
3129
+ const rawSession = await store.getSession(record.sessionId);
3130
+ if (!rawSession) throw invalidVaultSession();
3131
+ const session = snapshotVaultSession(rawSession, record.sessionId, getNow(options));
3132
+ assertDeviceBindingPolicy(options.deviceBinding, session.deviceBinding);
3133
+ assertVaultExchangeSessionBinding(record, session);
3134
+ const protectedExchange = proofVerifier !== void 0 || options.fingerprintRecognition !== void 0 || guarded;
3135
+ if (protectedExchange && typeof store.consumeExchangeCodeIfMatches !== "function") {
3136
+ throw new TypeError("Guarded exchange requires storeProvider.consumeExchangeCodeIfMatches.");
3137
+ }
3138
+ if (protectedExchange) assertVaultSessionIdentity(session, config);
3139
+ const cookieSecret = authenticateTransactionCookie(cookieHeader, options.transactionCookie, record.match);
3140
+ const recognition = options.fingerprintRecognition === void 0 ? void 0 : snapshotFingerprintRecognition(session.metadata);
3141
+ assertFingerprintRecognition(options.fingerprintRecognition, recognition, capturedRecognition);
3142
+ const verifiedBinding = proofVerifier === void 0 ? void 0 : await proofVerifier.verify(capturedProof, session.deviceBinding);
3143
+ if (canPreflight) {
3144
+ const consumed = protectedExchange ? await store.consumeExchangeCodeIfMatches({
3145
+ code,
3146
+ expectedSessionId: record.sessionId,
3147
+ match: record.match
3148
+ }) : await store.consumeExchangeCode(code);
3149
+ if (!consumed) throw invalidExchangeCode();
3150
+ const returned = snapshotVaultExchangeCode(consumed, code, getNow(options));
3151
+ assertVaultExchangeCodeMatches(returned, record);
3152
+ const returnedSession = await store.getSession(record.sessionId);
3153
+ if (!returnedSession) throw invalidVaultSession();
3154
+ const checkedSession = snapshotVaultSession(returnedSession, record.sessionId, getNow(options));
3155
+ assertVaultSessionIdentity(checkedSession, config);
3156
+ assertVaultExchangeSessionBinding(returned, checkedSession);
3157
+ assertVaultSessionMatches(checkedSession, session);
3158
+ if (options.fingerprintRecognition !== void 0 && !fingerprintRecognitionMatches(snapshotFingerprintRecognition(checkedSession.metadata), recognition))
3159
+ throw invalidVaultSession();
3160
+ }
3161
+ assertVaultSessionIdentity(session, config);
1278
3162
  try {
1279
- issuedToken = await withIssuedToken(req, res, options, session);
3163
+ const issuedToken = await withIssuedToken(req, res, options, session, verifiedBinding);
3164
+ if (usesCookieTransport(options)) setSessionCookie(res, options, session.sessionId);
3165
+ if (cookieSecret !== void 0) clearTransactionCookie(res, options.transactionCookie);
3166
+ applyCredentialResponseCachePolicy(res);
3167
+ res.status(200).json(createExchangeResponse(options, session, issuedToken));
1280
3168
  } catch (error) {
1281
- await options.storeProvider.deleteSessionsByLogicalSessionId({
1282
- logicalSessionId: session.logicalSessionId ?? session.sessionId
1283
- });
1284
- if (usesCookieTransport(options)) {
1285
- clearSessionCookie(res, options);
1286
- }
3169
+ if (cookieSecret !== void 0) clearTransactionCookie(res, options.transactionCookie);
1287
3170
  throw error;
1288
3171
  }
1289
- if (usesCookieTransport(options)) {
1290
- setSessionCookie(res, options, session.sessionId);
1291
- }
1292
- const response = createExchangeResponse(options, session, issuedToken);
1293
- res.status(200).json(response);
1294
3172
  });
1295
- var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandler("refresh", options, async (req, res) => {
3173
+ var createRefreshHandler = (options, config, trustedOrigins, proofVerifier) => createAsyncHandler("refresh", options, async (req, res) => {
3174
+ const capturedRecognition = captureFingerprintRecognition(req, options.fingerprintRecognition);
1296
3175
  assertTrustedOrigin(req, options, trustedOrigins, "refresh");
3176
+ const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req);
1297
3177
  const sessionId = getSessionIdFromRequest(req, options, "refresh");
1298
- const currentSession = await options.storeProvider.getSession(sessionId);
1299
- if (!currentSession) {
3178
+ const currentRecord = await options.storeProvider.getSession(sessionId);
3179
+ if (!currentRecord) {
1300
3180
  if (usesCookieTransport(options)) {
1301
3181
  clearSessionCookie(res, options);
1302
3182
  }
1303
3183
  throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
1304
3184
  }
3185
+ const currentSession = snapshotVaultSession(currentRecord, sessionId, getNow(options));
3186
+ assertVaultSessionIdentity(currentSession, config);
3187
+ assertDeviceBindingPolicy(options.deviceBinding, currentSession.deviceBinding);
3188
+ const recognition = options.fingerprintRecognition === void 0 ? void 0 : snapshotFingerprintRecognition(currentSession.metadata);
3189
+ assertFingerprintRecognition(options.fingerprintRecognition, recognition, capturedRecognition);
3190
+ const verifiedBinding = proofVerifier === void 0 ? void 0 : await proofVerifier.verify(capturedProof, currentSession.deviceBinding);
1305
3191
  const metadata = await resolveProviderMetadata(config, options);
3192
+ if (proofVerifier !== void 0 || options.fingerprintRecognition !== void 0) {
3193
+ const latest = await options.storeProvider.getSession(sessionId);
3194
+ if (!latest) throw invalidVaultSession();
3195
+ const latestSession = snapshotVaultSession(latest, sessionId, getNow(options));
3196
+ assertVaultSessionMatches(latestSession, currentSession);
3197
+ if (options.fingerprintRecognition !== void 0 && !fingerprintRecognitionMatches(snapshotFingerprintRecognition(latestSession.metadata), recognition))
3198
+ throw invalidVaultSession();
3199
+ }
1306
3200
  const tokenResponse = await requestToken(
1307
3201
  metadata,
1308
3202
  {
@@ -1311,39 +3205,40 @@ var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandl
1311
3205
  },
1312
3206
  options
1313
3207
  );
1314
- validateTokenResponse(tokenResponse);
3208
+ validateRefreshTokenResponse(tokenResponse);
1315
3209
  const newIdToken = isString(tokenResponse.id_token) ? tokenResponse.id_token : void 0;
1316
3210
  const idToken = newIdToken ?? currentSession.idToken;
1317
- const claims = newIdToken ? await verifyIdToken(metadata, newIdToken, void 0, options) : currentSession.user ?? { sub: currentSession.subject };
1318
- const subject = newIdToken ? getRequiredString(claims.sub, "OIDC id_token is missing sub.", "OIDC_VAULT_INVALID_ID_TOKEN", 502) : currentSession.subject;
3211
+ const freshClaims = newIdToken ? await verifyIdToken(metadata, newIdToken, void 0, options) : void 0;
3212
+ const subject = freshClaims ? getRequiredString(freshClaims.sub, "OIDC id_token is missing sub.", "OIDC_VAULT_INVALID_ID_TOKEN", 502) : currentSession.subject;
1319
3213
  if (newIdToken && subject !== currentSession.subject) {
1320
3214
  throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_ID_TOKEN", "OIDC refreshed id_token sub changed.");
1321
3215
  }
1322
3216
  const userInfo = shouldFetchUserInfo(options, metadata) && isString(tokenResponse.access_token) ? await fetchUserInfo(metadata, tokenResponse.access_token, options) : void 0;
1323
- if (userInfo) {
3217
+ if (userInfo !== void 0) {
1324
3218
  assertUserInfoSubject(userInfo, subject);
1325
3219
  }
1326
3220
  const now = getNow(options);
1327
3221
  const logicalSessionId = currentSession.logicalSessionId ?? currentSession.sessionId;
1328
3222
  const nextSession = {
1329
- ...currentSession,
3223
+ ...copyVaultSession(currentSession),
1330
3224
  sessionId: createOpaqueId("sess"),
1331
3225
  logicalSessionId,
1332
3226
  subject,
1333
- providerSessionId: typeof claims.sid === "string" ? claims.sid : currentSession.providerSessionId,
3227
+ providerSessionId: freshClaims && typeof freshClaims.sid === "string" ? freshClaims.sid : currentSession.providerSessionId,
1334
3228
  refreshToken: isString(tokenResponse.refresh_token) ? tokenResponse.refresh_token : currentSession.refreshToken,
1335
3229
  idToken,
1336
3230
  accessToken: isString(tokenResponse.access_token) ? tokenResponse.access_token : currentSession.accessToken,
1337
3231
  scope: typeof tokenResponse.scope === "string" ? tokenResponse.scope : currentSession.scope,
1338
3232
  expiresAt: currentSession.expiresAt,
1339
3233
  updatedAt: now,
1340
- user: mergeUserProfile(subject, claims, userInfo ?? currentSession.user)
3234
+ user: copyVaultUserProfile(composeRefreshedUserProfile(subject, currentSession.user, freshClaims, userInfo))
1341
3235
  };
1342
- let rotatedSession;
3236
+ const expectedSession = snapshotVaultSession(nextSession, nextSession.sessionId, getNow(options));
3237
+ let rotatedRecord;
1343
3238
  try {
1344
- rotatedSession = await options.storeProvider.rotateSession({
3239
+ rotatedRecord = await options.storeProvider.rotateSession({
1345
3240
  sessionId: currentSession.sessionId,
1346
- nextSession
3241
+ nextSession: copyVaultSession(expectedSession)
1347
3242
  });
1348
3243
  } catch (error) {
1349
3244
  if (isStoreConflictError(error)) {
@@ -1354,51 +3249,127 @@ var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandl
1354
3249
  }
1355
3250
  throw error;
1356
3251
  }
1357
- let issuedToken;
1358
3252
  try {
1359
- issuedToken = await withIssuedToken(req, res, options, rotatedSession);
3253
+ const returnedSession = snapshotVaultSession(rotatedRecord, expectedSession.sessionId, getNow(options));
3254
+ assertVaultSessionIdentity(returnedSession, config);
3255
+ assertVaultSessionMatches(returnedSession, expectedSession);
3256
+ if (options.fingerprintRecognition !== void 0 && !fingerprintRecognitionMatches(snapshotFingerprintRecognition(returnedSession.metadata), recognition))
3257
+ throw invalidVaultSession();
1360
3258
  } catch (error) {
1361
- await options.storeProvider.deleteSessionsByLogicalSessionId({
1362
- logicalSessionId: rotatedSession.logicalSessionId ?? rotatedSession.sessionId
1363
- });
1364
- if (usesCookieTransport(options)) {
1365
- clearSessionCookie(res, options);
1366
- }
3259
+ await options.storeProvider.deleteSessionsByLogicalSessionId({ logicalSessionId });
3260
+ if (usesCookieTransport(options)) clearSessionCookie(res, options);
1367
3261
  throw error;
1368
3262
  }
1369
- await callPostCommitHook("refresh", options, options.hooks?.onSessionRefreshed, req, res, rotatedSession, {
1370
- previousSessionId: currentSession.sessionId
1371
- });
3263
+ const rotatedSession = expectedSession;
3264
+ const issuedToken = await withIssuedToken(req, res, options, rotatedSession, verifiedBinding);
3265
+ await callPostCommitHook(
3266
+ "refresh",
3267
+ options,
3268
+ options.hooks?.onSessionRefreshed,
3269
+ req,
3270
+ res,
3271
+ copyVaultSession(rotatedSession),
3272
+ {
3273
+ previousSessionId: currentSession.sessionId
3274
+ }
3275
+ );
1372
3276
  if (usesCookieTransport(options)) {
1373
3277
  setSessionCookie(res, options, rotatedSession.sessionId);
1374
3278
  }
1375
3279
  const response = createExchangeResponse(options, rotatedSession, issuedToken);
3280
+ applyCredentialResponseCachePolicy(res);
1376
3281
  res.status(200).json(response);
1377
3282
  });
1378
- var createLogoutHandler = (options, config, trustedOrigins) => createAsyncHandler("logout", options, async (req, res) => {
3283
+ var createLogoutHandler = (options, config, trustedOrigins, proofVerifier) => createAsyncHandler("logout", options, async (req, res) => {
1379
3284
  assertTrustedOrigin(req, options, trustedOrigins, "logout");
3285
+ const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req);
1380
3286
  const body = getBody(req);
1381
3287
  const sessionId = getSessionIdFromRequest(req, options, "logout");
1382
3288
  const redirect = body.redirect === true;
1383
- const session = await options.storeProvider.getSession(sessionId);
1384
- if (!session) {
1385
- await options.storeProvider.deleteSession(sessionId);
1386
- if (usesCookieTransport(options)) {
1387
- clearSessionCookie(res, options);
1388
- }
3289
+ const store = options.storeProvider;
3290
+ const hasContext = typeof store.getSessionRevocationContext === "function";
3291
+ if (proofVerifier !== void 0 && !hasContext) {
3292
+ throw new TypeError("Device-bound logout requires storeProvider.getSessionRevocationContext.");
3293
+ }
3294
+ const rawContext = hasContext ? await store.getSessionRevocationContext(sessionId) : void 0;
3295
+ const context = rawContext ? snapshotVaultRevocationContext(rawContext) : void 0;
3296
+ const rawSession = await store.getSession(sessionId);
3297
+ const session = rawSession ? snapshotVaultSession(rawSession, sessionId, getNow(options)) : void 0;
3298
+ const finishLocalLogout = () => {
3299
+ if (usesCookieTransport(options)) clearSessionCookie(res, options);
3300
+ applyCredentialResponseCachePolicy(res);
1389
3301
  res.status(200).json({ loggedOut: true });
3302
+ };
3303
+ if (hasContext && context === void 0) {
3304
+ if (session !== void 0) {
3305
+ assertDeviceBindingPolicy(options.deviceBinding, session.deviceBinding);
3306
+ throw invalidVaultSession();
3307
+ }
3308
+ finishLocalLogout();
1390
3309
  return;
1391
3310
  }
1392
- await callHook("logout", options.hooks?.onBeforeLogout, req, res, session, void 0);
1393
- await options.storeProvider.deleteSessionsByLogicalSessionId({
1394
- logicalSessionId: session.logicalSessionId ?? session.sessionId
1395
- });
3311
+ const authority = context ?? (session === void 0 ? void 0 : snapshotVaultRevocationContext({
3312
+ logicalSessionId: session.logicalSessionId ?? session.sessionId,
3313
+ provider: session.provider,
3314
+ deviceBinding: session.deviceBinding
3315
+ }));
3316
+ if (authority === void 0) {
3317
+ await store.deleteSession(sessionId);
3318
+ finishLocalLogout();
3319
+ return;
3320
+ }
3321
+ assertVaultSessionIdentity(authority, config);
3322
+ assertDeviceBindingPolicy(options.deviceBinding, authority.deviceBinding);
3323
+ if (session !== void 0)
3324
+ assertVaultRevocationContextMatches(
3325
+ snapshotVaultRevocationContext({
3326
+ logicalSessionId: session.logicalSessionId ?? session.sessionId,
3327
+ provider: session.provider,
3328
+ deviceBinding: session.deviceBinding
3329
+ }),
3330
+ authority
3331
+ );
3332
+ if (proofVerifier !== void 0) await proofVerifier.verify(capturedProof, authority.deviceBinding);
3333
+ if (hasContext) {
3334
+ const latest = await store.getSessionRevocationContext(sessionId);
3335
+ if (!latest) {
3336
+ finishLocalLogout();
3337
+ return;
3338
+ }
3339
+ assertVaultRevocationContextMatches(snapshotVaultRevocationContext(latest), authority);
3340
+ }
3341
+ if (session !== void 0)
3342
+ await callHook("logout", options.hooks?.onBeforeLogout, req, res, copyVaultSession(session));
3343
+ await store.deleteSessionsByLogicalSessionId({ logicalSessionId: authority.logicalSessionId });
1396
3344
  if (usesCookieTransport(options)) {
1397
3345
  clearSessionCookie(res, options);
1398
3346
  }
1399
- const metadata = await resolveProviderMetadata(config, options);
1400
- const upstreamLogoutUrl = metadata.endSessionEndpoint ? buildLogoutUrl(metadata.endSessionEndpoint, session.idToken, options.postLogoutRedirectUri) : void 0;
1401
- await callPostCommitHook("logout", options, options.hooks?.onLogout, req, res, session);
3347
+ if (session === void 0) {
3348
+ applyCredentialResponseCachePolicy(res);
3349
+ res.status(200).json({ loggedOut: true });
3350
+ return;
3351
+ }
3352
+ if (!redirect) {
3353
+ await callPostCommitHook("logout", options, options.hooks?.onLogout, req, res, copyVaultSession(session));
3354
+ applyCredentialResponseCachePolicy(res);
3355
+ res.status(200).json({ loggedOut: true });
3356
+ return;
3357
+ }
3358
+ let upstreamLogoutUrl;
3359
+ try {
3360
+ const metadata = await resolveProviderMetadata(config, options);
3361
+ upstreamLogoutUrl = metadata.endSessionEndpoint ? buildLogoutUrl(metadata.endSessionEndpoint, session.idToken, options.postLogoutRedirectUri) : void 0;
3362
+ } catch (error) {
3363
+ try {
3364
+ await options.hooks?.onError?.({
3365
+ ...createHookContext("logout", req, res, copyVaultSession(session)),
3366
+ error
3367
+ });
3368
+ } catch {
3369
+ }
3370
+ }
3371
+ await callPostCommitHook("logout", options, options.hooks?.onLogout, req, res, copyVaultSession(session));
3372
+ applyCredentialResponseCachePolicy(res);
1402
3373
  if (redirect && upstreamLogoutUrl) {
1403
3374
  res.redirect(302, upstreamLogoutUrl);
1404
3375
  return;
@@ -1411,26 +3382,40 @@ var createBackchannelLogoutHandler = (options, config) => createAsyncHandler("ba
1411
3382
  const metadata = await resolveProviderMetadata(config, options);
1412
3383
  const logoutToken = getLogoutTokenFromRequest(req);
1413
3384
  const claims = await verifyBackchannelLogoutToken(metadata, logoutToken, options);
3385
+ const replayKey = buildBackchannelLogoutReplayKey({
3386
+ issuer: metadata.issuer,
3387
+ clientId: metadata.clientId,
3388
+ jti: claims.jti
3389
+ });
1414
3390
  const firstUse = await options.storeProvider.consumeBackchannelLogoutTokenJti({
1415
- jti: claims.jti,
3391
+ jti: replayKey,
1416
3392
  expiresAt: claims.exp * 1e3
1417
3393
  });
1418
- if (!firstUse) {
1419
- res.status(200).json({
1420
- loggedOut: true,
1421
- revokedSessions: 0
1422
- });
1423
- return;
1424
- }
1425
- const revokedSessions = isString(claims.sid) ? await options.storeProvider.deleteSessionsByProviderSessionId({
3394
+ const revokeMatchingSessions = () => isString(claims.sid) ? options.storeProvider.deleteSessionsByProviderSessionId({
1426
3395
  providerSessionId: claims.sid,
1427
3396
  issuer: metadata.issuer,
1428
3397
  clientId: metadata.clientId
1429
- }) : await options.storeProvider.deleteSessionsBySubject({
3398
+ }) : options.storeProvider.deleteSessionsBySubject({
1430
3399
  subject: String(claims.sub),
1431
3400
  issuer: metadata.issuer,
1432
3401
  clientId: metadata.clientId
1433
3402
  });
3403
+ if (!firstUse) {
3404
+ const catchUpRevokedSessions = await revokeMatchingSessions();
3405
+ if (catchUpRevokedSessions > 0) {
3406
+ await callPostCommitHook("backchannel-logout", options, options.hooks?.onLogout, req, res, void 0, {
3407
+ providerSessionId: claims.sid,
3408
+ subject: claims.sub,
3409
+ revokedSessions: catchUpRevokedSessions
3410
+ });
3411
+ }
3412
+ res.status(200).json({
3413
+ loggedOut: true,
3414
+ revokedSessions: catchUpRevokedSessions
3415
+ });
3416
+ return;
3417
+ }
3418
+ const revokedSessions = await revokeMatchingSessions();
1434
3419
  await callPostCommitHook("backchannel-logout", options, options.hooks?.onLogout, req, res, void 0, {
1435
3420
  providerSessionId: claims.sid,
1436
3421
  subject: claims.sub,
@@ -1442,19 +3427,39 @@ var createBackchannelLogoutHandler = (options, config) => createAsyncHandler("ba
1442
3427
  });
1443
3428
  });
1444
3429
  function registerRoutes(router, options, config, trustedOrigins, backendOrigin, basePath) {
3430
+ const proofVerifier = createVaultRouteProofVerifier(options, config, backendOrigin, basePath);
1445
3431
  router.get(OIDC_VAULT_ROUTE_PATHS.login, createLoginHandler(options, config, backendOrigin, basePath));
3432
+ if (proofVerifier !== void 0 || options.fingerprintRecognition !== void 0) {
3433
+ router.post(
3434
+ OIDC_VAULT_ROUTE_PATHS.login,
3435
+ createLoginInitiationHandler(options, config, trustedOrigins, backendOrigin, basePath, proofVerifier)
3436
+ );
3437
+ }
1446
3438
  router.get(OIDC_VAULT_ROUTE_PATHS.callback, createCallbackHandler(options, config, backendOrigin, basePath));
1447
- router.post(OIDC_VAULT_ROUTE_PATHS.exchange, createExchangeHandler(options));
1448
- router.post(OIDC_VAULT_ROUTE_PATHS.refresh, createRefreshHandler(options, config, trustedOrigins));
1449
- router.post(OIDC_VAULT_ROUTE_PATHS.logout, createLogoutHandler(options, config, trustedOrigins));
3439
+ router.post(OIDC_VAULT_ROUTE_PATHS.exchange, createExchangeHandler(options, config, trustedOrigins, proofVerifier));
3440
+ router.post(OIDC_VAULT_ROUTE_PATHS.refresh, createRefreshHandler(options, config, trustedOrigins, proofVerifier));
3441
+ router.post(OIDC_VAULT_ROUTE_PATHS.logout, createLogoutHandler(options, config, trustedOrigins, proofVerifier));
1450
3442
  router.post(OIDC_VAULT_ROUTE_PATHS["backchannel-logout"], createBackchannelLogoutHandler(options, config));
1451
3443
  }
1452
3444
  function createOidcVaultMiddleware(options) {
1453
- const { backendOrigin, config, trustedOrigins } = validateOidcVaultOptions(options);
3445
+ const { backendOrigin, config, trustedOrigins, resolvedOptions } = validateOidcVaultOptions(options);
1454
3446
  const rootRouter = express.Router();
1455
3447
  const baseRouter = express.Router();
1456
- const basePath = normalizeOidcVaultBasePath(options.basePath);
1457
- const requestBodyLimit = options.requestBodyLimit ?? DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT;
3448
+ const basePath = normalizeOidcVaultBasePath(resolvedOptions.basePath);
3449
+ const requestBodyLimit = resolvedOptions.requestBodyLimit ?? DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT;
3450
+ baseRouter.use((_req, res, next) => {
3451
+ applyCredentialResponseCachePolicy(res);
3452
+ next();
3453
+ });
3454
+ if (resolvedOptions.deviceBinding !== void 0 || resolvedOptions.fingerprintRecognition !== void 0) {
3455
+ baseRouter.post(
3456
+ OIDC_VAULT_ROUTE_PATHS.login,
3457
+ createAsyncHandler("login", resolvedOptions, async (req, res, next) => {
3458
+ assertLoginInitiationRequest(req, resolvedOptions, trustedOrigins);
3459
+ next();
3460
+ })
3461
+ );
3462
+ }
1458
3463
  baseRouter.use(express.json({ limit: requestBodyLimit }));
1459
3464
  baseRouter.use(
1460
3465
  express.urlencoded({
@@ -1464,7 +3469,7 @@ function createOidcVaultMiddleware(options) {
1464
3469
  })
1465
3470
  );
1466
3471
  baseRouter.use(createBodyParserErrorHandler());
1467
- registerRoutes(baseRouter, options, config, trustedOrigins, backendOrigin, basePath);
3472
+ registerRoutes(baseRouter, resolvedOptions, config, trustedOrigins, backendOrigin, basePath);
1468
3473
  rootRouter.use(basePath, baseRouter);
1469
3474
  return rootRouter;
1470
3475
  }
@@ -1476,6 +3481,7 @@ export {
1476
3481
  DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT,
1477
3482
  OIDC_VAULT_ROUTE_PATHS,
1478
3483
  OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT,
3484
+ OidcVaultDpopReplayCapacityError,
1479
3485
  OidcVaultStoreConflictError,
1480
3486
  createOidcVaultAccessTokenMiddleware,
1481
3487
  createOidcVaultJwtAccessTokenValidator,