UNPKG

jose

Version:

JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes

102 lines (101 loc) • 4.28 kB
import { JOSEError, JWKSNoMatchingKey, JWKSTimeout } from "../util/errors.js"; import { createLocalJWKSet } from "./local.js"; import { isJwkSet } from "../lib/validate.js"; function isCloudflareWorkers() { return typeof WebSocketPair < "u" || typeof navigator < "u" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime < "u" && EdgeRuntime === "vercel"; } let USER_AGENT; (typeof navigator > "u" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) && (USER_AGENT = "jose/v6.2.13"); const customFetch = /* @__PURE__ */ Symbol(); async function fetchJwks(url, headers, signal, fetchImpl = fetch) { const response = await fetchImpl(url, { method: "GET", signal, redirect: "manual", headers }).catch((err) => { throw err.name === "TimeoutError" ? new JWKSTimeout(void 0, { cause: err }) : err; }); if (response.status !== 200) throw new JOSEError("Expected 200 OK from the JSON Web Key Set HTTP response"); try { return await response.json(); } catch (cause) { throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON", { cause }); } } const jwksCache = /* @__PURE__ */ Symbol(); function isFreshFor(timestamp, duration) { return Number.isFinite(timestamp) && Date.now() < timestamp + duration; } function validateDuration(value, fallback, option) { if (Number.isNaN(value)) throw new TypeError(`"${option}" option must not be NaN`); return typeof value == "number" ? value : fallback; } function createRemoteJWKSet(url, options) { if (!(url instanceof URL)) throw new TypeError("url must be an instance of URL"); const href = new URL(url.href).href, opts = options ?? {}, timeoutOption = opts.timeoutDuration; if (typeof timeoutOption == "number" && (!Number.isInteger(timeoutOption) || timeoutOption < 0)) throw new TypeError('"timeoutDuration" option must be a non-negative integer'); const timeoutDuration = typeof timeoutOption == "number" ? timeoutOption : 5e3, cooldownDuration = validateDuration(opts.cooldownDuration, 3e4, "cooldownDuration"), cacheMaxAge = validateDuration(opts.cacheMaxAge, 6e5, "cacheMaxAge"), headers = new Headers(opts.headers); USER_AGENT && !headers.has("User-Agent") && headers.set("User-Agent", USER_AGENT), headers.has("accept") || headers.set("accept", "application/json, application/jwk-set+json"); const fetchImpl = opts[customFetch], cache = opts[jwksCache]; let jwksTimestamp, pendingFetch, reloadSequence = 0, appliedSequence = 0, local; if (cache && typeof cache == "object") { const { uat, jwks } = cache; isFreshFor(uat, cacheMaxAge) && isJwkSet(jwks) && (jwksTimestamp = uat, local = createLocalJWKSet(jwks)); } const reload = async () => { if (pendingFetch && isCloudflareWorkers() && (pendingFetch = void 0), !pendingFetch) { const sequence = ++reloadSequence, current = pendingFetch = fetchJwks(href, headers, AbortSignal.timeout(timeoutDuration), fetchImpl).then((json) => { const next = createLocalJWKSet(json); if (sequence <= appliedSequence) return; local = next; const updatedAt = Date.now(); cache && (cache.uat = updatedAt, cache.jwks = json), jwksTimestamp = updatedAt, appliedSequence = sequence; }).finally(() => { pendingFetch === current && (pendingFetch = void 0); }); } await pendingFetch; }; return Object.defineProperties(async (protectedHeader, token) => { (!local || !isFreshFor(jwksTimestamp, cacheMaxAge)) && await reload(); try { return await local(protectedHeader, token); } catch (err) { if (err instanceof JWKSNoMatchingKey && !isFreshFor(jwksTimestamp, cooldownDuration)) return await reload(), local(protectedHeader, token); throw err; } }, { coolingDown: { get: () => isFreshFor(jwksTimestamp, cooldownDuration), enumerable: !0 }, fresh: { get: () => isFreshFor(jwksTimestamp, cacheMaxAge), enumerable: !0 }, reload: { value: reload, enumerable: !0 }, reloading: { get: () => !!pendingFetch, enumerable: !0 }, jwks: { value: () => local?.jwks(), enumerable: !0 } }); } export { createRemoteJWKSet, customFetch, jwksCache };