@vzn/vx-devtools 0.0.648 → 0.0.650

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 (3) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/src/oidc.ts +23 -1
package/README.md CHANGED
@@ -71,6 +71,8 @@ export function oidcTokenAuth(
71
71
  - jose checks the JWT: `RS256`, the signature against the issuer's keys
72
72
  (found through its `/.well-known/openid-configuration`, kept and
73
73
  refetched by `createRemoteJWKSet`), `aud`, `exp` and `nbf` within 60 s.
74
+ When a refetch fails, the keys last read still check tokens. A key they
75
+ lack is then `502`, not a refused token.
74
76
  - With `oidcAuth`, every request names its repository: `vx-issuer` and `vx-repository`,
75
77
  the `hashPart`s of the issuer's URL (no trailing slash) and of its
76
78
  `org/name` lowercased; one missing or not a hash is `400`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx-devtools",
3
- "version": "0.0.648",
3
+ "version": "0.0.650",
4
4
  "license": "MIT",
5
5
  "description": "Code vx's servers and plugins share, never core: first, the CI OIDC token check.",
6
6
  "keywords": [
package/src/oidc.ts CHANGED
@@ -293,6 +293,26 @@ function tokenVerifier(opts: OidcOptions): (token: string, req: Request) => Prom
293
293
  const now = opts.now ?? (() => Date.now() / 1000)
294
294
  // jose keeps each issuer's keys, and refetches them for a key it lacks.
295
295
  const keys = new Map<string, Promise<ReturnType<typeof createRemoteJWKSet>>>()
296
+ // The key set each URL last answered. jose refetches every 10 minutes and
297
+ // threw when that failed: one slow answer from GitHub's key endpoint failed
298
+ // an upload 502 and lost a DTE worker's entry (2026-10-29). Keys rotate
299
+ // rarely, so the last set answers until the issuer answers again.
300
+ const lastRead = new Map<string, unknown>()
301
+ // The issuers answered from lastRead since their last fetch: a kid that
302
+ // set lacks is then the issuer out of reach, not a token refused.
303
+ const stale = new Set<string>()
304
+ const keySet = async (iss: string, url: string): Promise<unknown> => {
305
+ try {
306
+ const json = await fetchJson(url)
307
+ lastRead.set(url, json)
308
+ stale.delete(iss)
309
+ return json
310
+ } catch (err) {
311
+ if (!lastRead.has(url)) throw err
312
+ stale.add(iss)
313
+ return lastRead.get(url)
314
+ }
315
+ }
296
316
  const keysOf = (iss: string) => {
297
317
  let set = keys.get(iss)
298
318
  if (set === undefined) {
@@ -300,7 +320,7 @@ function tokenVerifier(opts: OidcOptions): (token: string, req: Request) => Prom
300
320
  const uri = (c as { jwks_uri?: unknown }).jwks_uri
301
321
  if (typeof uri !== 'string') throw new Error('no jwks_uri')
302
322
  return createRemoteJWKSet(new URL(uri), {
303
- [customFetch]: async (url: string) => Response.json(await fetchJson(url)),
323
+ [customFetch]: async (url: string) => Response.json(await keySet(iss, url)),
304
324
  })
305
325
  })
306
326
  keys.set(iss, set)
@@ -346,6 +366,8 @@ function tokenVerifier(opts: OidcOptions): (token: string, req: Request) => Prom
346
366
  })
347
367
  ).payload
348
368
  } catch (err) {
369
+ if (err instanceof errors.JWKSNoMatchingKey && stale.has(iss))
370
+ throw keysUnread(iss, new Error('a key the keys last read lack, and a refetch failed'))
349
371
  throw refusal(err, iss, audience)
350
372
  }
351
373
  const claim = repoClaimOf(iss)