@valbuild/next 0.120.0 → 0.120.2
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.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# @valbuild/next
|
|
2
2
|
|
|
3
|
+
## 0.120.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#593](https://github.com/valbuild/val/pull/593) [`095ee0d`](https://github.com/valbuild/val/commit/095ee0dd011b069c30bc99ae58356e28796e106b) Thanks [@freekh](https://github.com/freekh)! - Publish the packages that 0.120.1 did not reach.
|
|
8
|
+
|
|
9
|
+
`@valbuild/server@0.120.1` made it to npm, but `@valbuild/cli`,
|
|
10
|
+
`@valbuild/language-server` and `@valbuild/next` did not — the release job
|
|
11
|
+
failed part-way through, and the version numbers it had already claimed could
|
|
12
|
+
not be reused. This release carries the same contents for those three packages:
|
|
13
|
+
they pick up the MCP signing-key rotation fix from `@valbuild/server@0.120.1`,
|
|
14
|
+
and there is nothing else in it.
|
|
15
|
+
|
|
16
|
+
If you are on 0.120.0, upgrade straight to this version. There is no 0.120.1 of
|
|
17
|
+
these three packages, and there will not be one.
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [[`095ee0d`](https://github.com/valbuild/val/commit/095ee0dd011b069c30bc99ae58356e28796e106b)]:
|
|
20
|
+
- @valbuild/language-server@0.120.2
|
|
21
|
+
|
|
22
|
+
## 0.120.1
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- [#590](https://github.com/valbuild/val/pull/590) [`6f318d4`](https://github.com/valbuild/val/commit/6f318d406295b772e721bf463283f47e2822e996) Thanks [@freekh](https://github.com/freekh)! - MCP: survive a signing-key rotation, and say what a refused local token means.
|
|
27
|
+
|
|
28
|
+
`@valbuild/next` caches the authorization server's JWKS for five minutes. Until
|
|
29
|
+
now a token signed with a key that arrived inside that window was refused
|
|
30
|
+
outright, so every warm instance rejected valid tokens until the cache expired —
|
|
31
|
+
a rotation on the issuer's side showed up as an outage on yours.
|
|
32
|
+
|
|
33
|
+
A token naming a key the cache does not hold now provokes one refetch of the key
|
|
34
|
+
set, at most once per issuer every 30 seconds. The rate limit matters because the
|
|
35
|
+
key id comes from the token: without it, unknown key ids would be a way to make
|
|
36
|
+
your app call its issuer once per request. It limits how often a fetch is
|
|
37
|
+
_started_, so requests that arrive while one is already running join it — which
|
|
38
|
+
is the normal shape of a rotation, where many requests meet the new key at once.
|
|
39
|
+
|
|
40
|
+
Separately, an MCP call that presents an access token to a project running in
|
|
41
|
+
local filesystem mode is still refused — there is nothing to authenticate against
|
|
42
|
+
— but the message now names the cause, which is that the project has an `oauth`
|
|
43
|
+
issuer configured (often `VAL_OAUTH_ISSUER` in a local `.env`) and should not
|
|
44
|
+
have one for local development.
|
|
45
|
+
|
|
46
|
+
- Updated dependencies [[`6f318d4`](https://github.com/valbuild/val/commit/6f318d406295b772e721bf463283f47e2822e996)]:
|
|
47
|
+
- @valbuild/server@0.120.1
|
|
48
|
+
- @valbuild/language-server@0.120.1
|
|
49
|
+
|
|
3
50
|
## 0.120.0
|
|
4
51
|
|
|
5
52
|
### Minor Changes
|
|
@@ -31,6 +31,10 @@ import { type ValToolAuth } from "@valbuild/server";
|
|
|
31
31
|
* - **Nothing is read from the payload before the signature verifies.** Claims
|
|
32
32
|
* from an unverified token are attacker input.
|
|
33
33
|
* - **Keys come only from the configured issuer's JWKS**, never from the token.
|
|
34
|
+
* The key set is cached, but a token naming a `kid` the cache does not hold
|
|
35
|
+
* provokes one rate-limited refetch rather than a refusal — see
|
|
36
|
+
* {@link UNKNOWN_KID_REFETCH_INTERVAL_MS}. Without that, this server's own
|
|
37
|
+
* cache turns any key rotation into an outage lasting the rest of the TTL.
|
|
34
38
|
* - **ECDSA JWS signatures are raw `r||s`** (RFC 7518), not DER, which is what
|
|
35
39
|
* `dsaEncoding: "ieee-p1363"` below is for. Omit it and every valid signature
|
|
36
40
|
* is rejected — or worse, a future change makes it accept the wrong thing.
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"next",
|
|
13
13
|
"react"
|
|
14
14
|
],
|
|
15
|
-
"version": "0.120.
|
|
15
|
+
"version": "0.120.2",
|
|
16
16
|
"main": "dist/valbuild-next.cjs.js",
|
|
17
17
|
"module": "dist/valbuild-next.esm.js",
|
|
18
18
|
"exports": {
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"client-only": "^0.0.1",
|
|
49
49
|
"server-only": "^0.0.1",
|
|
50
|
+
"@valbuild/language-server": "0.120.2",
|
|
50
51
|
"@valbuild/core": "0.120.0",
|
|
51
52
|
"@valbuild/react": "0.120.0",
|
|
52
|
-
"@valbuild/server": "0.120.
|
|
53
|
+
"@valbuild/server": "0.120.1",
|
|
53
54
|
"@valbuild/shared": "0.120.0",
|
|
54
|
-
"@valbuild/language-server": "0.120.0",
|
|
55
55
|
"@valbuild/ui": "0.120.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
@@ -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
|
|
192
|
-
var
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
+
_context.n = 1;
|
|
207
253
|
break;
|
|
208
254
|
}
|
|
209
|
-
return
|
|
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 =
|
|
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
|
|
346
|
+
pending = version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee3() {
|
|
220
347
|
var res, body, keys;
|
|
221
|
-
return version._regenerator().w(function (
|
|
222
|
-
while (1) switch (
|
|
348
|
+
return version._regenerator().w(function (_context3) {
|
|
349
|
+
while (1) switch (_context3.p = _context3.n) {
|
|
223
350
|
case 0:
|
|
224
|
-
|
|
225
|
-
|
|
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 =
|
|
359
|
+
res = _context3.v;
|
|
233
360
|
if (res.ok) {
|
|
234
|
-
|
|
361
|
+
_context3.n = 2;
|
|
235
362
|
break;
|
|
236
363
|
}
|
|
237
|
-
return
|
|
364
|
+
return _context3.a(2, {
|
|
238
365
|
status: "error",
|
|
239
366
|
failedAtMs: nowMs
|
|
240
367
|
});
|
|
241
368
|
case 2:
|
|
242
|
-
|
|
369
|
+
_context3.n = 3;
|
|
243
370
|
return res.json();
|
|
244
371
|
case 3:
|
|
245
|
-
body =
|
|
372
|
+
body = _context3.v;
|
|
246
373
|
keys = readKeys(body);
|
|
247
374
|
if (!(keys === null)) {
|
|
248
|
-
|
|
375
|
+
_context3.n = 4;
|
|
249
376
|
break;
|
|
250
377
|
}
|
|
251
|
-
return
|
|
378
|
+
return _context3.a(2, {
|
|
252
379
|
status: "error",
|
|
253
380
|
failedAtMs: nowMs
|
|
254
381
|
});
|
|
255
382
|
case 4:
|
|
256
|
-
return
|
|
383
|
+
return _context3.a(2, {
|
|
257
384
|
status: "keys",
|
|
258
385
|
keys: keys,
|
|
259
386
|
fetchedAtMs: nowMs
|
|
260
387
|
});
|
|
261
388
|
case 5:
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
return
|
|
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
|
-
},
|
|
396
|
+
}, _callee3, null, [[0, 5]]);
|
|
270
397
|
}))();
|
|
271
398
|
inFlight.set(url, pending);
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
412
|
+
return _context4.a(2, entry);
|
|
279
413
|
case 5:
|
|
280
|
-
|
|
414
|
+
_context4.p = 5;
|
|
281
415
|
inFlight["delete"](url);
|
|
282
|
-
return
|
|
416
|
+
return _context4.f(5);
|
|
283
417
|
case 6:
|
|
284
|
-
return
|
|
418
|
+
return _context4.a(2);
|
|
285
419
|
}
|
|
286
|
-
},
|
|
420
|
+
}, _callee4, null, [[2,, 5, 6]]);
|
|
287
421
|
}));
|
|
288
|
-
return
|
|
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(
|
|
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
|
|
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,
|
|
330
|
-
return version._regenerator().w(function (
|
|
331
|
-
while (1) switch (
|
|
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
|
-
|
|
469
|
+
_context5.n = 1;
|
|
336
470
|
break;
|
|
337
471
|
}
|
|
338
|
-
return
|
|
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
|
-
|
|
480
|
+
_context5.n = 2;
|
|
347
481
|
break;
|
|
348
482
|
}
|
|
349
|
-
return
|
|
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
|
-
|
|
488
|
+
_context5.n = 3;
|
|
355
489
|
break;
|
|
356
490
|
}
|
|
357
|
-
return
|
|
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
|
-
|
|
494
|
+
_context5.n = 4;
|
|
361
495
|
break;
|
|
362
496
|
}
|
|
363
|
-
return
|
|
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
|
-
|
|
501
|
+
_context5.n = 5;
|
|
368
502
|
return loadJwks(config, nowMs);
|
|
369
503
|
case 5:
|
|
370
|
-
|
|
371
|
-
if (!(
|
|
372
|
-
|
|
504
|
+
loaded = _context5.v;
|
|
505
|
+
if (!(loaded.entry.status === "error")) {
|
|
506
|
+
_context5.n = 6;
|
|
373
507
|
break;
|
|
374
508
|
}
|
|
375
|
-
return
|
|
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 =
|
|
511
|
+
candidates = loaded.entry.keys.filter(function (key) {
|
|
378
512
|
return isVerifyingP256Key(key, kid);
|
|
379
513
|
});
|
|
380
|
-
if (!(candidates.length === 0)) {
|
|
381
|
-
|
|
514
|
+
if (!(candidates.length === 0 && kid !== null && loaded.fromCache)) {
|
|
515
|
+
_context5.n = 9;
|
|
382
516
|
break;
|
|
383
517
|
}
|
|
384
|
-
|
|
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
|
-
|
|
544
|
+
_context5.n = 11;
|
|
389
545
|
break;
|
|
390
546
|
}
|
|
391
|
-
return
|
|
392
|
-
case
|
|
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
|
-
|
|
557
|
+
_context5.n = 12;
|
|
401
558
|
break;
|
|
402
559
|
}
|
|
403
|
-
return
|
|
404
|
-
case
|
|
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
|
-
|
|
566
|
+
_context5.n = 13;
|
|
410
567
|
break;
|
|
411
568
|
}
|
|
412
|
-
return
|
|
413
|
-
case
|
|
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
|
-
|
|
574
|
+
_context5.n = 14;
|
|
418
575
|
break;
|
|
419
576
|
}
|
|
420
|
-
return
|
|
421
|
-
case
|
|
577
|
+
return _context5.a(2, invalidToken("The access token has no expiry."));
|
|
578
|
+
case 14:
|
|
422
579
|
if (!(payload.exp + tolerance <= nowSeconds)) {
|
|
423
|
-
|
|
580
|
+
_context5.n = 15;
|
|
424
581
|
break;
|
|
425
582
|
}
|
|
426
|
-
return
|
|
427
|
-
case
|
|
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
|
-
|
|
586
|
+
_context5.n = 16;
|
|
430
587
|
break;
|
|
431
588
|
}
|
|
432
|
-
return
|
|
433
|
-
case
|
|
589
|
+
return _context5.a(2, invalidToken("The access token is not valid yet."));
|
|
590
|
+
case 16:
|
|
434
591
|
if (!(payload.iss !== config.issuer)) {
|
|
435
|
-
|
|
592
|
+
_context5.n = 17;
|
|
436
593
|
break;
|
|
437
594
|
}
|
|
438
|
-
return
|
|
439
|
-
case
|
|
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
|
-
|
|
598
|
+
_context5.n = 18;
|
|
442
599
|
break;
|
|
443
600
|
}
|
|
444
|
-
return
|
|
445
|
-
case
|
|
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
|
-
|
|
605
|
+
_context5.n = 19;
|
|
449
606
|
break;
|
|
450
607
|
}
|
|
451
|
-
return
|
|
452
|
-
case
|
|
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
|
-
|
|
612
|
+
_context5.n = 20;
|
|
456
613
|
break;
|
|
457
614
|
}
|
|
458
|
-
return
|
|
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
|
|
464
|
-
return
|
|
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
|
-
},
|
|
630
|
+
}, _callee5);
|
|
474
631
|
}));
|
|
475
632
|
return _verifyValAccessToken.apply(this, arguments);
|
|
476
633
|
}
|