import fetchJwks from '../runtime/fetch_jwks.js'; import { JWKSInvalid, JWKSNoMatchingKey } from '../util/errors.js'; import { isJWKSLike, LocalJWKSet } from './local.js'; function isCloudflareWorkers() { return (typeof WebSocketPair !== 'undefined' || (typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers') || (typeof EdgeRuntime !== 'undefined' && EdgeRuntime === 'vercel')); } let USER_AGENT; if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozilla/5.0 ')) { const NAME = 'jose'; const VERSION = 'v5.2.0'; USER_AGENT = `${NAME}/${VERSION}`; } class RemoteJWKSet extends LocalJWKSet { _url; _timeoutDuration; _cooldownDuration; _cacheMaxAge; _jwksTimestamp; _pendingFetch; _options; constructor(url, options) { super({ keys: [] }); this._jwks = undefined; if (!(url instanceof URL)) { throw new TypeError('url must be an instance of URL'); } this._url = new URL(url.href); this._options = { agent: options?.agent, headers: options?.headers }; this._timeoutDuration = typeof options?.timeoutDuration === 'number' ? options?.timeoutDuration : 5000; this._cooldownDuration = typeof options?.cooldownDuration === 'number' ? options?.cooldownDuration : 30000; this._cacheMaxAge = typeof options?.cacheMaxAge === 'number' ? options?.cacheMaxAge : 600000; } coolingDown() { return typeof this._jwksTimestamp === 'number' ? Date.now() < this._jwksTimestamp + this._cooldownDuration : false; } fresh() { return typeof this._jwksTimestamp === 'number' ? Date.now() < this._jwksTimestamp + this._cacheMaxAge : false; } async getKey(protectedHeader, token) { if (!this._jwks || !this.fresh()) { await this.reload(); } try { return await super.getKey(protectedHeader, token); } catch (err) { if (err instanceof JWKSNoMatchingKey) { if (this.coolingDown() === false) { await this.reload(); return super.getKey(protectedHeader, token); } } throw err; } } async reload() { if (this._pendingFetch && isCloudflareWorkers()) { this._pendingFetch = undefined; } const headers = new Headers(this._options.headers); if (USER_AGENT && !headers.has('User-Agent')) { headers.set('User-Agent', USER_AGENT); this._options.headers = Object.fromEntries(headers.entries()); } this._pendingFetch ||= fetchJwks(this._url, this._timeoutDuration, this._options) .then((json) => { if (!isJWKSLike(json)) { throw new JWKSInvalid('JSON Web Key Set malformed'); } this._jwks = { keys: json.keys }; this._jwksTimestamp = Date.now(); this._pendingFetch = undefined; }) .catch((err) => { this._pendingFetch = undefined; throw err; }); await this._pendingFetch; } } export function createRemoteJWKSet(url, options) { const set = new RemoteJWKSet(url, options); return async function (protectedHeader, token) { return set.getKey(protectedHeader, token); }; }