@valbuild/next 0.121.0 → 0.123.0
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 +161 -0
- package/client/dist/valbuild-next-client.cjs.dev.js +3 -4
- package/client/dist/valbuild-next-client.cjs.prod.js +3 -4
- package/client/dist/valbuild-next-client.esm.js +1 -2
- package/dist/{ValApp-e17eb734.esm.js → ValApp-074c8289.esm.js} +1 -1
- package/dist/ValApp-17eb6e93.cjs.js +7 -0
- package/dist/{ValApp-90c4171d.cjs.prod.js → ValApp-17eb6e93.cjs.prod.js} +1 -1
- package/dist/{ValApp-c1c3ef55.cjs.dev.js → ValApp-2bdb460a.cjs.dev.js} +1 -1
- package/dist/{ValNextProvider-f206d2ea.cjs.dev.js → ValNextProvider-016a494b.cjs.dev.js} +1 -1
- package/dist/{ValNextProvider-588f2ff0.esm.js → ValNextProvider-32156acd.esm.js} +1 -1
- package/dist/ValNextProvider-72d3561b.cjs.js +7 -0
- package/dist/{ValNextProvider-bd3f89d3.cjs.prod.js → ValNextProvider-72d3561b.cjs.prod.js} +1 -1
- package/dist/declarations/src/server/index.d.ts +1 -1
- package/dist/declarations/src/server/initValMcp.d.ts +19 -54
- package/dist/{fallbackColors-5ead2c26.esm.js → fallbackColors-77aeedf8.esm.js} +2 -2
- package/dist/{fallbackColors-d4a5b0da.cjs.prod.js → fallbackColors-8842243c.cjs.prod.js} +2 -2
- package/dist/{fallbackColors-9a2ac53a.cjs.dev.js → fallbackColors-a491e700.cjs.dev.js} +2 -2
- package/dist/{routeFromVal-bd69625f.esm.js → routeFromVal-263eec96.esm.js} +11 -2
- package/dist/{routeFromVal-8032f5f7.cjs.prod.js → routeFromVal-8ac2f984.cjs.prod.js} +15 -5
- package/dist/{routeFromVal-7df20dfe.cjs.dev.js → routeFromVal-b48a454a.cjs.dev.js} +15 -5
- package/dist/valbuild-next.cjs.dev.js +2 -2
- package/dist/valbuild-next.cjs.prod.js +2 -2
- package/dist/valbuild-next.esm.js +2 -2
- package/package.json +7 -6
- package/rsc/dist/valbuild-next-rsc.cjs.dev.js +2 -3
- package/rsc/dist/valbuild-next-rsc.cjs.prod.js +2 -3
- package/rsc/dist/valbuild-next-rsc.esm.js +1 -2
- package/server/dist/valbuild-next-server.cjs.dev.js +14 -1014
- package/server/dist/valbuild-next-server.cjs.prod.js +14 -1014
- package/server/dist/valbuild-next-server.esm.js +15 -1015
- package/dist/ValApp-90c4171d.cjs.js +0 -7
- package/dist/ValNextProvider-bd3f89d3.cjs.js +0 -7
- package/dist/declarations/src/server/valAccessToken.d.ts +0 -93
- package/dist/declarations/src/server/valMcpMetadata.d.ts +0 -47
- package/dist/typeof-16428c61.cjs.prod.js +0 -13
- package/dist/typeof-a1531d8f.esm.js +0 -11
- package/dist/typeof-b568f48f.cjs.dev.js +0 -13
|
@@ -9,8 +9,7 @@ var objectSpread2 = require('../../dist/objectSpread2-58024783.cjs.dev.js');
|
|
|
9
9
|
var core = require('@valbuild/core');
|
|
10
10
|
var server = require('@valbuild/server');
|
|
11
11
|
var server$1 = require('next/server');
|
|
12
|
-
var
|
|
13
|
-
var node_crypto = require('node:crypto');
|
|
12
|
+
var mcp = require('@valbuild/mcp');
|
|
14
13
|
require('../../dist/unsupportedIterableToArray-c8ab77c9.cjs.dev.js');
|
|
15
14
|
require('../../dist/defineProperty-f90345d7.cjs.dev.js');
|
|
16
15
|
|
|
@@ -123,1027 +122,28 @@ function initValServer(valModules, config, nextConfig) {
|
|
|
123
122
|
}
|
|
124
123
|
|
|
125
124
|
/**
|
|
126
|
-
*
|
|
127
|
-
* resource server rather than a relay.
|
|
125
|
+
* Val's tools over MCP, bound to Next.
|
|
128
126
|
*
|
|
129
|
-
* The
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* profile at all.
|
|
127
|
+
* The MCP endpoint itself — the tools, the request guards, the access-token
|
|
128
|
+
* verification — lives in `@valbuild/mcp`, which knows nothing about Next. All
|
|
129
|
+
* that is left here is the one thing this package can answer and that one
|
|
130
|
+
* cannot: which version of `@valbuild/next` is running, which
|
|
131
|
+
* `initHandlerOptions` insists on before it will build a proxy-mode config.
|
|
135
132
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* preference: version 6 is ESM-only (`"type": "module"`, no CJS export). This
|
|
140
|
-
* package is built by preconstruct and `require`d by Next.js server code, so an
|
|
141
|
-
* ESM-only dependency here is a runtime failure in consumers' apps, not a build
|
|
142
|
-
* inconvenience. Adding it would also put a dependency in every install of
|
|
143
|
-
* `@valbuild/next` for one function.
|
|
144
|
-
*
|
|
145
|
-
* The actual cryptography is still not hand-rolled — `node:crypto` does the
|
|
146
|
-
* ECDSA and the JWK import. What is written here is the JWS envelope and the
|
|
147
|
-
* claim checks, and the rules that keep that safe are worth stating because
|
|
148
|
-
* this repository has already shipped the counterexample (`decodeJwt`: `exp`
|
|
149
|
-
* never checked, a non-constant-time compare, verification skippable):
|
|
150
|
-
*
|
|
151
|
-
* - **`alg` is pinned**, not read from the token. The header is only consulted
|
|
152
|
-
* for `kid`. A verifier that honours the token's own `alg` can be handed
|
|
153
|
-
* `HS256` and will treat the *published* public key as a shared secret.
|
|
154
|
-
* - **Nothing is read from the payload before the signature verifies.** Claims
|
|
155
|
-
* from an unverified token are attacker input.
|
|
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.
|
|
161
|
-
* - **ECDSA JWS signatures are raw `r||s`** (RFC 7518), not DER, which is what
|
|
162
|
-
* `dsaEncoding: "ieee-p1363"` below is for. Omit it and every valid signature
|
|
163
|
-
* is rejected — or worse, a future change makes it accept the wrong thing.
|
|
164
|
-
*/
|
|
165
|
-
|
|
166
|
-
var DEFAULT_CLOCK_TOLERANCE_SECONDS = 60;
|
|
167
|
-
/** How long a fetched key set is reused before it is fetched again. */
|
|
168
|
-
var JWKS_TTL_MS = 5 * 60 * 1000;
|
|
169
|
-
/**
|
|
170
|
-
* How long to wait before re-fetching after a failure.
|
|
171
|
-
*
|
|
172
|
-
* Shorter than the success TTL so a key rotation recovers quickly, but not zero:
|
|
173
|
-
* an unreachable issuer must not turn every tool call into another request to
|
|
174
|
-
* it.
|
|
175
|
-
*/
|
|
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;
|
|
192
|
-
/**
|
|
193
|
-
* One cache per issuer, and it has to outlive the request or it is not a cache:
|
|
194
|
-
* a fetch per tool call would put a network round trip in front of every read.
|
|
195
|
-
*/
|
|
196
|
-
var jwksCache = new Map();
|
|
197
|
-
/** Concurrent misses share one fetch rather than starting several. */
|
|
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();
|
|
208
|
-
function jwksUrl(issuer) {
|
|
209
|
-
// Not discovered from the issuer's metadata document, deliberately:
|
|
210
|
-
// discovery would mean one more request on the hot path and one more thing
|
|
211
|
-
// that can be pointed elsewhere. The location is fixed by convention and by
|
|
212
|
-
// Val's own authorization server.
|
|
213
|
-
return new URL("/.well-known/jwks.json", issuer).toString();
|
|
214
|
-
}
|
|
215
|
-
function loadJwks(_x, _x2) {
|
|
216
|
-
return _loadJwks.apply(this, arguments);
|
|
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
|
-
*/
|
|
237
|
-
function _loadJwks() {
|
|
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) {
|
|
242
|
-
case 0:
|
|
243
|
-
url = jwksUrl(config.issuer);
|
|
244
|
-
cached = jwksCache.get(url);
|
|
245
|
-
if (!cached) {
|
|
246
|
-
_context.n = 1;
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
age = cached.status === "keys" ? nowMs - cached.fetchedAtMs : nowMs - cached.failedAtMs;
|
|
250
|
-
ttl = cached.status === "keys" ? JWKS_TTL_MS : JWKS_ERROR_TTL_MS;
|
|
251
|
-
if (!(age < ttl)) {
|
|
252
|
-
_context.n = 1;
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
255
|
-
return _context.a(2, {
|
|
256
|
-
entry: cached,
|
|
257
|
-
fromCache: true
|
|
258
|
-
});
|
|
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);
|
|
292
|
-
existing = inFlight.get(url);
|
|
293
|
-
if (!existing) {
|
|
294
|
-
_context2.n = 1;
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
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);
|
|
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:
|
|
345
|
-
fetchImpl = (_config$fetchImpl = config.fetchImpl) !== null && _config$fetchImpl !== void 0 ? _config$fetchImpl : fetch;
|
|
346
|
-
pending = version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee3() {
|
|
347
|
-
var res, body, keys;
|
|
348
|
-
return version._regenerator().w(function (_context3) {
|
|
349
|
-
while (1) switch (_context3.p = _context3.n) {
|
|
350
|
-
case 0:
|
|
351
|
-
_context3.p = 0;
|
|
352
|
-
_context3.n = 1;
|
|
353
|
-
return fetchImpl(url, {
|
|
354
|
-
headers: {
|
|
355
|
-
accept: "application/json"
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
case 1:
|
|
359
|
-
res = _context3.v;
|
|
360
|
-
if (res.ok) {
|
|
361
|
-
_context3.n = 2;
|
|
362
|
-
break;
|
|
363
|
-
}
|
|
364
|
-
return _context3.a(2, {
|
|
365
|
-
status: "error",
|
|
366
|
-
failedAtMs: nowMs
|
|
367
|
-
});
|
|
368
|
-
case 2:
|
|
369
|
-
_context3.n = 3;
|
|
370
|
-
return res.json();
|
|
371
|
-
case 3:
|
|
372
|
-
body = _context3.v;
|
|
373
|
-
keys = readKeys(body);
|
|
374
|
-
if (!(keys === null)) {
|
|
375
|
-
_context3.n = 4;
|
|
376
|
-
break;
|
|
377
|
-
}
|
|
378
|
-
return _context3.a(2, {
|
|
379
|
-
status: "error",
|
|
380
|
-
failedAtMs: nowMs
|
|
381
|
-
});
|
|
382
|
-
case 4:
|
|
383
|
-
return _context3.a(2, {
|
|
384
|
-
status: "keys",
|
|
385
|
-
keys: keys,
|
|
386
|
-
fetchedAtMs: nowMs
|
|
387
|
-
});
|
|
388
|
-
case 5:
|
|
389
|
-
_context3.p = 5;
|
|
390
|
-
_context3.v;
|
|
391
|
-
return _context3.a(2, {
|
|
392
|
-
status: "error",
|
|
393
|
-
failedAtMs: nowMs
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
}, _callee3, null, [[0, 5]]);
|
|
397
|
-
}))();
|
|
398
|
-
inFlight.set(url, pending);
|
|
399
|
-
_context4.p = 2;
|
|
400
|
-
_context4.n = 3;
|
|
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);
|
|
410
|
-
case 4:
|
|
411
|
-
jwksCache.set(url, entry);
|
|
412
|
-
return _context4.a(2, entry);
|
|
413
|
-
case 5:
|
|
414
|
-
_context4.p = 5;
|
|
415
|
-
inFlight["delete"](url);
|
|
416
|
-
return _context4.f(5);
|
|
417
|
-
case 6:
|
|
418
|
-
return _context4.a(2);
|
|
419
|
-
}
|
|
420
|
-
}, _callee4, null, [[2,, 5, 6]]);
|
|
421
|
-
}));
|
|
422
|
-
return _fetchJwks.apply(this, arguments);
|
|
423
|
-
}
|
|
424
|
-
function readKeys(body) {
|
|
425
|
-
if (_typeof._typeof(body) !== "object" || body === null || !("keys" in body)) {
|
|
426
|
-
return null;
|
|
427
|
-
}
|
|
428
|
-
// `in` narrows the property into the type, so no assertion is needed to read
|
|
429
|
-
// it — and the `Array.isArray` below is what actually establishes the shape.
|
|
430
|
-
var keys = body.keys;
|
|
431
|
-
if (!Array.isArray(keys)) {
|
|
432
|
-
return null;
|
|
433
|
-
}
|
|
434
|
-
return keys.filter(function (key) {
|
|
435
|
-
return _typeof._typeof(key) === "object" && key !== null;
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Read `Authorization: Bearer …`.
|
|
441
|
-
*
|
|
442
|
-
* Exported because the refusal needs to know whether a token was presented at
|
|
443
|
-
* all: RFC 6750 distinguishes "no credential" — a bare `401`, which is an
|
|
444
|
-
* invitation to authenticate — from "a bad credential", and a client that gets
|
|
445
|
-
* the second when it deserved the first will not start the authorization flow.
|
|
446
|
-
*/
|
|
447
|
-
function readBearerToken(request) {
|
|
448
|
-
var _match$;
|
|
449
|
-
var header = request.headers.get("authorization");
|
|
450
|
-
if (!header) {
|
|
451
|
-
return null;
|
|
452
|
-
}
|
|
453
|
-
var match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
454
|
-
var token = match === null || match === void 0 || (_match$ = match[1]) === null || _match$ === void 0 ? void 0 : _match$.trim();
|
|
455
|
-
return token ? token : null;
|
|
456
|
-
}
|
|
457
|
-
function verifyValAccessToken(_x7, _x8) {
|
|
458
|
-
return _verifyValAccessToken.apply(this, arguments);
|
|
459
|
-
}
|
|
460
|
-
function _verifyValAccessToken() {
|
|
461
|
-
_verifyValAccessToken = version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee5(request, config) {
|
|
462
|
-
var _config$clockToleranc;
|
|
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) {
|
|
466
|
-
case 0:
|
|
467
|
-
token = readBearerToken(request);
|
|
468
|
-
if (!(token === null)) {
|
|
469
|
-
_context5.n = 1;
|
|
470
|
-
break;
|
|
471
|
-
}
|
|
472
|
-
return _context5.a(2, {
|
|
473
|
-
status: "refused",
|
|
474
|
-
error: "invalid_request",
|
|
475
|
-
description: "This Val MCP endpoint needs an access token. Authorize with the Val authorization server and present it as `Authorization: Bearer`."
|
|
476
|
-
});
|
|
477
|
-
case 1:
|
|
478
|
-
parts = token.split(".");
|
|
479
|
-
if (!(parts.length !== 3)) {
|
|
480
|
-
_context5.n = 2;
|
|
481
|
-
break;
|
|
482
|
-
}
|
|
483
|
-
return _context5.a(2, invalidToken("The access token is not a JWS."));
|
|
484
|
-
case 2:
|
|
485
|
-
_parts = slicedToArray._slicedToArray(parts, 3), encodedHeader = _parts[0], encodedPayload = _parts[1], encodedSignature = _parts[2];
|
|
486
|
-
header = decodeJsonSegment(encodedHeader);
|
|
487
|
-
if (!(header === null)) {
|
|
488
|
-
_context5.n = 3;
|
|
489
|
-
break;
|
|
490
|
-
}
|
|
491
|
-
return _context5.a(2, invalidToken("The access token's header could not be read."));
|
|
492
|
-
case 3:
|
|
493
|
-
if (!(header.alg !== "ES256")) {
|
|
494
|
-
_context5.n = 4;
|
|
495
|
-
break;
|
|
496
|
-
}
|
|
497
|
-
return _context5.a(2, invalidToken("The access token is not signed with ES256, which is the only algorithm this server accepts."));
|
|
498
|
-
case 4:
|
|
499
|
-
kid = typeof header.kid === "string" ? header.kid : null;
|
|
500
|
-
nowMs = Date.now();
|
|
501
|
-
_context5.n = 5;
|
|
502
|
-
return loadJwks(config, nowMs);
|
|
503
|
-
case 5:
|
|
504
|
-
loaded = _context5.v;
|
|
505
|
-
if (!(loaded.entry.status === "error")) {
|
|
506
|
-
_context5.n = 6;
|
|
507
|
-
break;
|
|
508
|
-
}
|
|
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."));
|
|
510
|
-
case 6:
|
|
511
|
-
candidates = loaded.entry.keys.filter(function (key) {
|
|
512
|
-
return isVerifyingP256Key(key, kid);
|
|
513
|
-
});
|
|
514
|
-
if (!(candidates.length === 0 && kid !== null && loaded.fromCache)) {
|
|
515
|
-
_context5.n = 9;
|
|
516
|
-
break;
|
|
517
|
-
}
|
|
518
|
-
_context5.n = 7;
|
|
519
|
-
return refetchForUnknownKid(config, nowMs);
|
|
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:
|
|
542
|
-
signature = decodeBase64Url(encodedSignature);
|
|
543
|
-
if (!(signature === null)) {
|
|
544
|
-
_context5.n = 11;
|
|
545
|
-
break;
|
|
546
|
-
}
|
|
547
|
-
return _context5.a(2, invalidToken("The access token's signature could not be read."));
|
|
548
|
-
case 11:
|
|
549
|
-
signedData = Buffer.from("".concat(encodedHeader, ".").concat(encodedPayload), "ascii"); // Every published key is tried when the token names no `kid`, so a rotation
|
|
550
|
-
// that has not yet propagated to clients still verifies. With a `kid` the
|
|
551
|
-
// filter above leaves one — or, if this process had not yet seen that key,
|
|
552
|
-
// the one the refetch just found.
|
|
553
|
-
verified = candidates.some(function (key) {
|
|
554
|
-
return verifyWithJwk(key, signedData, signature);
|
|
555
|
-
});
|
|
556
|
-
if (verified) {
|
|
557
|
-
_context5.n = 12;
|
|
558
|
-
break;
|
|
559
|
-
}
|
|
560
|
-
return _context5.a(2, invalidToken("The access token's signature could not be verified."));
|
|
561
|
-
case 12:
|
|
562
|
-
// Only now: everything below reads the payload, and before this line it was
|
|
563
|
-
// attacker input.
|
|
564
|
-
payload = decodeJsonSegment(encodedPayload);
|
|
565
|
-
if (!(payload === null)) {
|
|
566
|
-
_context5.n = 13;
|
|
567
|
-
break;
|
|
568
|
-
}
|
|
569
|
-
return _context5.a(2, invalidToken("The access token's payload could not be read."));
|
|
570
|
-
case 13:
|
|
571
|
-
tolerance = (_config$clockToleranc = config.clockToleranceSeconds) !== null && _config$clockToleranc !== void 0 ? _config$clockToleranc : DEFAULT_CLOCK_TOLERANCE_SECONDS;
|
|
572
|
-
nowSeconds = Math.floor(nowMs / 1000);
|
|
573
|
-
if (!(typeof payload.exp !== "number")) {
|
|
574
|
-
_context5.n = 14;
|
|
575
|
-
break;
|
|
576
|
-
}
|
|
577
|
-
return _context5.a(2, invalidToken("The access token has no expiry."));
|
|
578
|
-
case 14:
|
|
579
|
-
if (!(payload.exp + tolerance <= nowSeconds)) {
|
|
580
|
-
_context5.n = 15;
|
|
581
|
-
break;
|
|
582
|
-
}
|
|
583
|
-
return _context5.a(2, invalidToken("The access token has expired. Refresh it and try again."));
|
|
584
|
-
case 15:
|
|
585
|
-
if (!(typeof payload.nbf === "number" && payload.nbf - tolerance > nowSeconds)) {
|
|
586
|
-
_context5.n = 16;
|
|
587
|
-
break;
|
|
588
|
-
}
|
|
589
|
-
return _context5.a(2, invalidToken("The access token is not valid yet."));
|
|
590
|
-
case 16:
|
|
591
|
-
if (!(payload.iss !== config.issuer)) {
|
|
592
|
-
_context5.n = 17;
|
|
593
|
-
break;
|
|
594
|
-
}
|
|
595
|
-
return _context5.a(2, invalidToken("The access token was not issued by this server's authorization server (iss claim)."));
|
|
596
|
-
case 17:
|
|
597
|
-
if (audienceMatches(payload.aud, config.resource)) {
|
|
598
|
-
_context5.n = 18;
|
|
599
|
-
break;
|
|
600
|
-
}
|
|
601
|
-
return _context5.a(2, invalidToken("The access token is not valid for this server (aud claim)."));
|
|
602
|
-
case 18:
|
|
603
|
-
subject = payload.sub;
|
|
604
|
-
if (!(typeof subject !== "string" || subject.length === 0)) {
|
|
605
|
-
_context5.n = 19;
|
|
606
|
-
break;
|
|
607
|
-
}
|
|
608
|
-
return _context5.a(2, invalidToken("The access token has no subject."));
|
|
609
|
-
case 19:
|
|
610
|
-
scopes = readScopes(payload.scope);
|
|
611
|
-
if (scopes.includes(server.VAL_SCOPE_READ)) {
|
|
612
|
-
_context5.n = 20;
|
|
613
|
-
break;
|
|
614
|
-
}
|
|
615
|
-
return _context5.a(2, {
|
|
616
|
-
status: "refused",
|
|
617
|
-
error: "insufficient_scope",
|
|
618
|
-
description: "The access token does not have the ".concat(server.VAL_SCOPE_READ, " scope, so it cannot read any content.")
|
|
619
|
-
});
|
|
620
|
-
case 20:
|
|
621
|
-
return _context5.a(2, {
|
|
622
|
-
status: "ok",
|
|
623
|
-
auth: {
|
|
624
|
-
type: "verified-profile",
|
|
625
|
-
profileId: server.authorIdFromVerifiedSubject(subject),
|
|
626
|
-
scopes: scopes
|
|
627
|
-
}
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
}, _callee5);
|
|
631
|
-
}));
|
|
632
|
-
return _verifyValAccessToken.apply(this, arguments);
|
|
633
|
-
}
|
|
634
|
-
function invalidToken(description) {
|
|
635
|
-
// Described by class, never by echoing the token or a raw error: a
|
|
636
|
-
// verification failure message is a place credentials leak into logs.
|
|
637
|
-
return {
|
|
638
|
-
status: "refused",
|
|
639
|
-
error: "invalid_token",
|
|
640
|
-
description: description
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* `aud` is a string or an array of strings (RFC 7519 section 4.1.3).
|
|
646
|
-
*
|
|
647
|
-
* A match on any member is a match, which is the spec's own rule — a token may
|
|
648
|
-
* legitimately be addressed to several resources.
|
|
649
|
-
*/
|
|
650
|
-
function audienceMatches(claim, resource) {
|
|
651
|
-
if (typeof claim === "string") {
|
|
652
|
-
return claim === resource;
|
|
653
|
-
}
|
|
654
|
-
if (Array.isArray(claim)) {
|
|
655
|
-
return claim.some(function (entry) {
|
|
656
|
-
return entry === resource;
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
return false;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* `scope` is a space-delimited string (RFC 6749 section 3.3).
|
|
664
|
-
*
|
|
665
|
-
* Anything else is read as no scopes rather than coerced. A token whose scope
|
|
666
|
-
* claim is the wrong shape is a token we do not understand, and understanding
|
|
667
|
-
* it generously is how a write gets authorized by an array someone sent.
|
|
668
|
-
*/
|
|
669
|
-
function readScopes(claim) {
|
|
670
|
-
if (typeof claim !== "string") {
|
|
671
|
-
return [];
|
|
672
|
-
}
|
|
673
|
-
return claim.split(" ").filter(function (scope) {
|
|
674
|
-
return scope.length > 0;
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
function isVerifyingP256Key(key, kid) {
|
|
678
|
-
if (key.kty !== "EC" || key.crv !== "P-256") {
|
|
679
|
-
return false;
|
|
680
|
-
}
|
|
681
|
-
if (typeof key.x !== "string" || typeof key.y !== "string") {
|
|
682
|
-
return false;
|
|
683
|
-
}
|
|
684
|
-
// A key published for encryption is not a key to verify signatures with, and
|
|
685
|
-
// an `alg` that disagrees with what we verify is a key meant for something
|
|
686
|
-
// else.
|
|
687
|
-
if (key.use !== undefined && key.use !== "sig") {
|
|
688
|
-
return false;
|
|
689
|
-
}
|
|
690
|
-
if (key.alg !== undefined && key.alg !== "ES256") {
|
|
691
|
-
return false;
|
|
692
|
-
}
|
|
693
|
-
if (kid !== null && typeof key.kid === "string" && key.kid !== kid) {
|
|
694
|
-
return false;
|
|
695
|
-
}
|
|
696
|
-
return true;
|
|
697
|
-
}
|
|
698
|
-
function verifyWithJwk(key, signedData, signature) {
|
|
699
|
-
try {
|
|
700
|
-
var publicKey = node_crypto.createPublicKey({
|
|
701
|
-
key: {
|
|
702
|
-
kty: "EC",
|
|
703
|
-
crv: "P-256",
|
|
704
|
-
x: String(key.x),
|
|
705
|
-
y: String(key.y)
|
|
706
|
-
},
|
|
707
|
-
format: "jwk"
|
|
708
|
-
});
|
|
709
|
-
return node_crypto.verify("sha256", signedData,
|
|
710
|
-
// `ieee-p1363` because a JWS ECDSA signature is the raw `r||s` pair, while
|
|
711
|
-
// node defaults to DER for EC keys. Getting this wrong rejects every
|
|
712
|
-
// valid signature.
|
|
713
|
-
{
|
|
714
|
-
key: publicKey,
|
|
715
|
-
dsaEncoding: "ieee-p1363"
|
|
716
|
-
}, signature);
|
|
717
|
-
} catch (_unused) {
|
|
718
|
-
// A malformed key in an otherwise good key set should not take down
|
|
719
|
-
// verification against the other keys.
|
|
720
|
-
return false;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
function decodeBase64Url(segment) {
|
|
724
|
-
if (segment === undefined || !/^[A-Za-z0-9_-]*$/.test(segment)) {
|
|
725
|
-
return null;
|
|
726
|
-
}
|
|
727
|
-
try {
|
|
728
|
-
return Buffer.from(segment, "base64url");
|
|
729
|
-
} catch (_unused2) {
|
|
730
|
-
return null;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
function decodeJsonSegment(segment) {
|
|
734
|
-
var decoded = decodeBase64Url(segment);
|
|
735
|
-
if (decoded === null) {
|
|
736
|
-
return null;
|
|
737
|
-
}
|
|
738
|
-
try {
|
|
739
|
-
var parsed = JSON.parse(decoded.toString("utf8"));
|
|
740
|
-
if (_typeof._typeof(parsed) !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
741
|
-
return null;
|
|
742
|
-
}
|
|
743
|
-
return objectSpread2._objectSpread2({}, parsed);
|
|
744
|
-
} catch (_unused3) {
|
|
745
|
-
return null;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
/**
|
|
750
|
-
* The one document an MCP client needs before it can authorize: RFC 9728
|
|
751
|
-
* Protected Resource Metadata, served by the *resource* server.
|
|
752
|
-
*
|
|
753
|
-
* This is how a client discovers where to authorize. It asks the resource — this
|
|
754
|
-
* app — and the resource names its authorization server. Which is why this
|
|
755
|
-
* belongs here and the RFC 8414 *authorization server* metadata does not: that
|
|
756
|
-
* document lives at the issuer, describes the issuer's own endpoints, and is
|
|
757
|
-
* served by the issuer. An app serving a copy would be asserting the issuer's
|
|
758
|
-
* configuration on its behalf, and would be wrong the moment the issuer changed
|
|
759
|
-
* anything.
|
|
760
|
-
*
|
|
761
|
-
* The flow, so the split reads as a whole:
|
|
762
|
-
*
|
|
763
|
-
* 1. client → `{app}/api/mcp` with no token → `401` naming this document
|
|
764
|
-
* 2. client → `{app}/.well-known/oauth-protected-resource` → the issuer
|
|
765
|
-
* 3. client → `{issuer}/.well-known/oauth-authorization-server` → endpoints
|
|
766
|
-
* 4. client → issuer's `/authorize`, then `/token`
|
|
767
|
-
* 5. client → `{app}/api/mcp` with the token
|
|
133
|
+
* Kept exported from `@valbuild/next/server` because that is where it has
|
|
134
|
+
* always been. New code can import `initValMcp` from `@valbuild/mcp` directly
|
|
135
|
+
* and pass `versions: { next }` itself; there is no difference in behaviour.
|
|
768
136
|
*/
|
|
769
|
-
|
|
770
|
-
var CORS_HEADERS = {
|
|
771
|
-
// The document is public and contains no secrets — it exists to be read by
|
|
772
|
-
// clients whose origin we cannot know in advance, so `*` is the correct value
|
|
773
|
-
// rather than a lazy one. Note there is no `Access-Control-Allow-Credentials`:
|
|
774
|
-
// with it, `*` would be rejected by browsers, and this document is never
|
|
775
|
-
// fetched with credentials.
|
|
776
|
-
"Access-Control-Allow-Origin": "*",
|
|
777
|
-
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
778
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization, MCP-Protocol-Version",
|
|
779
|
-
"Access-Control-Max-Age": "3600"
|
|
780
|
-
};
|
|
781
|
-
function createValMcpMetadata(oauth, scopesSupported) {
|
|
782
|
-
var document = {
|
|
783
|
-
// The resource identifier, which MUST be the value clients send as
|
|
784
|
-
// `resource` and the value that arrives back in `aud`. Same string as the
|
|
785
|
-
// audience this app verifies against — one value, so the two cannot drift.
|
|
786
|
-
resource: oauth.resource,
|
|
787
|
-
authorization_servers: [oauth.issuer],
|
|
788
|
-
scopes_supported: scopesSupported,
|
|
789
|
-
bearer_methods_supported: ["header"]
|
|
790
|
-
};
|
|
791
|
-
var body = JSON.stringify(document);
|
|
792
|
-
return {
|
|
793
|
-
GET: function GET() {
|
|
794
|
-
return new Response(body, {
|
|
795
|
-
status: 200,
|
|
796
|
-
headers: objectSpread2._objectSpread2({
|
|
797
|
-
"Content-Type": "application/json",
|
|
798
|
-
// Cacheable: it changes only when the app is reconfigured, and a
|
|
799
|
-
// client that re-reads it on every authorization costs a round trip
|
|
800
|
-
// for nothing.
|
|
801
|
-
"Cache-Control": "public, max-age=3600"
|
|
802
|
-
}, CORS_HEADERS)
|
|
803
|
-
});
|
|
804
|
-
},
|
|
805
|
-
OPTIONS: function OPTIONS() {
|
|
806
|
-
return new Response(null, {
|
|
807
|
-
status: 204,
|
|
808
|
-
headers: CORS_HEADERS
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
};
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
/**
|
|
815
|
-
* The `WWW-Authenticate` value for a refusal (RFC 6750 section 3, RFC 9728
|
|
816
|
-
* section 5.1).
|
|
817
|
-
*
|
|
818
|
-
* `resource_metadata` is the load-bearing parameter: it is how a client that has
|
|
819
|
-
* never seen this server learns where to authorize. A `401` without it is a dead
|
|
820
|
-
* end — the client knows it needs a token and has no way to find out from where.
|
|
821
|
-
*/
|
|
822
|
-
function wwwAuthenticate(oauth, scopesSupported, refusal) {
|
|
823
|
-
var metadataUrl = new URL("/.well-known/oauth-protected-resource", oauth.resource).toString();
|
|
824
|
-
var params = ["resource_metadata=\"".concat(metadataUrl, "\""), "scope=\"".concat(scopesSupported.join(" "), "\"")];
|
|
825
|
-
if (refusal) {
|
|
826
|
-
params.push("error=\"".concat(refusal.error, "\""));
|
|
827
|
-
params.push("error_description=\"".concat(headerSafe(refusal.description), "\""));
|
|
828
|
-
}
|
|
829
|
-
return "Bearer ".concat(params.join(", "));
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
/**
|
|
833
|
-
* Make a string safe to put inside a quoted header parameter.
|
|
834
|
-
*
|
|
835
|
-
* Three classes go, and the third is the one that matters most:
|
|
836
|
-
*
|
|
837
|
-
* - a **quote** would close the parameter early;
|
|
838
|
-
* - a **backslash** would start an escape the rest of the value does not
|
|
839
|
-
* finish;
|
|
840
|
-
* - a **CR or LF** would end the header line, which is response splitting — an
|
|
841
|
-
* attacker-influenced description could inject a header of their own, or a
|
|
842
|
-
* whole second response.
|
|
843
|
-
*
|
|
844
|
-
* The descriptions passed here today are all literals from this package and
|
|
845
|
-
* contain none of it. That is a property of today's callers rather than of the
|
|
846
|
-
* type, and this function exists so it stays true when a future one interpolates
|
|
847
|
-
* something from a request.
|
|
848
|
-
*/
|
|
849
|
-
function headerSafe(value) {
|
|
850
|
-
// eslint-disable-next-line no-control-regex -- the point is to remove them
|
|
851
|
-
return value.replace(/["\\]/g, "").replace(/[\u0000-\u001f\u007f]/g, " ");
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* Val's tools over MCP, and the two checks that have to happen before a request
|
|
856
|
-
* gets to them.
|
|
857
|
-
*
|
|
858
|
-
* Nothing here imports an MCP SDK. The app owns the transport — which SDK, which
|
|
859
|
-
* route, which framework — and this owns the parts that must not be re-decided
|
|
860
|
-
* per app: whether the request is allowed to reach the tools at all, and whose
|
|
861
|
-
* credential it carries. `docs/plans/mcp.md` Part A has the reasoning; the short
|
|
862
|
-
* version is that the SDK reorganised itself once already, and the security
|
|
863
|
-
* checks should not move when it does again.
|
|
864
|
-
*/
|
|
865
|
-
|
|
866
137
|
function initValMcp(valModules, config, opts) {
|
|
867
|
-
var route = "/api/val"; // TODO: get from config, as initValServer does
|
|
868
|
-
var coreVersion = core.Internal.VERSION.core;
|
|
869
|
-
if (!coreVersion) {
|
|
870
|
-
throw new Error("Could not get @valbuild/core package version");
|
|
871
|
-
}
|
|
872
138
|
var nextVersion = version.VERSION;
|
|
873
139
|
if (!nextVersion) {
|
|
874
140
|
throw new Error("Could not get @valbuild/next package version");
|
|
875
141
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
// on a promise with no handler attached becomes an unhandledRejection and
|
|
880
|
-
// takes the dev server down, and the error is reported per request below
|
|
881
|
-
// anyway.
|
|
882
|
-
var setupPromise = version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee() {
|
|
883
|
-
var options;
|
|
884
|
-
return version._regenerator().w(function (_context) {
|
|
885
|
-
while (1) switch (_context.n) {
|
|
886
|
-
case 0:
|
|
887
|
-
_context.n = 1;
|
|
888
|
-
return server.initHandlerOptions(route, objectSpread2._objectSpread2(objectSpread2._objectSpread2({}, config), {}, {
|
|
889
|
-
versions: {
|
|
890
|
-
core: coreVersion,
|
|
891
|
-
next: nextVersion
|
|
892
|
-
}
|
|
893
|
-
}), config);
|
|
894
|
-
case 1:
|
|
895
|
-
options = _context.v;
|
|
896
|
-
return _context.a(2, {
|
|
897
|
-
mode: options.mode,
|
|
898
|
-
tools: server.createValTools(valModules, objectSpread2._objectSpread2(objectSpread2._objectSpread2({}, options), {}, {
|
|
899
|
-
formatter: opts === null || opts === void 0 ? void 0 : opts.formatter
|
|
900
|
-
}))
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
}, _callee);
|
|
904
|
-
}))();
|
|
905
|
-
setupPromise["catch"](function () {
|
|
906
|
-
// handled per request
|
|
907
|
-
});
|
|
908
|
-
var oauth = opts === null || opts === void 0 ? void 0 : opts.oauth;
|
|
909
|
-
var scopesSupported = [server.VAL_SCOPE_READ, server.VAL_SCOPE_WRITE];
|
|
910
|
-
return {
|
|
911
|
-
valMcpMetadata: oauth ? createValMcpMetadata(oauth, scopesSupported) : null,
|
|
912
|
-
valMcpTools: function valMcpTools() {
|
|
913
|
-
return version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee2() {
|
|
914
|
-
return version._regenerator().w(function (_context2) {
|
|
915
|
-
while (1) switch (_context2.n) {
|
|
916
|
-
case 0:
|
|
917
|
-
_context2.n = 1;
|
|
918
|
-
return setupPromise;
|
|
919
|
-
case 1:
|
|
920
|
-
return _context2.a(2, _context2.v.tools);
|
|
921
|
-
}
|
|
922
|
-
}, _callee2);
|
|
923
|
-
}))();
|
|
924
|
-
},
|
|
925
|
-
valMcpAuthorize: function valMcpAuthorize(request) {
|
|
926
|
-
return version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee3() {
|
|
927
|
-
var setup, refusal, sessionId, verified, status, pat, _t;
|
|
928
|
-
return version._regenerator().w(function (_context3) {
|
|
929
|
-
while (1) switch (_context3.p = _context3.n) {
|
|
930
|
-
case 0:
|
|
931
|
-
_context3.p = 0;
|
|
932
|
-
_context3.n = 1;
|
|
933
|
-
return setupPromise;
|
|
934
|
-
case 1:
|
|
935
|
-
setup = _context3.v;
|
|
936
|
-
_context3.n = 3;
|
|
937
|
-
break;
|
|
938
|
-
case 2:
|
|
939
|
-
_context3.p = 2;
|
|
940
|
-
_t = _context3.v;
|
|
941
|
-
return _context3.a(2, {
|
|
942
|
-
status: "refused",
|
|
943
|
-
response: jsonResponse(500, {
|
|
944
|
-
error: "Val: could not start the Val MCP server",
|
|
945
|
-
details: _t instanceof Error ? _t.message : String(_t)
|
|
946
|
-
})
|
|
947
|
-
});
|
|
948
|
-
case 3:
|
|
949
|
-
if (!(request === undefined)) {
|
|
950
|
-
_context3.n = 4;
|
|
951
|
-
break;
|
|
952
|
-
}
|
|
953
|
-
return _context3.a(2, {
|
|
954
|
-
status: "refused",
|
|
955
|
-
response: jsonResponse(401, {
|
|
956
|
-
error: "Val: this MCP server needs the HTTP request to authorize a call, and none was available."
|
|
957
|
-
})
|
|
958
|
-
});
|
|
959
|
-
case 4:
|
|
960
|
-
refusal = refuseUnsafeRequest(request, setup.mode);
|
|
961
|
-
if (!refusal) {
|
|
962
|
-
_context3.n = 5;
|
|
963
|
-
break;
|
|
964
|
-
}
|
|
965
|
-
return _context3.a(2, {
|
|
966
|
-
status: "refused",
|
|
967
|
-
response: refusal
|
|
968
|
-
});
|
|
969
|
-
case 5:
|
|
970
|
-
// Not the MCP session id, in either branch below. Val's patch `sessionId`
|
|
971
|
-
// names a Val AI session, and putting an unrelated id in it would claim a
|
|
972
|
-
// relationship that does not exist.
|
|
973
|
-
sessionId = null;
|
|
974
|
-
if (!oauth) {
|
|
975
|
-
_context3.n = 8;
|
|
976
|
-
break;
|
|
977
|
-
}
|
|
978
|
-
_context3.n = 6;
|
|
979
|
-
return verifyValAccessToken(request, oauth);
|
|
980
|
-
case 6:
|
|
981
|
-
verified = _context3.v;
|
|
982
|
-
if (!(verified.status === "refused")) {
|
|
983
|
-
_context3.n = 7;
|
|
984
|
-
break;
|
|
985
|
-
}
|
|
986
|
-
// 401 for a missing or bad token, 403 once the token is good but does
|
|
987
|
-
// not carry the scope: RFC 6750 section 3.1, and the distinction is
|
|
988
|
-
// what tells a client whether to authorize again or to give up.
|
|
989
|
-
status = verified.error === "insufficient_scope" ? 403 : 401;
|
|
990
|
-
return _context3.a(2, {
|
|
991
|
-
status: "refused",
|
|
992
|
-
response: new Response(JSON.stringify({
|
|
993
|
-
error: verified.error,
|
|
994
|
-
error_description: verified.description
|
|
995
|
-
}), {
|
|
996
|
-
status: status,
|
|
997
|
-
headers: {
|
|
998
|
-
"Content-Type": "application/json",
|
|
999
|
-
"WWW-Authenticate": wwwAuthenticate(oauth, scopesSupported, {
|
|
1000
|
-
error: verified.error,
|
|
1001
|
-
description: verified.description
|
|
1002
|
-
})
|
|
1003
|
-
}
|
|
1004
|
-
})
|
|
1005
|
-
});
|
|
1006
|
-
case 7:
|
|
1007
|
-
return _context3.a(2, {
|
|
1008
|
-
status: "ok",
|
|
1009
|
-
tools: setup.tools,
|
|
1010
|
-
ctx: {
|
|
1011
|
-
auth: verified.auth,
|
|
1012
|
-
sessionId: sessionId
|
|
1013
|
-
}
|
|
1014
|
-
});
|
|
1015
|
-
case 8:
|
|
1016
|
-
pat = readBearerToken(request);
|
|
1017
|
-
return _context3.a(2, {
|
|
1018
|
-
status: "ok",
|
|
1019
|
-
tools: setup.tools,
|
|
1020
|
-
ctx: {
|
|
1021
|
-
// Passed through unverified, deliberately: without an `oauth` config
|
|
1022
|
-
// this app has no key to check anything against, so it is not the
|
|
1023
|
-
// authority on what the token may do and the registry sends it to the
|
|
1024
|
-
// backend that is. See `docs/plans/mcp.md` D.2.
|
|
1025
|
-
auth: pat === null ? null : {
|
|
1026
|
-
type: "pat",
|
|
1027
|
-
pat: pat
|
|
1028
|
-
},
|
|
1029
|
-
sessionId: sessionId
|
|
1030
|
-
}
|
|
1031
|
-
});
|
|
1032
|
-
}
|
|
1033
|
-
}, _callee3, null, [[0, 2]]);
|
|
1034
|
-
}))();
|
|
1035
|
-
}
|
|
1036
|
-
};
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
/**
|
|
1040
|
-
* The two ways this route is dangerous, both refused here.
|
|
1041
|
-
*
|
|
1042
|
-
* 1. **Local filesystem mode outside development.** In fs mode there is no
|
|
1043
|
-
* credential and no backend: the tools read and write the running process's
|
|
1044
|
-
* own working tree, and every permission check Val has lives on the other
|
|
1045
|
-
* side of a backend that is not in this path. Exposed on a deployed host,
|
|
1046
|
-
* that is unauthenticated write access to the site's content for anyone who
|
|
1047
|
-
* can reach the port. There is no configuration that makes it safe, so there
|
|
1048
|
-
* is no flag to turn this off — a project that wants MCP in production wants
|
|
1049
|
-
* proxy mode, where every call carries its caller's own token.
|
|
1050
|
-
*
|
|
1051
|
-
* 2. **A browser driving the local server.** A page on any origin can `fetch`
|
|
1052
|
-
* `http://localhost:3000/api/mcp` while a developer has the app running, and
|
|
1053
|
-
* with DNS rebinding it can do so with a `Host` of its own choosing. Neither
|
|
1054
|
-
* needs a credential in fs mode. So a cross-origin `Origin` is refused, and
|
|
1055
|
-
* in fs mode the request must actually be addressed to a loopback host.
|
|
1056
|
-
*
|
|
1057
|
-
* MCP clients are not browsers and send no `Origin`, so the check costs them
|
|
1058
|
-
* nothing.
|
|
1059
|
-
*/
|
|
1060
|
-
function refuseUnsafeRequest(request, mode) {
|
|
1061
|
-
if (mode === "fs" && process.env.NODE_ENV !== "development") {
|
|
1062
|
-
return jsonResponse(403, {
|
|
1063
|
-
error: "Val: the MCP endpoint is disabled. This project is running in local filesystem mode, where MCP calls are unauthenticated and write directly to the working tree, so it is only served in development. Configure Val for proxy mode to use MCP on a deployed host."
|
|
1064
|
-
});
|
|
1065
|
-
}
|
|
1066
|
-
var host = requestHost(request);
|
|
1067
|
-
var origin = request.headers.get("origin");
|
|
1068
|
-
if (origin !== null) {
|
|
1069
|
-
// `Origin: null` is refused along with the rest. It is the *opaque* origin —
|
|
1070
|
-
// a sandboxed iframe, a `file://` page, some redirects — so it cannot be
|
|
1071
|
-
// compared to anything, and "cannot be compared" has to mean refuse: a page
|
|
1072
|
-
// that would fail the check can otherwise pass it by arranging to have no
|
|
1073
|
-
// origin at all. Absent entirely is the case that is allowed, and that is
|
|
1074
|
-
// the one MCP clients produce.
|
|
1075
|
-
var originHost = origin === "null" ? null : hostOf(origin);
|
|
1076
|
-
if (originHost === null || host === null || originHost !== host) {
|
|
1077
|
-
return jsonResponse(403, {
|
|
1078
|
-
error: "Val: refusing a cross-origin MCP request from ".concat(JSON.stringify(origin), ". MCP clients do not send an Origin header; a browser does.")
|
|
1079
|
-
});
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
if (mode === "fs") {
|
|
1083
|
-
var hostname = host === null ? null : hostnameOf(host);
|
|
1084
|
-
if (hostname === null || !LOOPBACK_HOSTNAMES.has(hostname)) {
|
|
1085
|
-
return jsonResponse(403, {
|
|
1086
|
-
error: "Val: refusing an MCP request addressed to ".concat(JSON.stringify(host !== null && host !== void 0 ? host : "an unknown host"), ". In local filesystem mode this endpoint only answers on localhost, because a name that resolves to 127.0.0.1 is how a web page reaches a developer's own machine.")
|
|
1087
|
-
});
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
return null;
|
|
1091
|
-
}
|
|
1092
|
-
var LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
1093
|
-
|
|
1094
|
-
/**
|
|
1095
|
-
* Which host the request was addressed to, as `hostname:port`.
|
|
1096
|
-
*
|
|
1097
|
-
* `Host` only, and deliberately **not** `X-Forwarded-Host`. The forwarded header
|
|
1098
|
-
* is what a client asked for behind a proxy, but nothing stops a client sending
|
|
1099
|
-
* it directly — so preferring it hands an attacker the value both checks below
|
|
1100
|
-
* are decided on. `Host`, by contrast, a browser sets from the URL and page
|
|
1101
|
-
* script cannot override, which is exactly the property the loopback check
|
|
1102
|
-
* depends on.
|
|
1103
|
-
*
|
|
1104
|
-
* The cost is that behind a proxy that rewrites `Host`, a *browser* request
|
|
1105
|
-
* whose `Origin` is the public name no longer matches. That is acceptable: such
|
|
1106
|
-
* a request carries no personal access token, so proxy mode refuses it anyway,
|
|
1107
|
-
* and a non-browser MCP client sends no `Origin` and never reaches the
|
|
1108
|
-
* comparison. Trusting the forwarded header would need an explicit trusted-proxy
|
|
1109
|
-
* configuration, which is a bigger thing than this needs.
|
|
1110
|
-
*/
|
|
1111
|
-
function requestHost(request) {
|
|
1112
|
-
var host = request.headers.get("host");
|
|
1113
|
-
if (host) {
|
|
1114
|
-
return host.trim().toLowerCase();
|
|
1115
|
-
}
|
|
1116
|
-
// Last resort: the URL the framework saw.
|
|
1117
|
-
try {
|
|
1118
|
-
return new URL(request.url).host.toLowerCase();
|
|
1119
|
-
} catch (_unused) {
|
|
1120
|
-
return null;
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
function hostOf(origin) {
|
|
1124
|
-
try {
|
|
1125
|
-
return new URL(origin).host.toLowerCase();
|
|
1126
|
-
} catch (_unused2) {
|
|
1127
|
-
return null;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
/** Strips the port, keeping IPv6 brackets — `[::1]:3000` is hostname `[::1]`. */
|
|
1132
|
-
function hostnameOf(host) {
|
|
1133
|
-
if (host.startsWith("[")) {
|
|
1134
|
-
var end = host.indexOf("]");
|
|
1135
|
-
return end === -1 ? host : host.slice(0, end + 1);
|
|
1136
|
-
}
|
|
1137
|
-
var colon = host.indexOf(":");
|
|
1138
|
-
return colon === -1 ? host : host.slice(0, colon);
|
|
1139
|
-
}
|
|
1140
|
-
function jsonResponse(status, body) {
|
|
1141
|
-
return new Response(JSON.stringify(body), {
|
|
1142
|
-
status: status,
|
|
1143
|
-
headers: {
|
|
1144
|
-
"Content-Type": "application/json"
|
|
142
|
+
return mcp.initValMcp(valModules, config, objectSpread2._objectSpread2(objectSpread2._objectSpread2({}, opts), {}, {
|
|
143
|
+
versions: {
|
|
144
|
+
next: nextVersion
|
|
1145
145
|
}
|
|
1146
|
-
});
|
|
146
|
+
}));
|
|
1147
147
|
}
|
|
1148
148
|
|
|
1149
149
|
exports.initValMcp = initValMcp;
|