@valbuild/next 0.120.0 → 0.120.3

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.
@@ -154,6 +154,10 @@ function initValServer(valModules, config, nextConfig) {
154
154
  * - **Nothing is read from the payload before the signature verifies.** Claims
155
155
  * from an unverified token are attacker input.
156
156
  * - **Keys come only from the configured issuer's JWKS**, never from the token.
157
+ * The key set is cached, but a token naming a `kid` the cache does not hold
158
+ * provokes one rate-limited refetch rather than a refusal — see
159
+ * {@link UNKNOWN_KID_REFETCH_INTERVAL_MS}. Without that, this server's own
160
+ * cache turns any key rotation into an outage lasting the rest of the TTL.
157
161
  * - **ECDSA JWS signatures are raw `r||s`** (RFC 7518), not DER, which is what
158
162
  * `dsaEncoding: "ieee-p1363"` below is for. Omit it and every valid signature
159
163
  * is rejected — or worse, a future change makes it accept the wrong thing.
@@ -170,6 +174,21 @@ var JWKS_TTL_MS = 5 * 60 * 1000;
170
174
  * it.
171
175
  */
172
176
  var JWKS_ERROR_TTL_MS = 30 * 1000;
177
+ /**
178
+ * The floor between two refetches provoked by an unknown `kid`.
179
+ *
180
+ * A token naming a key this process has not seen is the one case where the TTL
181
+ * is the wrong answer: the issuer may have just rotated, and refusing for the
182
+ * rest of the five minutes turns a rotation into an outage. So an unknown `kid`
183
+ * bypasses the TTL — but only this often, because the `kid` comes from the
184
+ * token and the token comes from whoever is calling. Without a floor, a stream
185
+ * of random `kid`s would be a way to make this server hammer its own issuer.
186
+ *
187
+ * Note what the floor costs when it bites: a refusal, for a token that would
188
+ * have verified, for at most this long. That is the same failure the TTL used
189
+ * to guarantee for five minutes, so the trade only ever improves.
190
+ */
191
+ var UNKNOWN_KID_REFETCH_INTERVAL_MS = 30 * 1000;
173
192
  /**
174
193
  * One cache per issuer, and it has to outlive the request or it is not a cache:
175
194
  * a fetch per tool call would put a network round trip in front of every read.
@@ -177,6 +196,15 @@ var JWKS_ERROR_TTL_MS = 30 * 1000;
177
196
  var jwksCache = new Map();
178
197
  /** Concurrent misses share one fetch rather than starting several. */
179
198
  var inFlight = new Map();
199
+ /**
200
+ * When an unknown `kid` last made us go back to the issuer, per issuer.
201
+ *
202
+ * Separate from the cache because it is a rate limit rather than a cache: it
203
+ * records an *attempt*, so a refetch that found nothing new still spends the
204
+ * window. Keyed by issuer, not by `kid`, or an attacker would simply use a new
205
+ * one each time.
206
+ */
207
+ var unknownKidRefetchAtMs = new Map();
180
208
  function jwksUrl(issuer) {
181
209
  // Not discovered from the issuer's metadata document, deliberately:
182
210
  // discovery would mean one more request on the hot path and one more thing
@@ -187,105 +215,211 @@ function jwksUrl(issuer) {
187
215
  function loadJwks(_x, _x2) {
188
216
  return _loadJwks.apply(this, arguments);
189
217
  }
218
+ /**
219
+ * Go back to the issuer because the token named a key we do not have.
220
+ *
221
+ * Rate-limited, and the limit is the whole reason this is not just a call to
222
+ * {@link fetchJwks}: the `kid` that triggers it is attacker-controlled. Returns
223
+ * `null` when the window has not elapsed, which the caller reads as "nothing
224
+ * new to try" rather than as a failure.
225
+ *
226
+ * The limit is on *starting* a fetch, not on benefiting from one. A fetch
227
+ * already in flight is joined whatever the window says, because joining it
228
+ * costs the issuer nothing — and the case that matters is precisely a burst:
229
+ * at a rotation, many requests arrive at once carrying the same new `kid`, and
230
+ * refusing all but the first would be the outage this whole path exists to
231
+ * prevent, merely shortened from five minutes to thirty seconds.
232
+ *
233
+ * Only ever called with a successfully-fetched key set in hand. An error entry
234
+ * has its own, shorter TTL and its own recovery, and letting an unknown `kid`
235
+ * shortcut it would hand an unreachable issuer a retry storm.
236
+ */
190
237
  function _loadJwks() {
191
- _loadJwks = version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee2(config, nowMs) {
192
- var _config$fetchImpl;
193
- var url, cached, age, ttl, existing, fetchImpl, pending, entry;
194
- return version._regenerator().w(function (_context2) {
195
- while (1) switch (_context2.p = _context2.n) {
238
+ _loadJwks = version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee(config, nowMs) {
239
+ var url, cached, age, ttl, _t;
240
+ return version._regenerator().w(function (_context) {
241
+ while (1) switch (_context.n) {
196
242
  case 0:
197
243
  url = jwksUrl(config.issuer);
198
244
  cached = jwksCache.get(url);
199
245
  if (!cached) {
200
- _context2.n = 1;
246
+ _context.n = 1;
201
247
  break;
202
248
  }
203
249
  age = cached.status === "keys" ? nowMs - cached.fetchedAtMs : nowMs - cached.failedAtMs;
204
250
  ttl = cached.status === "keys" ? JWKS_TTL_MS : JWKS_ERROR_TTL_MS;
205
251
  if (!(age < ttl)) {
206
- _context2.n = 1;
252
+ _context.n = 1;
207
253
  break;
208
254
  }
209
- return _context2.a(2, cached);
255
+ return _context.a(2, {
256
+ entry: cached,
257
+ fromCache: true
258
+ });
210
259
  case 1:
260
+ _context.n = 2;
261
+ return fetchJwks(config, nowMs);
262
+ case 2:
263
+ _t = _context.v;
264
+ return _context.a(2, {
265
+ entry: _t,
266
+ fromCache: false
267
+ });
268
+ }
269
+ }, _callee);
270
+ }));
271
+ return _loadJwks.apply(this, arguments);
272
+ }
273
+ function refetchForUnknownKid(_x3, _x4) {
274
+ return _refetchForUnknownKid.apply(this, arguments);
275
+ }
276
+ /**
277
+ * Fetch the key set, ignoring whatever is cached, and cache the result.
278
+ *
279
+ * Shares {@link inFlight} with every other caller, so a forced refetch that
280
+ * lands during an ordinary miss joins it rather than opening a second request.
281
+ * Only the caller that *starts* a fetch writes the cache — a joiner returns the
282
+ * shared promise above — so `keepCacheOnError` is a property of the fetch, not a
283
+ * race between callers.
284
+ */
285
+ function _refetchForUnknownKid() {
286
+ _refetchForUnknownKid = version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee2(config, nowMs) {
287
+ var url, existing, lastAttemptMs;
288
+ return version._regenerator().w(function (_context2) {
289
+ while (1) switch (_context2.n) {
290
+ case 0:
291
+ url = jwksUrl(config.issuer);
211
292
  existing = inFlight.get(url);
212
293
  if (!existing) {
213
- _context2.n = 2;
294
+ _context2.n = 1;
214
295
  break;
215
296
  }
216
297
  return _context2.a(2, existing);
298
+ case 1:
299
+ lastAttemptMs = unknownKidRefetchAtMs.get(url);
300
+ if (!(lastAttemptMs !== undefined && nowMs - lastAttemptMs < UNKNOWN_KID_REFETCH_INTERVAL_MS)) {
301
+ _context2.n = 2;
302
+ break;
303
+ }
304
+ return _context2.a(2, null);
217
305
  case 2:
306
+ // Recorded before the await, so that once this fetch has finished the window
307
+ // is already closed against the next unknown `kid`.
308
+ unknownKidRefetchAtMs.set(url, nowMs);
309
+ return _context2.a(2, fetchJwks(config, nowMs, {
310
+ keepCacheOnError: true
311
+ }));
312
+ }
313
+ }, _callee2);
314
+ }));
315
+ return _refetchForUnknownKid.apply(this, arguments);
316
+ }
317
+ function fetchJwks(_x5, _x6) {
318
+ return _fetchJwks.apply(this, arguments);
319
+ }
320
+ function _fetchJwks() {
321
+ _fetchJwks = version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee4(config, nowMs) {
322
+ var _config$fetchImpl;
323
+ var _ref,
324
+ _ref$keepCacheOnError,
325
+ keepCacheOnError,
326
+ url,
327
+ existing,
328
+ fetchImpl,
329
+ pending,
330
+ entry,
331
+ cached,
332
+ _args4 = arguments;
333
+ return version._regenerator().w(function (_context4) {
334
+ while (1) switch (_context4.p = _context4.n) {
335
+ case 0:
336
+ _ref = _args4.length > 2 && _args4[2] !== undefined ? _args4[2] : {}, _ref$keepCacheOnError = _ref.keepCacheOnError, keepCacheOnError = _ref$keepCacheOnError === void 0 ? false : _ref$keepCacheOnError;
337
+ url = jwksUrl(config.issuer);
338
+ existing = inFlight.get(url);
339
+ if (!existing) {
340
+ _context4.n = 1;
341
+ break;
342
+ }
343
+ return _context4.a(2, existing);
344
+ case 1:
218
345
  fetchImpl = (_config$fetchImpl = config.fetchImpl) !== null && _config$fetchImpl !== void 0 ? _config$fetchImpl : fetch;
219
- pending = version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee() {
346
+ pending = version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee3() {
220
347
  var res, body, keys;
221
- return version._regenerator().w(function (_context) {
222
- while (1) switch (_context.p = _context.n) {
348
+ return version._regenerator().w(function (_context3) {
349
+ while (1) switch (_context3.p = _context3.n) {
223
350
  case 0:
224
- _context.p = 0;
225
- _context.n = 1;
351
+ _context3.p = 0;
352
+ _context3.n = 1;
226
353
  return fetchImpl(url, {
227
354
  headers: {
228
355
  accept: "application/json"
229
356
  }
230
357
  });
231
358
  case 1:
232
- res = _context.v;
359
+ res = _context3.v;
233
360
  if (res.ok) {
234
- _context.n = 2;
361
+ _context3.n = 2;
235
362
  break;
236
363
  }
237
- return _context.a(2, {
364
+ return _context3.a(2, {
238
365
  status: "error",
239
366
  failedAtMs: nowMs
240
367
  });
241
368
  case 2:
242
- _context.n = 3;
369
+ _context3.n = 3;
243
370
  return res.json();
244
371
  case 3:
245
- body = _context.v;
372
+ body = _context3.v;
246
373
  keys = readKeys(body);
247
374
  if (!(keys === null)) {
248
- _context.n = 4;
375
+ _context3.n = 4;
249
376
  break;
250
377
  }
251
- return _context.a(2, {
378
+ return _context3.a(2, {
252
379
  status: "error",
253
380
  failedAtMs: nowMs
254
381
  });
255
382
  case 4:
256
- return _context.a(2, {
383
+ return _context3.a(2, {
257
384
  status: "keys",
258
385
  keys: keys,
259
386
  fetchedAtMs: nowMs
260
387
  });
261
388
  case 5:
262
- _context.p = 5;
263
- _context.v;
264
- return _context.a(2, {
389
+ _context3.p = 5;
390
+ _context3.v;
391
+ return _context3.a(2, {
265
392
  status: "error",
266
393
  failedAtMs: nowMs
267
394
  });
268
395
  }
269
- }, _callee, null, [[0, 5]]);
396
+ }, _callee3, null, [[0, 5]]);
270
397
  }))();
271
398
  inFlight.set(url, pending);
272
- _context2.p = 3;
273
- _context2.n = 4;
399
+ _context4.p = 2;
400
+ _context4.n = 3;
274
401
  return pending;
402
+ case 3:
403
+ entry = _context4.v;
404
+ cached = jwksCache.get(url);
405
+ if (!(entry.status === "error" && keepCacheOnError && (cached === null || cached === void 0 ? void 0 : cached.status) === "keys")) {
406
+ _context4.n = 4;
407
+ break;
408
+ }
409
+ return _context4.a(2, entry);
275
410
  case 4:
276
- entry = _context2.v;
277
411
  jwksCache.set(url, entry);
278
- return _context2.a(2, entry);
412
+ return _context4.a(2, entry);
279
413
  case 5:
280
- _context2.p = 5;
414
+ _context4.p = 5;
281
415
  inFlight["delete"](url);
282
- return _context2.f(5);
416
+ return _context4.f(5);
283
417
  case 6:
284
- return _context2.a(2);
418
+ return _context4.a(2);
285
419
  }
286
- }, _callee2, null, [[3,, 5, 6]]);
420
+ }, _callee4, null, [[2,, 5, 6]]);
287
421
  }));
288
- return _loadJwks.apply(this, arguments);
422
+ return _fetchJwks.apply(this, arguments);
289
423
  }
290
424
  function readKeys(body) {
291
425
  if (_typeof._typeof(body) !== "object" || body === null || !("keys" in body)) {
@@ -320,22 +454,22 @@ function readBearerToken(request) {
320
454
  var token = match === null || match === void 0 || (_match$ = match[1]) === null || _match$ === void 0 ? void 0 : _match$.trim();
321
455
  return token ? token : null;
322
456
  }
323
- function verifyValAccessToken(_x3, _x4) {
457
+ function verifyValAccessToken(_x7, _x8) {
324
458
  return _verifyValAccessToken.apply(this, arguments);
325
459
  }
326
460
  function _verifyValAccessToken() {
327
- _verifyValAccessToken = version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee3(request, config) {
461
+ _verifyValAccessToken = version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee5(request, config) {
328
462
  var _config$clockToleranc;
329
- var token, parts, _parts, encodedHeader, encodedPayload, encodedSignature, header, kid, nowMs, jwks, candidates, signature, signedData, verified, payload, tolerance, nowSeconds, subject, scopes;
330
- return version._regenerator().w(function (_context3) {
331
- while (1) switch (_context3.n) {
463
+ var token, parts, _parts, encodedHeader, encodedPayload, encodedSignature, header, kid, nowMs, loaded, candidates, refetched, signature, signedData, verified, payload, tolerance, nowSeconds, subject, scopes;
464
+ return version._regenerator().w(function (_context5) {
465
+ while (1) switch (_context5.n) {
332
466
  case 0:
333
467
  token = readBearerToken(request);
334
468
  if (!(token === null)) {
335
- _context3.n = 1;
469
+ _context5.n = 1;
336
470
  break;
337
471
  }
338
- return _context3.a(2, {
472
+ return _context5.a(2, {
339
473
  status: "refused",
340
474
  error: "invalid_request",
341
475
  description: "This Val MCP endpoint needs an access token. Authorize with the Val authorization server and present it as `Authorization: Bearer`."
@@ -343,125 +477,148 @@ function _verifyValAccessToken() {
343
477
  case 1:
344
478
  parts = token.split(".");
345
479
  if (!(parts.length !== 3)) {
346
- _context3.n = 2;
480
+ _context5.n = 2;
347
481
  break;
348
482
  }
349
- return _context3.a(2, invalidToken("The access token is not a JWS."));
483
+ return _context5.a(2, invalidToken("The access token is not a JWS."));
350
484
  case 2:
351
485
  _parts = slicedToArray._slicedToArray(parts, 3), encodedHeader = _parts[0], encodedPayload = _parts[1], encodedSignature = _parts[2];
352
486
  header = decodeJsonSegment(encodedHeader);
353
487
  if (!(header === null)) {
354
- _context3.n = 3;
488
+ _context5.n = 3;
355
489
  break;
356
490
  }
357
- return _context3.a(2, invalidToken("The access token's header could not be read."));
491
+ return _context5.a(2, invalidToken("The access token's header could not be read."));
358
492
  case 3:
359
493
  if (!(header.alg !== "ES256")) {
360
- _context3.n = 4;
494
+ _context5.n = 4;
361
495
  break;
362
496
  }
363
- return _context3.a(2, invalidToken("The access token is not signed with ES256, which is the only algorithm this server accepts."));
497
+ return _context5.a(2, invalidToken("The access token is not signed with ES256, which is the only algorithm this server accepts."));
364
498
  case 4:
365
499
  kid = typeof header.kid === "string" ? header.kid : null;
366
500
  nowMs = Date.now();
367
- _context3.n = 5;
501
+ _context5.n = 5;
368
502
  return loadJwks(config, nowMs);
369
503
  case 5:
370
- jwks = _context3.v;
371
- if (!(jwks.status === "error")) {
372
- _context3.n = 6;
504
+ loaded = _context5.v;
505
+ if (!(loaded.entry.status === "error")) {
506
+ _context5.n = 6;
373
507
  break;
374
508
  }
375
- return _context3.a(2, invalidToken("The access token could not be verified because the Val authorization server's keys could not be fetched. This may be temporary."));
509
+ return _context5.a(2, invalidToken("The access token could not be verified because the Val authorization server's keys could not be fetched. This may be temporary."));
376
510
  case 6:
377
- candidates = jwks.keys.filter(function (key) {
511
+ candidates = loaded.entry.keys.filter(function (key) {
378
512
  return isVerifyingP256Key(key, kid);
379
513
  });
380
- if (!(candidates.length === 0)) {
381
- _context3.n = 7;
514
+ if (!(candidates.length === 0 && kid !== null && loaded.fromCache)) {
515
+ _context5.n = 9;
382
516
  break;
383
517
  }
384
- return _context3.a(2, invalidToken("The access token was signed with a key the Val authorization server does not publish."));
518
+ _context5.n = 7;
519
+ return refetchForUnknownKid(config, nowMs);
385
520
  case 7:
521
+ refetched = _context5.v;
522
+ if (!(refetched !== null)) {
523
+ _context5.n = 9;
524
+ break;
525
+ }
526
+ if (!(refetched.status === "error")) {
527
+ _context5.n = 8;
528
+ break;
529
+ }
530
+ return _context5.a(2, invalidToken("The access token could not be verified because the Val authorization server's keys could not be fetched. This may be temporary."));
531
+ case 8:
532
+ candidates = refetched.keys.filter(function (key) {
533
+ return isVerifyingP256Key(key, kid);
534
+ });
535
+ case 9:
536
+ if (!(candidates.length === 0)) {
537
+ _context5.n = 10;
538
+ break;
539
+ }
540
+ return _context5.a(2, invalidToken("The access token was signed with a key the Val authorization server does not publish."));
541
+ case 10:
386
542
  signature = decodeBase64Url(encodedSignature);
387
543
  if (!(signature === null)) {
388
- _context3.n = 8;
544
+ _context5.n = 11;
389
545
  break;
390
546
  }
391
- return _context3.a(2, invalidToken("The access token's signature could not be read."));
392
- case 8:
547
+ return _context5.a(2, invalidToken("The access token's signature could not be read."));
548
+ case 11:
393
549
  signedData = Buffer.from("".concat(encodedHeader, ".").concat(encodedPayload), "ascii"); // Every published key is tried when the token names no `kid`, so a rotation
394
550
  // that has not yet propagated to clients still verifies. With a `kid` the
395
- // filter above leaves one.
551
+ // filter above leaves one — or, if this process had not yet seen that key,
552
+ // the one the refetch just found.
396
553
  verified = candidates.some(function (key) {
397
554
  return verifyWithJwk(key, signedData, signature);
398
555
  });
399
556
  if (verified) {
400
- _context3.n = 9;
557
+ _context5.n = 12;
401
558
  break;
402
559
  }
403
- return _context3.a(2, invalidToken("The access token's signature could not be verified."));
404
- case 9:
560
+ return _context5.a(2, invalidToken("The access token's signature could not be verified."));
561
+ case 12:
405
562
  // Only now: everything below reads the payload, and before this line it was
406
563
  // attacker input.
407
564
  payload = decodeJsonSegment(encodedPayload);
408
565
  if (!(payload === null)) {
409
- _context3.n = 10;
566
+ _context5.n = 13;
410
567
  break;
411
568
  }
412
- return _context3.a(2, invalidToken("The access token's payload could not be read."));
413
- case 10:
569
+ return _context5.a(2, invalidToken("The access token's payload could not be read."));
570
+ case 13:
414
571
  tolerance = (_config$clockToleranc = config.clockToleranceSeconds) !== null && _config$clockToleranc !== void 0 ? _config$clockToleranc : DEFAULT_CLOCK_TOLERANCE_SECONDS;
415
572
  nowSeconds = Math.floor(nowMs / 1000);
416
573
  if (!(typeof payload.exp !== "number")) {
417
- _context3.n = 11;
574
+ _context5.n = 14;
418
575
  break;
419
576
  }
420
- return _context3.a(2, invalidToken("The access token has no expiry."));
421
- case 11:
577
+ return _context5.a(2, invalidToken("The access token has no expiry."));
578
+ case 14:
422
579
  if (!(payload.exp + tolerance <= nowSeconds)) {
423
- _context3.n = 12;
580
+ _context5.n = 15;
424
581
  break;
425
582
  }
426
- return _context3.a(2, invalidToken("The access token has expired. Refresh it and try again."));
427
- case 12:
583
+ return _context5.a(2, invalidToken("The access token has expired. Refresh it and try again."));
584
+ case 15:
428
585
  if (!(typeof payload.nbf === "number" && payload.nbf - tolerance > nowSeconds)) {
429
- _context3.n = 13;
586
+ _context5.n = 16;
430
587
  break;
431
588
  }
432
- return _context3.a(2, invalidToken("The access token is not valid yet."));
433
- case 13:
589
+ return _context5.a(2, invalidToken("The access token is not valid yet."));
590
+ case 16:
434
591
  if (!(payload.iss !== config.issuer)) {
435
- _context3.n = 14;
592
+ _context5.n = 17;
436
593
  break;
437
594
  }
438
- return _context3.a(2, invalidToken("The access token was not issued by this server's authorization server (iss claim)."));
439
- case 14:
595
+ return _context5.a(2, invalidToken("The access token was not issued by this server's authorization server (iss claim)."));
596
+ case 17:
440
597
  if (audienceMatches(payload.aud, config.resource)) {
441
- _context3.n = 15;
598
+ _context5.n = 18;
442
599
  break;
443
600
  }
444
- return _context3.a(2, invalidToken("The access token is not valid for this server (aud claim)."));
445
- case 15:
601
+ return _context5.a(2, invalidToken("The access token is not valid for this server (aud claim)."));
602
+ case 18:
446
603
  subject = payload.sub;
447
604
  if (!(typeof subject !== "string" || subject.length === 0)) {
448
- _context3.n = 16;
605
+ _context5.n = 19;
449
606
  break;
450
607
  }
451
- return _context3.a(2, invalidToken("The access token has no subject."));
452
- case 16:
608
+ return _context5.a(2, invalidToken("The access token has no subject."));
609
+ case 19:
453
610
  scopes = readScopes(payload.scope);
454
611
  if (scopes.includes(server.VAL_SCOPE_READ)) {
455
- _context3.n = 17;
612
+ _context5.n = 20;
456
613
  break;
457
614
  }
458
- return _context3.a(2, {
615
+ return _context5.a(2, {
459
616
  status: "refused",
460
617
  error: "insufficient_scope",
461
618
  description: "The access token does not have the ".concat(server.VAL_SCOPE_READ, " scope, so it cannot read any content.")
462
619
  });
463
- case 17:
464
- return _context3.a(2, {
620
+ case 20:
621
+ return _context5.a(2, {
465
622
  status: "ok",
466
623
  auth: {
467
624
  type: "verified-profile",
@@ -470,7 +627,7 @@ function _verifyValAccessToken() {
470
627
  }
471
628
  });
472
629
  }
473
- }, _callee3);
630
+ }, _callee5);
474
631
  }));
475
632
  return _verifyValAccessToken.apply(this, arguments);
476
633
  }