(
url: URL,
options: CachedRemoteJWKSetOptions = {},
)
| 113 | * MCP auth path — see module header for why we don't just use jose's built-in. |
| 114 | */ |
| 115 | export const createCachedRemoteJWKSet = ( |
| 116 | url: URL, |
| 117 | options: CachedRemoteJWKSetOptions = {}, |
| 118 | ): CachedRemoteJWKSet => { |
| 119 | const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS; |
| 120 | const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; |
| 121 | // Capture the fetch impl lazily so consumers can swap globalThis.fetch |
| 122 | // (tests do this) without us snapshotting a stale reference. |
| 123 | const fetchImpl = (): typeof globalThis.fetch => |
| 124 | options.fetch ?? globalThis.fetch.bind(globalThis); |
| 125 | |
| 126 | let entry: CacheEntry | null = null; |
| 127 | let inflight: Promise<CacheEntry> | null = null; |
| 128 | |
| 129 | const refresh = (): Promise<CacheEntry> => { |
| 130 | if (inflight) return inflight; |
| 131 | inflight = (async () => { |
| 132 | const jwks = await fetchJwksOnce(url, fetchImpl(), timeoutMs); |
| 133 | const next: CacheEntry = { |
| 134 | jwks, |
| 135 | fetchedAt: Date.now(), |
| 136 | resolver: createLocalJWKSet(jwks), |
| 137 | }; |
| 138 | entry = next; |
| 139 | return next; |
| 140 | })().finally(() => { |
| 141 | inflight = null; |
| 142 | }); |
| 143 | return inflight; |
| 144 | }; |
| 145 | |
| 146 | const ensureFresh = async (forceRefresh: boolean): Promise<CacheEntry> => { |
| 147 | if (forceRefresh) return refresh(); |
| 148 | if (entry && Date.now() - entry.fetchedAt < ttlMs) return entry; |
| 149 | return refresh(); |
| 150 | }; |
| 151 | |
| 152 | const get: JWTVerifyGetKey = async (protectedHeader, token) => { |
| 153 | const current = await ensureFresh(false); |
| 154 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: jose JWTVerifyGetKey retry path is defined by thrown resolver failures |
| 155 | try { |
| 156 | return await current.resolver(protectedHeader, token); |
| 157 | } catch (error) { |
| 158 | // Likely cause: keys rotated upstream after our TTL window started. |
| 159 | // Refetch once and try again. Anything still failing bubbles up so |
| 160 | // jose can classify it (we do not silently swallow real failures). |
| 161 | if (!isJwksNoMatchingKey(error)) { |
| 162 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: jose JWTVerifyGetKey requires preserving upstream resolver rejection |
| 163 | throw error; |
| 164 | } |
| 165 | const refreshed = await ensureFresh(true); |
| 166 | return refreshed.resolver(protectedHeader, token); |
| 167 | } |
| 168 | }; |
| 169 | |
| 170 | const result = get as CachedRemoteJWKSet; |
| 171 | Object.defineProperty(result, "forceRefresh", { |
| 172 | value: () => { |
no outgoing calls
no test coverage detected