@passflow/node 0.0.0-bootstrap.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,558 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_evaluate = require("./evaluate-C20MecTf.cjs");
3
+ let jose = require("jose");
4
+ // src/header.ts
5
+ const ALLOWED_ALGORITHMS = ["RS256", "ES256"];
6
+ const UTF8 = new TextDecoder("utf-8", {
7
+ fatal: true,
8
+ ignoreBOM: true
9
+ });
10
+ const BASE64URL$1 = /^[A-Za-z0-9_-]+$/;
11
+ function decodeProtectedHeader(segment) {
12
+ if (!BASE64URL$1.test(segment) || segment.length % 4 === 1) return;
13
+ try {
14
+ const decoded = JSON.parse(UTF8.decode(jose.base64url.decode(segment)));
15
+ return typeof decoded === "object" && decoded !== null && !Array.isArray(decoded) ? decoded : void 0;
16
+ } catch {
17
+ return;
18
+ }
19
+ }
20
+ /**
21
+ * Header inspection, in the order Go and the corpus pin, before any key lookup: malformed, alg, crit,
22
+ * kid. It has to run before jose, which reports a standard parameter in `crit` with the same code as a
23
+ * malformed token. Throws `PassflowAuthError` with `invalid_token` and the reason.
24
+ */
25
+ function inspectHeader(token) {
26
+ const segments = token.split(".");
27
+ const protectedSegment = segments[0];
28
+ if (segments.length !== 3 || protectedSegment === void 0) throw new require_evaluate.PassflowAuthError("invalid_token", "malformed");
29
+ const header = decodeProtectedHeader(protectedSegment);
30
+ if (header === void 0) throw new require_evaluate.PassflowAuthError("invalid_token", "malformed");
31
+ if (typeof header.alg !== "string" || !ALLOWED_ALGORITHMS.includes(header.alg)) throw new require_evaluate.PassflowAuthError("invalid_token", "alg");
32
+ if (Object.hasOwn(header, "crit")) throw new require_evaluate.PassflowAuthError("invalid_token", "crit");
33
+ if (typeof header.kid !== "string" || header.kid === "") throw new require_evaluate.PassflowAuthError("invalid_token", "kid_missing");
34
+ }
35
+ // src/jwks.ts
36
+ /** One failed reload attempt. Internal, and deliberately holds no URL and no underlying error. */
37
+ var JWKSFetchError = class extends Error {
38
+ stage;
39
+ constructor(stage) {
40
+ super(`[passflow] key set reload failed at the ${stage} stage`);
41
+ this.name = "JWKSFetchError";
42
+ this.stage = stage;
43
+ }
44
+ };
45
+ /** No key set can be used right now: the verifier answers 503. */
46
+ var JWKSUnavailableError = class extends Error {
47
+ constructor(options) {
48
+ super("[passflow] key set unavailable", options);
49
+ this.name = "JWKSUnavailableError";
50
+ }
51
+ };
52
+ /** The key set was reachable and the token's key is not in it: the verifier answers 401. */
53
+ var KeyUnknownError = class extends Error {
54
+ constructor() {
55
+ super("[passflow] no key for the token");
56
+ this.name = "KeyUnknownError";
57
+ }
58
+ };
59
+ const ACCEPT = "application/json, application/jwk-set+json";
60
+ const ALG_FOR_KTY = {
61
+ RSA: "RS256",
62
+ EC: "ES256"
63
+ };
64
+ const PRIVATE_MEMBERS = [
65
+ "d",
66
+ "p",
67
+ "q",
68
+ "dp",
69
+ "dq",
70
+ "qi",
71
+ "oth"
72
+ ];
73
+ const BASE64URL = /^[A-Za-z0-9_-]+$/;
74
+ const MIN_RSA_MODULUS_BITS = 2048;
75
+ const MIN_RSA_EXPONENT = 3;
76
+ const MAX_RSA_EXPONENT = 2147483647;
77
+ const MAX_RSA_EXPONENT_BYTES = 4;
78
+ const P256_COORDINATE_BYTES = 32;
79
+ function isPlainObject(value) {
80
+ return typeof value === "object" && value !== null && !Array.isArray(value);
81
+ }
82
+ /** Go decodes `key_ops` into `[]string`, where a wrong member type is an error and a JSON null is an empty string. */
83
+ function isKeyOperation(value) {
84
+ return typeof value === "string" || value === null;
85
+ }
86
+ /**
87
+ * Usability is metadata alone and never fails a reload: a key rule 3 could never select is skipped.
88
+ */
89
+ function isUsableKey(jwk) {
90
+ const { kty, use, key_ops: keyOps, alg, kid } = jwk;
91
+ if (typeof kty !== "string" || !Object.hasOwn(ALG_FOR_KTY, kty)) return false;
92
+ if (use !== void 0 && use !== "sig") return false;
93
+ if (keyOps !== void 0 && !(Array.isArray(keyOps) && keyOps.every(isKeyOperation) && keyOps.includes("verify"))) return false;
94
+ if (alg !== void 0 && alg !== ALG_FOR_KTY[kty]) return false;
95
+ return typeof kid === "string" && kid !== "";
96
+ }
97
+ /**
98
+ * One key member, decoded strictly. WebCrypto's import is no check here: it imports a modulus that is
99
+ * not base64url with length 0, and reads a modulus with a stray trailing character as a longer one.
100
+ */
101
+ function decodeMember(value) {
102
+ if (typeof value !== "string" || !BASE64URL.test(value) || value.length % 4 === 1) throw new JWKSFetchError("key");
103
+ const decoded = jose.base64url.decode(value);
104
+ if (decoded.length === 0 || jose.base64url.encode(decoded) !== value) throw new JWKSFetchError("key");
105
+ return decoded;
106
+ }
107
+ /**
108
+ * Big-endian, odd and within [3, 2^31 - 1], as Go's `acceptRSA` requires. WebCrypto imports `e=1` and a
109
+ * token then verifies with no private key. Only significant bytes count, and there are few enough of
110
+ * them that a long `e` is refused before it could lose precision.
111
+ */
112
+ function isValidExponent(bytes) {
113
+ const first = bytes.findIndex((byte) => byte !== 0);
114
+ if (first === -1 || bytes.length - first > MAX_RSA_EXPONENT_BYTES) return false;
115
+ let exponent = 0;
116
+ for (const byte of bytes.subarray(first)) exponent = exponent * 256 + byte;
117
+ return exponent % 2 === 1 && exponent >= MIN_RSA_EXPONENT && exponent <= MAX_RSA_EXPONENT;
118
+ }
119
+ /**
120
+ * The public members of a usable key and nothing else. jose receives this form, so its own usability
121
+ * filter cannot disagree with ours over members such as `ext` or a duplicated `key_ops` entry.
122
+ */
123
+ function publicMembers(jwk) {
124
+ const { kty, kid } = jwk;
125
+ if (kty === "RSA") {
126
+ const { n, e } = jwk;
127
+ decodeMember(n);
128
+ if (!isValidExponent(decodeMember(e))) throw new JWKSFetchError("key");
129
+ return {
130
+ kty,
131
+ kid,
132
+ n,
133
+ e
134
+ };
135
+ }
136
+ const { crv, x, y } = jwk;
137
+ if (crv !== "P-256" || decodeMember(x).length !== P256_COORDINATE_BYTES || decodeMember(y).length !== P256_COORDINATE_BYTES) throw new JWKSFetchError("key");
138
+ return {
139
+ kty,
140
+ kid,
141
+ crv,
142
+ x,
143
+ y
144
+ };
145
+ }
146
+ /**
147
+ * Acceptance is key material, and one failure fails the whole reload. jose's local set imports lazily
148
+ * at resolution, so without this a malformed or private key would replace a working set and surface
149
+ * as a token defect. A publisher leaking private key material is not trusted at all: keeping the stale
150
+ * set and then answering 503 is the fail-closed response.
151
+ */
152
+ async function acceptKeys(keys) {
153
+ const usable = keys.filter(isUsableKey);
154
+ if (usable.length === 0) throw new JWKSFetchError("key");
155
+ const accepted = [];
156
+ const seen = /* @__PURE__ */ new Set();
157
+ for (const jwk of usable) {
158
+ if (PRIVATE_MEMBERS.some((member) => Object.hasOwn(jwk, member))) throw new JWKSFetchError("key");
159
+ const members = publicMembers(jwk);
160
+ const key = await (0, jose.importJWK)(members, ALG_FOR_KTY[members.kty]);
161
+ if (key instanceof Uint8Array || key.type !== "public") throw new JWKSFetchError("key");
162
+ if (members.kty === "RSA") {
163
+ const { modulusLength } = key.algorithm;
164
+ if (!(modulusLength >= MIN_RSA_MODULUS_BITS)) throw new JWKSFetchError("key");
165
+ } else if (key.algorithm.namedCurve !== "P-256") throw new JWKSFetchError("key");
166
+ if (seen.has(members.kid)) throw new JWKSFetchError("key");
167
+ seen.add(members.kid);
168
+ accepted.push(members);
169
+ }
170
+ return accepted;
171
+ }
172
+ function failureStage(error, fallback) {
173
+ if (error instanceof JWKSFetchError) return error;
174
+ const name = error?.name;
175
+ return new JWKSFetchError(name === "TimeoutError" || name === "AbortError" ? "timeout" : fallback);
176
+ }
177
+ /**
178
+ * Cleanup is best effort and never awaited: a stream whose cancel does not settle must not hold an attempt,
179
+ * and with it the single in-flight reload, open past the failure it is cleaning up after.
180
+ */
181
+ function cancelQuietly(stream) {
182
+ try {
183
+ stream?.cancel().catch(() => void 0);
184
+ } catch {}
185
+ }
186
+ /** A read that failed or lost to the deadline: the source is told to stop, and the body is not left locked. */
187
+ function abandonReader(reader) {
188
+ cancelQuietly(reader);
189
+ try {
190
+ reader.releaseLock();
191
+ } catch {}
192
+ }
193
+ async function readBody(response, maxBytes, abort) {
194
+ const declared = Number(response.headers.get("content-length"));
195
+ if (Number.isFinite(declared) && declared > maxBytes) {
196
+ cancelQuietly(response.body);
197
+ throw new JWKSFetchError("body");
198
+ }
199
+ if (response.body === null) return /* @__PURE__ */ new Uint8Array();
200
+ const reader = response.body.getReader();
201
+ const chunks = [];
202
+ let total = 0;
203
+ try {
204
+ for (;;) {
205
+ const { done, value } = await Promise.race([reader.read(), abort]);
206
+ if (done) break;
207
+ total += value.byteLength;
208
+ if (total > maxBytes) throw new JWKSFetchError("body");
209
+ chunks.push(value);
210
+ }
211
+ } catch (error) {
212
+ abandonReader(reader);
213
+ throw error;
214
+ }
215
+ const body = new Uint8Array(total);
216
+ let offset = 0;
217
+ for (const chunk of chunks) {
218
+ body.set(chunk, offset);
219
+ offset += chunk.byteLength;
220
+ }
221
+ return body;
222
+ }
223
+ function parseKeySet(body) {
224
+ let parsed;
225
+ try {
226
+ parsed = JSON.parse(new TextDecoder("utf-8", {
227
+ fatal: true,
228
+ ignoreBOM: true
229
+ }).decode(body));
230
+ } catch {
231
+ throw new JWKSFetchError("parse");
232
+ }
233
+ const keys = isPlainObject(parsed) ? parsed.keys : void 0;
234
+ if (!Array.isArray(keys) || !keys.every(isPlainObject)) throw new JWKSFetchError("parse");
235
+ return keys;
236
+ }
237
+ /**
238
+ * The fetch and cache half of a verifier, a port of the Go package's rules 1 to 6 over the injected or
239
+ * global `fetch` and the verifier's clock. jose owns the keys and the signature; it never touches the
240
+ * network. `createRemoteJWKSet` is not used: it reports retrieval failures with the same code as token
241
+ * defects, counts success where a wrapper cannot see it, and clocks its cache with `Date.now()`.
242
+ */
243
+ function createKeySet(options) {
244
+ const { url, now, maxAge, minRefreshInterval, maxStale, maxBytes, timeout } = options;
245
+ let local;
246
+ let lastSuccess;
247
+ let lastAttempt;
248
+ let lastAttemptFailed = false;
249
+ let lastFailure;
250
+ let inflight;
251
+ const unavailable = () => new JWKSUnavailableError(lastFailure === void 0 ? void 0 : { cause: lastFailure });
252
+ const resolver = (protectedHeader, token) => {
253
+ if (local === void 0) throw new JWKSUnavailableError();
254
+ return local(protectedHeader, token);
255
+ };
256
+ async function retrieve() {
257
+ const doFetch = options.fetch ?? globalThis.fetch;
258
+ let response;
259
+ let abort;
260
+ let fetched;
261
+ try {
262
+ const signal = AbortSignal.timeout(timeout);
263
+ abort = new Promise((_, reject) => {
264
+ signal.addEventListener("abort", () => reject(signal.reason), { once: true });
265
+ });
266
+ abort.catch(() => void 0);
267
+ fetched = doFetch(url, {
268
+ method: "GET",
269
+ redirect: "manual",
270
+ signal,
271
+ headers: { accept: ACCEPT }
272
+ });
273
+ response = await Promise.race([fetched, abort]);
274
+ } catch (error) {
275
+ fetched?.then((late) => cancelQuietly(late.body), () => void 0);
276
+ throw failureStage(error, "network");
277
+ }
278
+ if (response.status !== 200) {
279
+ cancelQuietly(response.body);
280
+ throw new JWKSFetchError("status");
281
+ }
282
+ let body;
283
+ try {
284
+ body = await readBody(response, maxBytes, abort);
285
+ } catch (error) {
286
+ throw failureStage(error, "body");
287
+ }
288
+ const keys = parseKeySet(body);
289
+ try {
290
+ return (0, jose.createLocalJWKSet)({ keys: await acceptKeys(keys) });
291
+ } catch (error) {
292
+ throw error instanceof JWKSFetchError ? error : new JWKSFetchError("key");
293
+ }
294
+ }
295
+ async function attempt() {
296
+ try {
297
+ const next = await retrieve();
298
+ const at = now();
299
+ local = next;
300
+ lastSuccess = at;
301
+ lastAttempt = at;
302
+ lastAttemptFailed = false;
303
+ lastFailure = void 0;
304
+ } catch (error) {
305
+ lastAttempt = now();
306
+ lastAttemptFailed = true;
307
+ lastFailure = failureStage(error, "network");
308
+ throw lastFailure;
309
+ }
310
+ }
311
+ function reload() {
312
+ if (inflight === void 0) inflight = attempt().finally(() => {
313
+ inflight = void 0;
314
+ });
315
+ return inflight;
316
+ }
317
+ const gateOpen = (at) => lastAttempt === void 0 || at - lastAttempt >= minRefreshInterval;
318
+ /**
319
+ * Rules 1 and 2: have a set that may be used, or say why not. `pending` is the reload this call waited
320
+ * for or left running in the background, which is the one fetch this verification may cost.
321
+ */
322
+ async function prepare() {
323
+ const at = now();
324
+ if (local === void 0) {
325
+ if (inflight === void 0 && !gateOpen(at)) throw unavailable();
326
+ const pending = reload();
327
+ await pending.catch(() => {
328
+ throw unavailable();
329
+ });
330
+ return { pending };
331
+ }
332
+ const age = at - lastSuccess;
333
+ if (maxAge !== 0 && !(age > maxAge)) return inflight === void 0 ? {} : { pending: inflight };
334
+ if (age > maxStale) {
335
+ if (inflight === void 0 && lastAttemptFailed && !gateOpen(at)) throw unavailable();
336
+ const pending = reload();
337
+ await pending.catch(() => void 0);
338
+ if (now() - lastSuccess > maxStale) throw unavailable();
339
+ return { pending };
340
+ }
341
+ const pending = inflight ?? (gateOpen(at) ? reload() : void 0);
342
+ pending?.catch(() => void 0);
343
+ return pending === void 0 ? {} : { pending };
344
+ }
345
+ /** Rule 4: returns when a reload succeeded, so the lookup is worth one retry. */
346
+ async function reloadForMissingKey() {
347
+ if (inflight === void 0 && !gateOpen(now())) {
348
+ if (lastAttemptFailed) throw unavailable();
349
+ throw new KeyUnknownError();
350
+ }
351
+ await reload().catch(() => {
352
+ throw unavailable();
353
+ });
354
+ }
355
+ const isNoMatchingKey = (error) => error instanceof jose.errors.JWKSNoMatchingKey;
356
+ return {
357
+ async withKeys(verify) {
358
+ const { pending } = await prepare();
359
+ const searched = local;
360
+ try {
361
+ return await verify(resolver);
362
+ } catch (error) {
363
+ if (!isNoMatchingKey(error)) throw error;
364
+ }
365
+ if (pending !== void 0) await pending.catch(() => {
366
+ throw unavailable();
367
+ });
368
+ else if (local === searched) await reloadForMissingKey();
369
+ try {
370
+ return await verify(resolver);
371
+ } catch (error) {
372
+ if (isNoMatchingKey(error)) throw new KeyUnknownError();
373
+ throw error;
374
+ }
375
+ },
376
+ async refresh() {
377
+ if (inflight !== void 0) await inflight.catch(() => void 0);
378
+ await reload().catch(() => {
379
+ throw unavailable();
380
+ });
381
+ }
382
+ };
383
+ }
384
+ // src/verifier.ts
385
+ const DEFAULT_LEEWAY_SECONDS = 30;
386
+ const DEFAULT_MAX_AGE = 6e5;
387
+ const DEFAULT_MIN_REFRESH_INTERVAL = 3e4;
388
+ const DEFAULT_MAX_STALE = 36e5;
389
+ const DEFAULT_MAX_BYTES = 1048576;
390
+ const DEFAULT_TIMEOUT = 5e3;
391
+ const MAX_TIMER_MS = 2147483647;
392
+ function isNonEmptyString(value) {
393
+ return typeof value === "string" && value !== "";
394
+ }
395
+ function requireIssuer(issuer) {
396
+ if (!isNonEmptyString(issuer)) throw new TypeError("[passflow] issuer must be a non-empty string");
397
+ return issuer;
398
+ }
399
+ function requireAudiences(audience) {
400
+ const audiences = typeof audience === "string" ? [audience] : audience;
401
+ if (!Array.isArray(audiences) || audiences.length === 0 || !audiences.every(isNonEmptyString)) throw new TypeError("[passflow] audience must name one or more non-empty audiences");
402
+ return [...audiences];
403
+ }
404
+ /** The host of an `http://` URL as written, without userinfo or port. A URL spelled any other way has none. */
405
+ const WRITTEN_HOST = /^http:\/\/(?:[^/?#\\\s]*@)?(\[[^\]]*\]|[^/?#:@\\%\s]*)(?::\d*)?(?:[/?#]|$)/i;
406
+ const LOOPBACK_IPV4 = /^127(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
407
+ /**
408
+ * Loopback by the host as written, the rule Go applies. WHATWG normalization would turn `127.1`, `2130706433`
409
+ * and `0x7f.1` into `127.0.0.1`, so a config that starts here would fail to start a Go API. An IPv6 literal
410
+ * takes the normalized form, which gives each address one spelling. A name that merely contains "localhost"
411
+ * is not loopback.
412
+ */
413
+ function isLoopbackHost(raw, url) {
414
+ const host = WRITTEN_HOST.exec(raw)?.[1]?.toLowerCase();
415
+ if (host === void 0) return false;
416
+ if (host.startsWith("[")) return url.hostname === "[::1]";
417
+ return host === "localhost" || host.endsWith(".localhost") || LOOPBACK_IPV4.test(host);
418
+ }
419
+ /** The key set is the root of trust. The error names the rule, never the URL. */
420
+ function requireJwksUrl(config, issuer) {
421
+ const raw = config.jwksUrl ?? `${issuer.replace(/\/+$/, "")}/.well-known/jwks.json`;
422
+ if (typeof raw !== "string") throw new TypeError("[passflow] jwksUrl must be a URL string");
423
+ let url;
424
+ try {
425
+ url = new URL(raw);
426
+ } catch {
427
+ throw new TypeError("[passflow] jwksUrl must be a valid URL");
428
+ }
429
+ if (url.protocol === "https:") return raw;
430
+ if (url.protocol === "http:" && (config.allowInsecureJwksUrl === true || isLoopbackHost(raw, url))) return raw;
431
+ throw new TypeError("[passflow] jwksUrl must use https unless the host is loopback or allowInsecureJwksUrl is set");
432
+ }
433
+ function requireTokenTypes(tokenTypes) {
434
+ if (tokenTypes === void 0) return ["access"];
435
+ if (!Array.isArray(tokenTypes) || tokenTypes.length === 0 || !tokenTypes.every(isNonEmptyString)) throw new TypeError("[passflow] tokenTypes must name one or more token types");
436
+ return [...tokenTypes];
437
+ }
438
+ /** `undefined` means the default. Anything else must be a finite number within `min`, or a TypeError naming the option. */
439
+ function requireNumber(name, value, fallback, min) {
440
+ if (value === void 0) return fallback;
441
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min) throw new TypeError(`[passflow] ${name} must be a finite number no less than ${String(min)}`);
442
+ return value;
443
+ }
444
+ function requireJwksOptions(jwks) {
445
+ if (jwks !== void 0 && (typeof jwks !== "object" || jwks === null)) throw new TypeError("[passflow] jwks must be an object");
446
+ const maxAge = requireNumber("jwks.maxAge", jwks?.maxAge, DEFAULT_MAX_AGE, 0);
447
+ const minRefreshInterval = requireNumber("jwks.minRefreshInterval", jwks?.minRefreshInterval, DEFAULT_MIN_REFRESH_INTERVAL, 0);
448
+ const maxStale = requireNumber("jwks.maxStale", jwks?.maxStale, DEFAULT_MAX_STALE, 0);
449
+ if (maxStale < maxAge) throw new TypeError("[passflow] jwks.maxStale must not be below jwks.maxAge");
450
+ const maxBytes = requireNumber("jwks.maxBytes", jwks?.maxBytes, DEFAULT_MAX_BYTES, 1);
451
+ const timeout = jwks?.timeout === void 0 ? DEFAULT_TIMEOUT : jwks.timeout;
452
+ if (typeof timeout !== "number" || !Number.isInteger(timeout) || timeout < 1 || timeout > MAX_TIMER_MS) throw new TypeError(`[passflow] jwks.timeout must be an integer between 1 and ${String(MAX_TIMER_MS)}`);
453
+ if (jwks?.fetch !== void 0 && typeof jwks.fetch !== "function") throw new TypeError("[passflow] jwks.fetch must be a function");
454
+ return {
455
+ maxAge,
456
+ minRefreshInterval,
457
+ maxStale,
458
+ maxBytes,
459
+ timeout,
460
+ fetch: jwks?.fetch
461
+ };
462
+ }
463
+ function claimFailureReason(claim) {
464
+ switch (claim) {
465
+ case "nbf": return "not_yet_valid";
466
+ case "iss": return "issuer";
467
+ case "exp": return "exp_missing";
468
+ default: return "malformed";
469
+ }
470
+ }
471
+ /**
472
+ * Only token defects may become a 401. Nothing jose throws here can mean "service down", because jose no
473
+ * longer talks to the network; `ERR_JWKS_INVALID` at resolution and any non-jose error say nothing about
474
+ * the token, so they are key set or runtime defects and answer 503.
475
+ */
476
+ function toAuthError(error) {
477
+ if (error instanceof require_evaluate.PassflowAuthError) return error;
478
+ if (error instanceof KeyUnknownError) return new require_evaluate.PassflowAuthError("invalid_token", "key_unknown");
479
+ if (error instanceof JWKSUnavailableError) return new require_evaluate.PassflowAuthError("jwks_unavailable", void 0, { cause: error.cause });
480
+ if (error instanceof jose.errors.JWSSignatureVerificationFailed) return new require_evaluate.PassflowAuthError("invalid_token", "signature");
481
+ if (error instanceof jose.errors.JWTExpired) return new require_evaluate.PassflowAuthError("invalid_token", "expired");
482
+ if (error instanceof jose.errors.JWTClaimValidationFailed) return new require_evaluate.PassflowAuthError("invalid_token", claimFailureReason(error.claim));
483
+ if (error instanceof jose.errors.JWKSMultipleMatchingKeys) return new require_evaluate.PassflowAuthError("invalid_token", "key_unknown");
484
+ if (error instanceof jose.errors.JWKSInvalid || !(error instanceof jose.errors.JOSEError)) return new require_evaluate.PassflowAuthError("jwks_unavailable");
485
+ return new require_evaluate.PassflowAuthError("invalid_token", "malformed");
486
+ }
487
+ /**
488
+ * Builds a verifier from a validated configuration and touches no network: the key set loads on the first
489
+ * `verify`, or on `refresh`. Build one per trust policy, at startup: each owns a key cache, and a verifier
490
+ * built per request would defeat it.
491
+ */
492
+ function createVerifier(config) {
493
+ if (typeof config !== "object" || config === null) throw new TypeError("[passflow] config must be an object");
494
+ const issuer = requireIssuer(config.issuer);
495
+ const audiences = requireAudiences(config.audience);
496
+ const url = requireJwksUrl(config, issuer);
497
+ const clockTolerance = requireNumber("leewaySeconds", config.leewaySeconds, DEFAULT_LEEWAY_SECONDS, 0);
498
+ const tokenTypes = requireTokenTypes(config.tokenTypes);
499
+ if (config.now !== void 0 && typeof config.now !== "function") throw new TypeError("[passflow] now must be a function");
500
+ const now = config.now ?? (() => /* @__PURE__ */ new Date());
501
+ const keySet = createKeySet({
502
+ url,
503
+ now: () => now().getTime(),
504
+ ...requireJwksOptions(config.jwks)
505
+ });
506
+ const verifier = {
507
+ async verify(token) {
508
+ try {
509
+ if (typeof token !== "string") throw new require_evaluate.PassflowAuthError("invalid_token", "malformed");
510
+ inspectHeader(token);
511
+ const { payload } = await keySet.withKeys((resolver) => (0, jose.jwtVerify)(token, resolver, {
512
+ algorithms: [...ALLOWED_ALGORITHMS],
513
+ issuer,
514
+ requiredClaims: ["exp"],
515
+ clockTolerance,
516
+ currentDate: now()
517
+ }));
518
+ if (payload.iat !== void 0 && !Number.isFinite(payload.iat)) throw new require_evaluate.PassflowAuthError("invalid_token", "malformed");
519
+ if (!Number.isFinite(payload.exp)) throw new require_evaluate.PassflowAuthError("invalid_token", "exp_missing");
520
+ const presented = typeof payload.aud === "string" ? [payload.aud] : payload.aud ?? [];
521
+ if (!Array.isArray(presented) || !presented.every((aud) => typeof aud === "string") || !presented.some((aud) => audiences.includes(aud))) throw new require_evaluate.PassflowAuthError("invalid_token", "audience");
522
+ if (typeof payload.type !== "string" || !tokenTypes.includes(payload.type)) throw new require_evaluate.PassflowAuthError("invalid_token", "type");
523
+ const claims = require_evaluate.extractClaims(payload, Math.floor(now().getTime() / 1e3));
524
+ require_evaluate.recordProvenance(claims, verifier);
525
+ return claims;
526
+ } catch (error) {
527
+ throw toAuthError(error);
528
+ }
529
+ },
530
+ async refresh() {
531
+ try {
532
+ await keySet.refresh();
533
+ } catch (error) {
534
+ throw toAuthError(error);
535
+ }
536
+ }
537
+ };
538
+ return verifier;
539
+ }
540
+ exports.AAL1 = require_evaluate.AAL1;
541
+ exports.AAL2 = require_evaluate.AAL2;
542
+ exports.AAL3 = require_evaluate.AAL3;
543
+ exports.ACR_NONE = require_evaluate.ACR_NONE;
544
+ exports.CHALLENGE_HEADER = require_evaluate.CHALLENGE_HEADER;
545
+ exports.CORS_EXPOSED_HEADERS = require_evaluate.CORS_EXPOSED_HEADERS;
546
+ exports.PassflowAuthError = require_evaluate.PassflowAuthError;
547
+ exports.PassflowUsageError = require_evaluate.PassflowUsageError;
548
+ exports.acrLevel = require_evaluate.acrLevel;
549
+ exports.acrSatisfies = require_evaluate.acrSatisfies;
550
+ exports.assertMaxAge = require_evaluate.assertMaxAge;
551
+ exports.authenticate = require_evaluate.authenticate;
552
+ exports.createVerifier = createVerifier;
553
+ exports.evaluate = require_evaluate.evaluate;
554
+ exports.extractBearer = require_evaluate.extractBearer;
555
+ exports.formatChallenge = require_evaluate.formatChallenge;
556
+ exports.isPassflowAuthError = require_evaluate.isPassflowAuthError;
557
+ exports.rolesOf = require_evaluate.rolesOf;
558
+ exports.verifiedBy = require_evaluate.verifiedBy;