@valbuild/mcp 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.
Files changed (31) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +79 -0
  3. package/dist/asyncToGenerator-500f022f.esm.js +137 -0
  4. package/dist/asyncToGenerator-8e5c36c8.cjs.prod.js +140 -0
  5. package/dist/asyncToGenerator-c3823d62.cjs.dev.js +140 -0
  6. package/dist/declarations/src/images/imageTools.d.ts +14 -0
  7. package/dist/declarations/src/images/index.d.ts +2 -0
  8. package/dist/declarations/src/images/remoteUploadTarget.d.ts +70 -0
  9. package/dist/declarations/src/images/types.d.ts +60 -0
  10. package/dist/declarations/src/index.d.ts +24 -0
  11. package/dist/declarations/src/initValMcp.d.ts +95 -0
  12. package/dist/declarations/src/sharp/index.d.ts +46 -0
  13. package/dist/declarations/src/tools/createValTools.d.ts +47 -0
  14. package/dist/declarations/src/tools/defineTool.d.ts +79 -0
  15. package/dist/declarations/src/tools/index.d.ts +7 -0
  16. package/dist/declarations/src/tools/types.d.ts +172 -0
  17. package/dist/declarations/src/tools/writePath.d.ts +160 -0
  18. package/dist/declarations/src/valAccessToken.d.ts +93 -0
  19. package/dist/declarations/src/valMcpMetadata.d.ts +47 -0
  20. package/dist/valbuild-mcp.cjs.d.ts +2 -0
  21. package/dist/valbuild-mcp.cjs.dev.js +4399 -0
  22. package/dist/valbuild-mcp.cjs.js +7 -0
  23. package/dist/valbuild-mcp.cjs.prod.js +4399 -0
  24. package/dist/valbuild-mcp.esm.js +4381 -0
  25. package/package.json +66 -0
  26. package/sharp/dist/valbuild-mcp-sharp.cjs.d.ts +2 -0
  27. package/sharp/dist/valbuild-mcp-sharp.cjs.dev.js +157 -0
  28. package/sharp/dist/valbuild-mcp-sharp.cjs.js +7 -0
  29. package/sharp/dist/valbuild-mcp-sharp.cjs.prod.js +157 -0
  30. package/sharp/dist/valbuild-mcp-sharp.esm.js +153 -0
  31. package/sharp/package.json +4 -0
@@ -0,0 +1,4381 @@
1
+ import { _ as _asyncToGenerator, a as _regenerator } from './asyncToGenerator-500f022f.esm.js';
2
+ import { z } from 'zod';
3
+ import { createValOps, getSettings, resolveRemoteFileAuth, getFileExt, initHandlerOptions } from '@valbuild/server';
4
+ import { Internal, getSourcePathFromRoute, DEFAULT_VAL_REMOTE_HOST } from '@valbuild/core';
5
+ import { performSearch, createSearchIndex, indexModule, filterBlockingValidationErrors, describeContainerAtPath, safeParsePatch, buildDuplicatePatch, buildEmptyAtPathPatch, buildRemoveImageGalleryEntryPatch, resolveEncodeSettings, resolveSerializedSchemaAtPath, withExtension, fitWithin, isSkippedSource, ENCODE_MIME_TYPE_OF, isMimeTypeAccepted, chooseEncoded, buildImageGalleryPatch } from '@valbuild/shared/internal';
6
+ import { minimatch } from 'minimatch';
7
+ import { randomUUID, createPublicKey, verify } from 'node:crypto';
8
+ import { JSONOps, applyPatch, deepClone } from '@valbuild/core/patch';
9
+ import { result } from '@valbuild/core/fp';
10
+ import fs from 'node:fs/promises';
11
+ import path from 'node:path';
12
+
13
+ function _arrayWithHoles(r) {
14
+ if (Array.isArray(r)) return r;
15
+ }
16
+
17
+ function _iterableToArrayLimit(r, l) {
18
+ var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
19
+ if (null != t) {
20
+ var e,
21
+ n,
22
+ i,
23
+ u,
24
+ a = [],
25
+ f = !0,
26
+ o = !1;
27
+ try {
28
+ if (i = (t = t.call(r)).next, 0 === l) {
29
+ if (Object(t) !== t) return;
30
+ f = !1;
31
+ } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
32
+ } catch (r) {
33
+ o = !0, n = r;
34
+ } finally {
35
+ try {
36
+ if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
37
+ } finally {
38
+ if (o) throw n;
39
+ }
40
+ }
41
+ return a;
42
+ }
43
+ }
44
+
45
+ function _arrayLikeToArray(r, a) {
46
+ (null == a || a > r.length) && (a = r.length);
47
+ for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
48
+ return n;
49
+ }
50
+
51
+ function _unsupportedIterableToArray(r, a) {
52
+ if (r) {
53
+ if ("string" == typeof r) return _arrayLikeToArray(r, a);
54
+ var t = {}.toString.call(r).slice(8, -1);
55
+ return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
56
+ }
57
+ }
58
+
59
+ function _nonIterableRest() {
60
+ throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
61
+ }
62
+
63
+ function _slicedToArray(r, e) {
64
+ return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
65
+ }
66
+
67
+ function _toPrimitive(t, r) {
68
+ if ("object" != typeof t || !t) return t;
69
+ var e = t[Symbol.toPrimitive];
70
+ if (void 0 !== e) {
71
+ var i = e.call(t, r || "default");
72
+ if ("object" != typeof i) return i;
73
+ throw new TypeError("@@toPrimitive must return a primitive value.");
74
+ }
75
+ return ("string" === r ? String : Number)(t);
76
+ }
77
+
78
+ function _toPropertyKey(t) {
79
+ var i = _toPrimitive(t, "string");
80
+ return "symbol" == typeof i ? i : i + "";
81
+ }
82
+
83
+ function _defineProperty(e, r, t) {
84
+ return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
85
+ value: t,
86
+ enumerable: !0,
87
+ configurable: !0,
88
+ writable: !0
89
+ }) : e[r] = t, e;
90
+ }
91
+
92
+ function ownKeys(e, r) {
93
+ var t = Object.keys(e);
94
+ if (Object.getOwnPropertySymbols) {
95
+ var o = Object.getOwnPropertySymbols(e);
96
+ r && (o = o.filter(function (r) {
97
+ return Object.getOwnPropertyDescriptor(e, r).enumerable;
98
+ })), t.push.apply(t, o);
99
+ }
100
+ return t;
101
+ }
102
+ function _objectSpread2(e) {
103
+ for (var r = 1; r < arguments.length; r++) {
104
+ var t = null != arguments[r] ? arguments[r] : {};
105
+ r % 2 ? ownKeys(Object(t), !0).forEach(function (r) {
106
+ _defineProperty(e, r, t[r]);
107
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
108
+ Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
109
+ });
110
+ }
111
+ return e;
112
+ }
113
+
114
+ function _objectWithoutPropertiesLoose(r, e) {
115
+ if (null == r) return {};
116
+ var t = {};
117
+ for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
118
+ if (-1 !== e.indexOf(n)) continue;
119
+ t[n] = r[n];
120
+ }
121
+ return t;
122
+ }
123
+
124
+ function _objectWithoutProperties(e, t) {
125
+ if (null == e) return {};
126
+ var o,
127
+ r,
128
+ i = _objectWithoutPropertiesLoose(e, t);
129
+ if (Object.getOwnPropertySymbols) {
130
+ var n = Object.getOwnPropertySymbols(e);
131
+ for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]);
132
+ }
133
+ return i;
134
+ }
135
+
136
+ function _createForOfIteratorHelper(r, e) {
137
+ var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
138
+ if (!t) {
139
+ if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) {
140
+ t && (r = t);
141
+ var n = 0,
142
+ F = function () {};
143
+ return {
144
+ s: F,
145
+ n: function () {
146
+ return n >= r.length ? {
147
+ done: !0
148
+ } : {
149
+ done: !1,
150
+ value: r[n++]
151
+ };
152
+ },
153
+ e: function (r) {
154
+ throw r;
155
+ },
156
+ f: F
157
+ };
158
+ }
159
+ throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
160
+ }
161
+ var o,
162
+ a = !0,
163
+ u = !1;
164
+ return {
165
+ s: function () {
166
+ t = t.call(r);
167
+ },
168
+ n: function () {
169
+ var r = t.next();
170
+ return a = r.done, r;
171
+ },
172
+ e: function (r) {
173
+ u = !0, o = r;
174
+ },
175
+ f: function () {
176
+ try {
177
+ a || null == t.return || t.return();
178
+ } finally {
179
+ if (u) throw o;
180
+ }
181
+ }
182
+ };
183
+ }
184
+
185
+ function _arrayWithoutHoles(r) {
186
+ if (Array.isArray(r)) return _arrayLikeToArray(r);
187
+ }
188
+
189
+ function _iterableToArray(r) {
190
+ if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
191
+ }
192
+
193
+ function _nonIterableSpread() {
194
+ throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
195
+ }
196
+
197
+ function _toConsumableArray(r) {
198
+ return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread();
199
+ }
200
+
201
+ function _typeof(o) {
202
+ "@babel/helpers - typeof";
203
+
204
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
205
+ return typeof o;
206
+ } : function (o) {
207
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
208
+ }, _typeof(o);
209
+ }
210
+
211
+ /**
212
+ * How a tool is written, and what it is handed.
213
+ *
214
+ * Tools are defined with {@link defineTool} so that the handler's `args` are
215
+ * inferred from the tool's own `inputSchema`. Without that the array of tools
216
+ * would have to be typed at its widest and every handler would start by
217
+ * re-narrowing `unknown`, which is exactly where a tool and its schema drift
218
+ * apart unnoticed.
219
+ */
220
+
221
+ /** Everything a tool is allowed to reach. Deliberately narrow. */
222
+
223
+ /**
224
+ * Declare a tool, binding its handler to its input schema.
225
+ *
226
+ * The handler receives already-parsed arguments: the registry validates against
227
+ * `inputSchema` before calling, so a handler never sees input its schema would
228
+ * have rejected.
229
+ */
230
+ function defineTool(definition, _handler) {
231
+ return _objectSpread2(_objectSpread2({}, definition), {}, {
232
+ handler: function handler(args, deps) {
233
+ return _handler(args, deps);
234
+ }
235
+ });
236
+ }
237
+ function ok(data) {
238
+ return {
239
+ status: "ok",
240
+ data: data
241
+ };
242
+ }
243
+ function err(code, message) {
244
+ return {
245
+ status: "error",
246
+ code: code,
247
+ message: message
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Full-text search across a project's content, built fresh on every call.
253
+ *
254
+ * No index lifecycle, which `docs/plans/mcp.md` listed as the reason this tool
255
+ * was deferred. It turned out not to be needed, and the measurement is the
256
+ * argument — indexing `valbuild/web`, a real production site:
257
+ *
258
+ * ```
259
+ * 20 modules, 206 KB of source JSON → 162 ms
260
+ * 200 modules, 2.0 MB (the same, x10) → 1.4 s
261
+ * 1000 modules, 10 MB (the same, x50) → 7.9 s
262
+ * ```
263
+ *
264
+ * (node 22, 4-cpu container. Searching the built index is another 0.2 ms, which
265
+ * is why none of the numbers below are about searching.)
266
+ *
267
+ * Two things follow. Indexing is **linear and cheap** — a real project is a
268
+ * sixth of a second, and the 10 s default deadline is not reached until roughly
269
+ * 13 MB of content, which is far past any Val project anyone has. And loading
270
+ * the modules costs *more* than indexing them (843 ms for those same 20), which
271
+ * every tool call already pays in `loadState` — so search adds a fraction on top
272
+ * of a cost that is already sunk, rather than being the expensive thing it looks
273
+ * like.
274
+ *
275
+ * That ratio between building the index and querying it — 162 ms against
276
+ * 0.2 ms — is also why the tool takes a LIST of queries rather than one.
277
+ * Everything expensive about a call happens before the first query runs, so a
278
+ * second query against the same index is free next to a second call, which pays
279
+ * for the load and the build again. An agent exploring content asks several
280
+ * near-identical things, because the whole point of searching is not knowing
281
+ * which word the content uses; batching them turns five calls into one.
282
+ *
283
+ * It is also the wrong comparison that matters: the alternative is `get_source`
284
+ * on every module so the model can read them itself, which moves the whole
285
+ * corpus through the context window. This moves a handful of queries in and a
286
+ * page of hits back for each.
287
+ */
288
+
289
+ /** Low on purpose. See the numbers above: reaching it means something is wrong. */
290
+ var DEFAULT_TIMEOUT_MS = 10000;
291
+ var MAX_TIMEOUT_MS = 120000;
292
+
293
+ /**
294
+ * Hits per query, and high because the expensive work is already done by the
295
+ * time any of them is counted.
296
+ *
297
+ * A page-sized default made sense when the answer was a screen for a person to
298
+ * scroll. It is a model reading this, and it can filter a long list far more
299
+ * cheaply than it can ask again — a low default just buys a second call for
300
+ * content it was going to look at anyway.
301
+ */
302
+ var DEFAULT_LIMIT = 100;
303
+ var MAX_LIMIT = 500;
304
+
305
+ /**
306
+ * Queries per call.
307
+ *
308
+ * Not a cost bound — the index is built either way and each query is
309
+ * sub-millisecond. It bounds the RESPONSE, which is `queries x limit` hits at
310
+ * worst and is the only part of this that reaches a context window.
311
+ */
312
+ var MAX_QUERIES = 20;
313
+
314
+ /** How many omitted module paths to name before summarising the rest. */
315
+ var MAX_NAMED_OMISSIONS = 20;
316
+ var ModulePatternSchema = z.array(z.string()).optional().describe('Module file path globs, e.g. ["/content/blogs/**"]. Matched against the whole module file path.');
317
+
318
+ /**
319
+ * One query or several.
320
+ *
321
+ * A bare string is accepted because a caller that means one query will send one
322
+ * whatever the schema says, and refusing it teaches nothing. Normalised to a
323
+ * list here so the handler and the response have a single shape.
324
+ */
325
+ var QueriesSchema = z.union([z.string(), z.array(z.string()).min(1).max(MAX_QUERIES)]).transform(function (queries) {
326
+ return typeof queries === "string" ? [queries] : queries;
327
+ });
328
+ function searchContentTool() {
329
+ return defineTool({
330
+ name: "search_content",
331
+ title: "Search content",
332
+ description: "Find content by text across the project's Val modules, with unpublished changes applied. Returns the source paths of matching values, which get_source reads. Pass every query you have in one call: the index is built per call and querying it is thousands of times cheaper than building it, so five queries cost about what one costs. Narrow with include/exclude when you know roughly where to look — that is also the fix if a search reports omitted modules.",
333
+ inputSchema: z.object({
334
+ queries: QueriesSchema.describe("The text to search for: one string, or up to ".concat(MAX_QUERIES, " of them answered in a single pass over the index. Matches are on word prefixes, so \"blog\" finds \"blogging\". Prefer several guesses in one call over one call per guess \u2014 the content may not use the word you would.")),
335
+ include: ModulePatternSchema.describe('Only search these modules, e.g. ["/content/blogs/**"]. Everything else is skipped and is NOT reported as omitted — omissions mean the deadline was hit, not that you excluded something.'),
336
+ exclude: ModulePatternSchema.describe("Skip these modules. Applied after include."),
337
+ limit: z.number()["int"]().min(1).max(MAX_LIMIT)["default"](DEFAULT_LIMIT).describe("Maximum hits to return PER QUERY, so a call returns up to queries x limit of them. Defaults to ".concat(DEFAULT_LIMIT, "; lower it when searching a common word across many queries at once.")),
338
+ offset: z.number()["int"]().min(0)["default"](0).describe("Number of hits to skip per query, for paging."),
339
+ timeoutMs: z.number()["int"]().min(100).max(MAX_TIMEOUT_MS)["default"](DEFAULT_TIMEOUT_MS).describe("How long to spend indexing before answering with what has been indexed so far. It bounds the indexing, which every query in the call shares — not the queries, which are free. The default is enough for any ordinary project; a search that reports omitted modules is better narrowed with include than given longer.")
340
+ }),
341
+ annotations: {
342
+ readOnlyHint: true,
343
+ idempotentHint: true
344
+ }
345
+ }, /*#__PURE__*/function () {
346
+ var _ref = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(args, deps) {
347
+ var queries, selected, built;
348
+ return _regenerator().w(function (_context) {
349
+ while (1) switch (_context.n) {
350
+ case 0:
351
+ // First occurrence wins, so the answers stay in the order they were
352
+ // asked. A repeated query costs nothing to run and a duplicate page of
353
+ // hits to read, and every answer names its own query, so collapsing them
354
+ // loses nothing a caller can correlate by.
355
+ queries = _toConsumableArray(new Set(args.queries.map(function (query) {
356
+ return query.trim();
357
+ })));
358
+ if (!queries.some(function (query) {
359
+ return query === "";
360
+ })) {
361
+ _context.n = 1;
362
+ break;
363
+ }
364
+ return _context.a(2, err("invalid-args", "One of the queries is empty, so there is nothing to search for."));
365
+ case 1:
366
+ selected = selectModules(deps.state, args.include, args.exclude);
367
+ if (!(selected.length === 0)) {
368
+ _context.n = 2;
369
+ break;
370
+ }
371
+ return _context.a(2, err("not-found", args.include || args.exclude ? "No modules matched include/exclude. get_all_schema lists the module paths." : "This project has no Val modules to search."));
372
+ case 2:
373
+ built = buildWithDeadline(deps.state, selected, args.timeoutMs);
374
+ return _context.a(2, ok(_objectSpread2({
375
+ // Answered per query rather than merged, because which guess found the
376
+ // thing is most of why several were asked. One query gets a list of
377
+ // one: a response shape that changes with the arguments is a shape
378
+ // every caller has to branch on.
379
+ queries: queries.map(function (query) {
380
+ var found = performSearch(built.index, query, args.limit, args.offset);
381
+ return _objectSpread2({
382
+ query: query,
383
+ results: found.results.map(function (hit) {
384
+ return {
385
+ path: hit.path,
386
+ // What the Studio would show for this hit, so an agent and an
387
+ // editor are looking at the same thing.
388
+ label: hit.label,
389
+ moduleFilePath: moduleOf(hit.path)
390
+ };
391
+ }),
392
+ // Matches for this query across the indexed modules, not the size
393
+ // of the page above: it is how a caller knows whether narrowing or
394
+ // paging is worth it.
395
+ total: found.total
396
+ }, found.totalIsLowerBound ? {
397
+ totalIsLowerBound: true
398
+ } : {});
399
+ }),
400
+ // What was actually searched, so the totals can be read for what they
401
+ // are: counts over these modules, not over the project.
402
+ searched: {
403
+ modules: built.indexedModules,
404
+ of: selected.length
405
+ },
406
+ // Present and empty rather than absent when nothing was dropped: a
407
+ // caller should not have to tell "no omissions" from "this tool does
408
+ // not report them".
409
+ omittedModules: built.omitted.slice(0, MAX_NAMED_OMISSIONS),
410
+ omittedModuleCount: built.omitted.length,
411
+ timedOut: built.timedOut
412
+ }, built.timedOut ? {
413
+ hint: "Indexing stopped after ".concat(args.timeoutMs, "ms with ").concat(built.omitted.length, " module(s) unread, so these results are partial \u2014 for every query in this call, including the ones that found plenty. Search again with include set to the modules you care about, or use count_entries to see which of the omitted ones are large.")
414
+ } : {})));
415
+ }
416
+ }, _callee);
417
+ }));
418
+ return function (_x, _x2) {
419
+ return _ref.apply(this, arguments);
420
+ };
421
+ }());
422
+ }
423
+
424
+ /**
425
+ * Which modules this search covers.
426
+ *
427
+ * Sorted, and that is load bearing rather than tidiness: indexing stops at a
428
+ * deadline, so the ORDER decides what a partial answer contains. A stable order
429
+ * means the same call twice gives the same partial answer, and that narrowing
430
+ * with `include` predictably reaches what was dropped. Indexing whatever
431
+ * `Object.keys` happened to yield would make a timed-out search irreproducible.
432
+ */
433
+ function selectModules(state, include, exclude) {
434
+ return Object.keys(state.sources).filter(function (moduleFilePath) {
435
+ if (include && !include.some(function (p) {
436
+ return minimatch(moduleFilePath, p);
437
+ })) {
438
+ return false;
439
+ }
440
+ if (exclude && exclude.some(function (p) {
441
+ return minimatch(moduleFilePath, p);
442
+ })) {
443
+ return false;
444
+ }
445
+ return true;
446
+ }).sort().map(function (moduleFilePath) {
447
+ return moduleFilePath;
448
+ });
449
+ }
450
+
451
+ /**
452
+ * Index until the work is done or the clock runs out.
453
+ *
454
+ * Checked BETWEEN modules, so one module is always indexed whole. That is the
455
+ * only granularity the index has — `indexModule` is atomic, and half a module
456
+ * in the index is a module whose absent half looks like content that does not
457
+ * exist. It also means a single module larger than the whole deadline cannot be
458
+ * interrupted, which the numbers say is not a real case: the largest module in
459
+ * `valbuild/web` is 290 KB of source and indexes in well under a second.
460
+ *
461
+ * The first module is always indexed, deadline or not. A search that returned
462
+ * nothing at all because the clock had already run out would be a worse answer
463
+ * than a slow one.
464
+ *
465
+ * The deadline covers the indexing and nothing else, and there is nothing else
466
+ * for it to cover: the queries run against the finished index in well under a
467
+ * millisecond each, so no number of them can be what made a call slow.
468
+ *
469
+ * Exported for its own tests: driving the deadline through the tool would mean
470
+ * a corpus big enough to take longer than the smallest timeout the schema
471
+ * allows, which is a race on a fast machine and a slow suite on any machine.
472
+ */
473
+ function buildWithDeadline(state, modules, timeoutMs) {
474
+ var index = createSearchIndex();
475
+ var deadline = Date.now() + timeoutMs;
476
+ var indexedModules = 0;
477
+ for (var i = 0; i < modules.length; i++) {
478
+ if (i > 0 && Date.now() >= deadline) {
479
+ return {
480
+ index: index,
481
+ indexedModules: indexedModules,
482
+ omitted: modules.slice(i),
483
+ timedOut: true
484
+ };
485
+ }
486
+ var moduleFilePath = modules[i];
487
+ var source = state.sources[moduleFilePath];
488
+ var schema = state.serializedSchemas[moduleFilePath];
489
+ if (source === undefined || schema === undefined) {
490
+ // A module with no schema is not searchable and is not an omission
491
+ // either: nothing was skipped for want of time.
492
+ continue;
493
+ }
494
+ indexModule(index, moduleFilePath, source, schema);
495
+ indexedModules++;
496
+ }
497
+ return {
498
+ index: index,
499
+ indexedModules: indexedModules,
500
+ omitted: [],
501
+ timedOut: false
502
+ };
503
+ }
504
+ function moduleOf(sourcePath) {
505
+ var _Internal$splitModule = Internal.splitModuleFilePathAndModulePath(sourcePath),
506
+ _Internal$splitModule2 = _slicedToArray(_Internal$splitModule, 1),
507
+ moduleFilePath = _Internal$splitModule2[0];
508
+ return moduleFilePath;
509
+ }
510
+
511
+ /**
512
+ * The tools that only read.
513
+ *
514
+ * Names match the Studio's chat tools exactly. MCP clients namespace by server,
515
+ * so there is no `val_` prefix to add, and keeping the names identical means
516
+ * converging the two definitions later is a move rather than a rename.
517
+ *
518
+ * All of these read from `deps.state`, which already has pending patches
519
+ * applied — an agent should see the content as the Studio would show it, not the
520
+ * last published version.
521
+ */
522
+
523
+ var ModuleFilePathSchema$2 = z.string().describe('Path of the Val module, e.g. "/content/pages.val.ts". Use get_all_schema to discover these.');
524
+ function readTools() {
525
+ return [searchContentTool(), defineTool({
526
+ name: "get_all_schema",
527
+ title: "Get all schemas",
528
+ description: "List every Val module in the project and its schema. Start here: the module paths this returns are what every other tool takes.",
529
+ inputSchema: z.object({}),
530
+ annotations: {
531
+ readOnlyHint: true,
532
+ idempotentHint: true
533
+ }
534
+ }, /*#__PURE__*/function () {
535
+ var _ref2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(_args, _ref) {
536
+ var state;
537
+ return _regenerator().w(function (_context) {
538
+ while (1) switch (_context.n) {
539
+ case 0:
540
+ state = _ref.state;
541
+ return _context.a(2, ok(state.serializedSchemas));
542
+ }
543
+ }, _callee);
544
+ }));
545
+ return function (_x, _x2) {
546
+ return _ref2.apply(this, arguments);
547
+ };
548
+ }()), defineTool({
549
+ name: "get_source",
550
+ title: "Get source",
551
+ description: "Read the content of one Val module, with any unpublished changes already applied.",
552
+ inputSchema: z.object({
553
+ moduleFilePath: ModuleFilePathSchema$2
554
+ }),
555
+ annotations: {
556
+ readOnlyHint: true,
557
+ idempotentHint: true
558
+ }
559
+ }, /*#__PURE__*/function () {
560
+ var _ref5 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(_ref3, _ref4) {
561
+ var moduleFilePath, state, path, source;
562
+ return _regenerator().w(function (_context2) {
563
+ while (1) switch (_context2.n) {
564
+ case 0:
565
+ moduleFilePath = _ref3.moduleFilePath;
566
+ state = _ref4.state;
567
+ path = moduleFilePath;
568
+ if (path in state.serializedSchemas) {
569
+ _context2.n = 1;
570
+ break;
571
+ }
572
+ return _context2.a(2, err("not-found", unknownModuleMessage(path, state)));
573
+ case 1:
574
+ source = state.sources[path];
575
+ return _context2.a(2, ok(source === undefined ? null : source));
576
+ }
577
+ }, _callee2);
578
+ }));
579
+ return function (_x3, _x4) {
580
+ return _ref5.apply(this, arguments);
581
+ };
582
+ }()), defineTool({
583
+ name: "get_record_keys",
584
+ title: "Get record keys",
585
+ description: "List the keys of the record or object at a path inside a module, a page at a time. Use this to enumerate entries without reading their contents — and before adding one, so you do not collide with an existing key. Fails on arrays, galleries, richtext and primitives: use count_entries for an array or richtext length, and get_source to read a gallery.",
586
+ inputSchema: z.object({
587
+ moduleFilePath: ModuleFilePathSchema$2,
588
+ path: z.array(z.string())["default"]([]).describe("Path within the module to the record or object. Empty means the module root."),
589
+ // Clamped by the schema rather than in the handler: a negative offset
590
+ // makes `slice` read from the END and a negative limit makes it drop
591
+ // the last N, so either would return a window that is not the page
592
+ // asked for while `total` alongside implied it was.
593
+ limit: z.number()["int"]().min(1)["default"](100).describe("Maximum number of keys to return."),
594
+ offset: z.number()["int"]().min(0)["default"](0).describe("Number of keys to skip, for paging.")
595
+ }),
596
+ annotations: {
597
+ readOnlyHint: true,
598
+ idempotentHint: true
599
+ }
600
+ }, /*#__PURE__*/function () {
601
+ var _ref8 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(_ref6, _ref7) {
602
+ var moduleFilePath, path, limit, offset, state, described, container, value, keys;
603
+ return _regenerator().w(function (_context3) {
604
+ while (1) switch (_context3.n) {
605
+ case 0:
606
+ moduleFilePath = _ref6.moduleFilePath, path = _ref6.path, limit = _ref6.limit, offset = _ref6.offset;
607
+ state = _ref7.state;
608
+ described = describeContainer(state, moduleFilePath, path);
609
+ if (!(described.kind !== "ok")) {
610
+ _context3.n = 1;
611
+ break;
612
+ }
613
+ return _context3.a(2, described.result);
614
+ case 1:
615
+ container = described.container, value = described.value; // Records and objects only, matching the Studio's tool of the same name.
616
+ // A gallery's keys are file paths whose bytes live elsewhere, and
617
+ // richtext blocks are positional — neither is a key set to hand back.
618
+ if (!(container !== "record" && container !== "object" || !isPlainObject(value))) {
619
+ _context3.n = 2;
620
+ break;
621
+ }
622
+ return _context3.a(2, err("invalid-args", "The value at that path is ".concat(article(container), " ").concat(container, ". get_record_keys only works on a record or an object \u2014 ").concat(container === "array" ? "use count_entries for the array length" : container === "richtext" ? "use count_entries for the number of blocks" : "use get_source to read the gallery's entries", ".")));
623
+ case 2:
624
+ keys = Object.keys(value);
625
+ return _context3.a(2, ok({
626
+ kind: container,
627
+ keys: keys.slice(offset, offset + limit),
628
+ // The unpaged size, so a caller can tell a short page from the end of
629
+ // the record without asking for another one.
630
+ total: keys.length
631
+ }));
632
+ }
633
+ }, _callee3);
634
+ }));
635
+ return function (_x5, _x6) {
636
+ return _ref8.apply(this, arguments);
637
+ };
638
+ }()), defineTool({
639
+ name: "count_entries",
640
+ title: "Count entries",
641
+ description: "Count the entries at a path inside a module — record or gallery keys, object fields, array indices, or top-level richtext blocks — without reading them. Use this to answer 'how many?' or to size a record before paging through it.",
642
+ inputSchema: z.object({
643
+ moduleFilePath: ModuleFilePathSchema$2,
644
+ path: z.array(z.string())["default"]([]).describe("Path within the module to count at. Empty means the module root.")
645
+ }),
646
+ annotations: {
647
+ readOnlyHint: true,
648
+ idempotentHint: true
649
+ }
650
+ }, /*#__PURE__*/function () {
651
+ var _ref1 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(_ref9, _ref0) {
652
+ var moduleFilePath, path, state, described, container, value;
653
+ return _regenerator().w(function (_context4) {
654
+ while (1) switch (_context4.n) {
655
+ case 0:
656
+ moduleFilePath = _ref9.moduleFilePath, path = _ref9.path;
657
+ state = _ref0.state;
658
+ described = describeContainer(state, moduleFilePath, path);
659
+ if (!(described.kind !== "ok")) {
660
+ _context4.n = 1;
661
+ break;
662
+ }
663
+ return _context4.a(2, described.result);
664
+ case 1:
665
+ container = described.container, value = described.value; // Every container `describeContainerAtPath` admits can be counted, so
666
+ // unlike get_record_keys this does not narrow further. Non-containers
667
+ // never get this far.
668
+ if (!Array.isArray(value)) {
669
+ _context4.n = 2;
670
+ break;
671
+ }
672
+ return _context4.a(2, ok({
673
+ kind: container,
674
+ count: value.length
675
+ }));
676
+ case 2:
677
+ if (!isPlainObject(value)) {
678
+ _context4.n = 3;
679
+ break;
680
+ }
681
+ return _context4.a(2, ok({
682
+ kind: container,
683
+ count: Object.keys(value).length
684
+ }));
685
+ case 3:
686
+ return _context4.a(2, err("invalid-args", "The value at that path is ".concat(article(container), " ").concat(container, ", which has nothing to count.")));
687
+ }
688
+ }, _callee4);
689
+ }));
690
+ return function (_x7, _x8) {
691
+ return _ref1.apply(this, arguments);
692
+ };
693
+ }()), defineTool({
694
+ name: "validate_content",
695
+ title: "Validate content",
696
+ description: "Check the project's content against its schemas, including unpublished changes. Returns only errors that would block publishing.",
697
+ inputSchema: z.object({
698
+ moduleFilePath: ModuleFilePathSchema$2.optional().describe("Limit the check to one module. Omit to validate everything.")
699
+ }),
700
+ annotations: {
701
+ readOnlyHint: true
702
+ }
703
+ }, /*#__PURE__*/function () {
704
+ var _ref12 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5(_ref10, _ref11) {
705
+ var moduleFilePath, ops, state, validation, fileErrors, bySourcePath, add, _i, _Object$values, moduleErrors, _i2, _Object$entries, _moduleErrors$validat, _Object$entries$_i, path, _errors, _i3, _Object$entries2, _Object$entries2$_i, _path, _errors2, blocking, unreadable, scope, errors, unreadableInScope;
706
+ return _regenerator().w(function (_context5) {
707
+ while (1) switch (_context5.n) {
708
+ case 0:
709
+ moduleFilePath = _ref10.moduleFilePath;
710
+ ops = _ref11.ops, state = _ref11.state;
711
+ _context5.n = 1;
712
+ return ops.validateSources(state.schemas, state.sources,
713
+ // Every module. The third argument filters which modules are
714
+ // validated at all, so passing the pending-patch analysis would make
715
+ // a project with no pending changes report `valid: true` without
716
+ // having checked anything. Scoping to one module, when asked, is done
717
+ // on the results below.
718
+ undefined);
719
+ case 1:
720
+ validation = _context5.v;
721
+ _context5.n = 2;
722
+ return ops.validateFiles(state.schemas, state.sources, validation.files, state.analysis.fileLastUpdatedByPatchId);
723
+ case 2:
724
+ fileErrors = _context5.v;
725
+ // Per-module results, flattened to the by-source-path shape the filter
726
+ // takes. Merged rather than overwritten: a path can pick up an error
727
+ // from validation and another from its file.
728
+ bySourcePath = {};
729
+ add = function add(path, errors) {
730
+ var _bySourcePath$path;
731
+ bySourcePath[path] = ((_bySourcePath$path = bySourcePath[path]) !== null && _bySourcePath$path !== void 0 ? _bySourcePath$path : []).concat(errors);
732
+ };
733
+ for (_i = 0, _Object$values = Object.values(validation.errors); _i < _Object$values.length; _i++) {
734
+ moduleErrors = _Object$values[_i];
735
+ for (_i2 = 0, _Object$entries = Object.entries((_moduleErrors$validat = moduleErrors.validations) !== null && _moduleErrors$validat !== void 0 ? _moduleErrors$validat : {}); _i2 < _Object$entries.length; _i2++) {
736
+ _Object$entries$_i = _slicedToArray(_Object$entries[_i2], 2), path = _Object$entries$_i[0], _errors = _Object$entries$_i[1];
737
+ add(path, _errors);
738
+ }
739
+ }
740
+ for (_i3 = 0, _Object$entries2 = Object.entries(fileErrors); _i3 < _Object$entries2.length; _i3++) {
741
+ _Object$entries2$_i = _slicedToArray(_Object$entries2[_i3], 2), _path = _Object$entries2$_i[0], _errors2 = _Object$entries2$_i[1];
742
+ add(_path, _errors2);
743
+ }
744
+
745
+ // Drops the errors the Studio would not show either: ones whose only
746
+ // effect is an offered fix. Left in, an agent would loop trying to
747
+ // "repair" content that is already publishable.
748
+ blocking = filterBlockingValidationErrors(bySourcePath, state.serializedSchemas, state.sources); // A module whose source could not be read at all has no source path to
749
+ // hang an error on, so it is reported separately rather than lost.
750
+ unreadable = Object.entries(validation.errors).filter(function (_ref13) {
751
+ var _ref14 = _slicedToArray(_ref13, 2),
752
+ moduleErrors = _ref14[1];
753
+ return moduleErrors.invalidSource;
754
+ }).map(function (_ref15) {
755
+ var _moduleErrors$invalid, _moduleErrors$invalid2;
756
+ var _ref16 = _slicedToArray(_ref15, 2),
757
+ path = _ref16[0],
758
+ moduleErrors = _ref16[1];
759
+ return {
760
+ moduleFilePath: path,
761
+ message: (_moduleErrors$invalid = (_moduleErrors$invalid2 = moduleErrors.invalidSource) === null || _moduleErrors$invalid2 === void 0 ? void 0 : _moduleErrors$invalid2.message) !== null && _moduleErrors$invalid !== void 0 ? _moduleErrors$invalid : "Invalid source"
762
+ };
763
+ });
764
+ scope = moduleFilePath;
765
+ errors = scope === undefined ? blocking : filterKeysByModule(blocking, scope);
766
+ unreadableInScope = scope === undefined ? unreadable : unreadable.filter(function (u) {
767
+ return u.moduleFilePath === scope;
768
+ });
769
+ return _context5.a(2, ok({
770
+ valid: Object.keys(errors).length === 0 && unreadableInScope.length === 0,
771
+ errors: Object.fromEntries(Object.entries(errors).map(function (_ref17) {
772
+ var _ref18 = _slicedToArray(_ref17, 2),
773
+ path = _ref18[0],
774
+ errs = _ref18[1];
775
+ return [path, errs.map(toJsonValidationError)];
776
+ })),
777
+ // Always present, empty when there are none: a caller should not have
778
+ // to tell "absent" from "empty" to decide whether content is publishable.
779
+ unreadableModules: unreadableInScope
780
+ }));
781
+ }
782
+ }, _callee5);
783
+ }));
784
+ return function (_x9, _x0) {
785
+ return _ref12.apply(this, arguments);
786
+ };
787
+ }()), defineTool({
788
+ name: "get_patches",
789
+ title: "Get patches",
790
+ description: "List the unpublished changes in the project, oldest first, with who made each one. A change reported as not applying is why a module's content may not match what publishing would produce, and why writing to that module is refused.",
791
+ inputSchema: z.object({
792
+ moduleFilePath: ModuleFilePathSchema$2.optional().describe("Limit to changes touching one module.")
793
+ }),
794
+ annotations: {
795
+ readOnlyHint: true
796
+ }
797
+ }, /*#__PURE__*/function () {
798
+ var _ref21 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee6(_ref19, _ref20) {
799
+ var moduleFilePath, state, wanted, failures, _i4, _Object$values2, unapplied, _iterator, _step, failure;
800
+ return _regenerator().w(function (_context6) {
801
+ while (1) switch (_context6.n) {
802
+ case 0:
803
+ moduleFilePath = _ref19.moduleFilePath;
804
+ state = _ref20.state;
805
+ wanted = moduleFilePath === undefined ? state.patches.patches : state.patches.patches.filter(function (p) {
806
+ return p.path === moduleFilePath;
807
+ }); // Which patches would not apply, flattened to one lookup by id: without
808
+ // this a module's content can silently differ from what publishing
809
+ // would produce, and nothing anywhere says why.
810
+ failures = new Map();
811
+ for (_i4 = 0, _Object$values2 = Object.values(state.unappliedPatches); _i4 < _Object$values2.length; _i4++) {
812
+ unapplied = _Object$values2[_i4];
813
+ _iterator = _createForOfIteratorHelper(unapplied);
814
+ try {
815
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
816
+ failure = _step.value;
817
+ failures.set(failure.patchId, failure.error.message);
818
+ }
819
+ } catch (err) {
820
+ _iterator.e(err);
821
+ } finally {
822
+ _iterator.f();
823
+ }
824
+ }
825
+ return _context6.a(2, ok(wanted.map(function (patch) {
826
+ var failure = failures.get(patch.patchId);
827
+ return _objectSpread2({
828
+ patchId: patch.patchId,
829
+ moduleFilePath: patch.path,
830
+ createdAt: patch.createdAt,
831
+ authorId: patch.authorId,
832
+ // `appliedAt` non-null means this change is already committed, so
833
+ // it is history rather than something still pending.
834
+ published: patch.appliedAt !== null,
835
+ // Always present, so "applies cleanly" is stated rather than
836
+ // inferred from the absence of a field.
837
+ appliesCleanly: failure === undefined
838
+ }, failure === undefined ? {} : {
839
+ applyError: failure
840
+ });
841
+ })));
842
+ }
843
+ }, _callee6);
844
+ }));
845
+ return function (_x1, _x10) {
846
+ return _ref21.apply(this, arguments);
847
+ };
848
+ }()), defineTool({
849
+ name: "get_source_path_from_route",
850
+ title: "Get source path from route",
851
+ description: "Given a URL path on the site, find the Val module and source path that renders it. Use this when the user names a page rather than a module.",
852
+ inputSchema: z.object({
853
+ route: z.string().describe('A route on the site, e.g. "/blog/my-post".')
854
+ }),
855
+ annotations: {
856
+ readOnlyHint: true,
857
+ idempotentHint: true
858
+ }
859
+ }, /*#__PURE__*/function () {
860
+ var _ref24 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7(_ref22, _ref23) {
861
+ var route, state, found;
862
+ return _regenerator().w(function (_context7) {
863
+ while (1) switch (_context7.n) {
864
+ case 0:
865
+ route = _ref22.route;
866
+ state = _ref23.state;
867
+ found = getSourcePathFromRoute(route, state.serializedSchemas);
868
+ if (found) {
869
+ _context7.n = 1;
870
+ break;
871
+ }
872
+ return _context7.a(2, err("not-found", "No Val module renders the route ".concat(JSON.stringify(route), ". Routes come from modules with a router configured; get_all_schema shows which have one.")));
873
+ case 1:
874
+ return _context7.a(2, ok(found));
875
+ }
876
+ }, _callee7);
877
+ }));
878
+ return function (_x11, _x12) {
879
+ return _ref24.apply(this, arguments);
880
+ };
881
+ }())];
882
+ }
883
+
884
+ /**
885
+ * Resolve a module and classify the value at a path inside it.
886
+ *
887
+ * Shared by `get_record_keys` and `count_entries` so the two cannot drift on
888
+ * what counts as a missing module, and so both map the same failure to the same
889
+ * error code: a path that is not there is `not-found`, while a path that is
890
+ * there but holds a string or an image is `invalid-args` — the caller should
891
+ * reach for a different tool, not go looking for the path again.
892
+ */
893
+ function describeContainer(state, moduleFilePath, path) {
894
+ var modulePath = moduleFilePath;
895
+ var schema = state.serializedSchemas[modulePath];
896
+ if (!schema) {
897
+ return {
898
+ kind: "error",
899
+ result: err("not-found", unknownModuleMessage(modulePath, state))
900
+ };
901
+ }
902
+ var described = describeContainerAtPath(schema, state.sources[modulePath], path);
903
+ if (described.kind === "error") {
904
+ return {
905
+ kind: "error",
906
+ result: err(described.reason === "missing" ? "not-found" : "invalid-args", described.message)
907
+ };
908
+ }
909
+ return described;
910
+ }
911
+
912
+ /** "a record", but "an object" and "an array". */
913
+ function article(container) {
914
+ return container === "object" || container === "array" ? "an" : "a";
915
+ }
916
+ function unknownModuleMessage(path, state) {
917
+ var known = Object.keys(state.serializedSchemas);
918
+ return "No Val module at ".concat(JSON.stringify(path), ". Known modules: ").concat(known.length === 0 ? "(none)" : known.join(", "));
919
+ }
920
+
921
+ /**
922
+ * Project a validation error into something JSON-safe and worth reading.
923
+ *
924
+ * `ValidationError.value` is dropped rather than serialized: it is `unknown` (so
925
+ * not `Json` to begin with) and it holds the offending source value, which can
926
+ * be arbitrarily large. A caller already has the source path and can read the
927
+ * value with `get_source` if it needs to — putting it here would bloat every
928
+ * result for the rare case that wants it.
929
+ *
930
+ * `fixes` is kept, because it names what Val already knows how to repair, which
931
+ * is directly actionable.
932
+ */
933
+ function toJsonValidationError(error) {
934
+ return {
935
+ message: error.message,
936
+ fixes: error.fixes ? _toConsumableArray(error.fixes) : [],
937
+ typeError: error.typeError === true,
938
+ schemaError: error.schemaError === true,
939
+ keyError: error.keyError === true
940
+ };
941
+ }
942
+ function isPlainObject(value) {
943
+ return _typeof(value) === "object" && value !== null && !Array.isArray(value);
944
+ }
945
+
946
+ /**
947
+ * Keep only the entries belonging to one module.
948
+ *
949
+ * Keyed by SourcePath, which begins with the module file path, so a prefix match
950
+ * is the right test — there is no per-module grouping left to index by.
951
+ */
952
+ function filterKeysByModule(record, moduleFilePath) {
953
+ var out = {};
954
+ for (var _i5 = 0, _Object$entries3 = Object.entries(record); _i5 < _Object$entries3.length; _i5++) {
955
+ var _Object$entries3$_i = _slicedToArray(_Object$entries3[_i5], 2),
956
+ path = _Object$entries3$_i[0],
957
+ value = _Object$entries3$_i[1];
958
+ if (path.startsWith(moduleFilePath)) {
959
+ out[path] = value;
960
+ }
961
+ }
962
+ return out;
963
+ }
964
+
965
+ /**
966
+ * Everything the Studio does client-side before a patch can be saved, done
967
+ * server-side.
968
+ *
969
+ * Three things had no server equivalent, and each is a way to be quietly wrong:
970
+ * where the patch id comes from, what the patch says its parent is, and whether
971
+ * the result would even be valid. `docs/plans/mcp.md` Part C is the design.
972
+ */
973
+
974
+ var jsonOps = new JSONOps();
975
+
976
+ /**
977
+ * A patch id, minted before the write is attempted.
978
+ *
979
+ * Same shape the Studio mints (a v4 UUID), and minting one that never gets used
980
+ * costs nothing — ids are not registered anywhere until a patch carries them.
981
+ */
982
+ function mintPatchId() {
983
+ // A branded string has no constructor; this is the same conversion the Studio
984
+ // and ValServer both make.
985
+ return randomUUID();
986
+ }
987
+
988
+ /**
989
+ * What the new patch should hang off.
990
+ *
991
+ * The last known patch if there is one, otherwise the current head. Note the
992
+ * asymmetry between the two backends: `ValOpsFS` ignores `parentRef` entirely
993
+ * because its append-only ordering log defines order, while `ValOpsHttp` sends
994
+ * it up as `parentPatchId` for optimistic concurrency. So a wrong value here is
995
+ * invisible locally and a conflict in production — which is why this is derived
996
+ * fresh rather than remembered.
997
+ */
998
+ function deriveParentRef(_x, _x2) {
999
+ return _deriveParentRef.apply(this, arguments);
1000
+ }
1001
+
1002
+ /**
1003
+ * Would this patch leave the content valid?
1004
+ *
1005
+ * Applied to a **clone** of the sources, never the real ones: `applyPatch`
1006
+ * mutates the document it is given, and ValOps carries a standing note that
1007
+ * add operations misbehave without a clone. Validating in place would corrupt
1008
+ * the sources every later call in this process reads.
1009
+ *
1010
+ * Server-side this is strictly better than the Studio's speculative check.
1011
+ * `getSchemas()` returns real `Schema` instances, so the user's own `validate`
1012
+ * closures run — and those are not carried by the serialized schema the browser
1013
+ * has, which means the browser cannot run them at all.
1014
+ */
1015
+ function _deriveParentRef() {
1016
+ _deriveParentRef = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(ops,
1017
+ // Only the ids matter, so this accepts either shape `fetchPatches` can
1018
+ // return — the metadata-only variant omits the ops but keeps the ids.
1019
+ patches) {
1020
+ var last, _t;
1021
+ return _regenerator().w(function (_context) {
1022
+ while (1) switch (_context.n) {
1023
+ case 0:
1024
+ last = patches.patches[patches.patches.length - 1];
1025
+ if (!last) {
1026
+ _context.n = 1;
1027
+ break;
1028
+ }
1029
+ return _context.a(2, {
1030
+ type: "patch",
1031
+ patchId: last.patchId
1032
+ });
1033
+ case 1:
1034
+ _context.n = 2;
1035
+ return ops.getBaseSha();
1036
+ case 2:
1037
+ _t = _context.v;
1038
+ return _context.a(2, {
1039
+ type: "head",
1040
+ headBaseSha: _t
1041
+ });
1042
+ }
1043
+ }, _callee);
1044
+ }));
1045
+ return _deriveParentRef.apply(this, arguments);
1046
+ }
1047
+ function validateSpeculatively(_x3, _x4, _x5, _x6) {
1048
+ return _validateSpeculatively.apply(this, arguments);
1049
+ }
1050
+ function _validateSpeculatively() {
1051
+ _validateSpeculatively = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(ops, state, moduleFilePath, patch) {
1052
+ var pendingFiles,
1053
+ current,
1054
+ applied,
1055
+ speculativeSources,
1056
+ after,
1057
+ before,
1058
+ existing,
1059
+ introduced,
1060
+ _args2 = arguments;
1061
+ return _regenerator().w(function (_context2) {
1062
+ while (1) switch (_context2.n) {
1063
+ case 0:
1064
+ pendingFiles = _args2.length > 4 && _args2[4] !== undefined ? _args2[4] : {};
1065
+ current = state.sources[moduleFilePath];
1066
+ if (!(current === undefined)) {
1067
+ _context2.n = 1;
1068
+ break;
1069
+ }
1070
+ return _context2.a(2, {
1071
+ status: "unapplicable",
1072
+ result: {
1073
+ status: "error",
1074
+ code: "not-found",
1075
+ message: "No Val module at ".concat(JSON.stringify(moduleFilePath), ".")
1076
+ }
1077
+ });
1078
+ case 1:
1079
+ applied = applyPatch(deepClone(current), jsonOps,
1080
+ // Without `file` ops: `JSONOps` refuses one outright ("Cannot apply a file
1081
+ // patch here"), and rightly — a file op carries bytes, not a change to the
1082
+ // document. The change those bytes belong to is the `replace` or `add`
1083
+ // beside them, which is in this list and is what there is to validate.
1084
+ patch.filter(Internal.notFileOp));
1085
+ if (!result.isErr(applied)) {
1086
+ _context2.n = 2;
1087
+ break;
1088
+ }
1089
+ return _context2.a(2, {
1090
+ status: "unapplicable",
1091
+ result: {
1092
+ status: "error",
1093
+ code: "invalid-args",
1094
+ message: "The patch cannot be applied to ".concat(moduleFilePath, ": ").concat(applied.error.message)
1095
+ }
1096
+ });
1097
+ case 2:
1098
+ speculativeSources = _objectSpread2(_objectSpread2({}, state.sources), {}, _defineProperty({}, moduleFilePath, applied.value));
1099
+ _context2.n = 3;
1100
+ return blockingErrorsIn(ops, state, speculativeSources, moduleFilePath, pendingFiles);
1101
+ case 3:
1102
+ after = _context2.v;
1103
+ if (!(after.length === 0)) {
1104
+ _context2.n = 4;
1105
+ break;
1106
+ }
1107
+ return _context2.a(2, {
1108
+ status: "valid"
1109
+ });
1110
+ case 4:
1111
+ _context2.n = 5;
1112
+ return blockingErrorsIn(ops, state, state.sources, moduleFilePath, pendingFiles);
1113
+ case 5:
1114
+ before = _context2.v;
1115
+ existing = new Set(before.map(identify));
1116
+ introduced = after.filter(function (error) {
1117
+ return !existing.has(identify(error));
1118
+ });
1119
+ if (!(introduced.length === 0)) {
1120
+ _context2.n = 6;
1121
+ break;
1122
+ }
1123
+ return _context2.a(2, {
1124
+ status: "valid"
1125
+ });
1126
+ case 6:
1127
+ return _context2.a(2, {
1128
+ status: "invalid",
1129
+ errors: describeErrors(introduced)
1130
+ });
1131
+ }
1132
+ }, _callee2);
1133
+ }));
1134
+ return _validateSpeculatively.apply(this, arguments);
1135
+ }
1136
+ /** Path and message together: the same message at another path is another problem. */
1137
+ function identify(error) {
1138
+ return "".concat(error.path, "\0").concat(error.message);
1139
+ }
1140
+
1141
+ /**
1142
+ * The publishing-blocking errors in one module, for a given set of sources.
1143
+ *
1144
+ * Scoped to one module by source path, which starts with the module file path.
1145
+ * Errors elsewhere in the project are somebody else's: refusing on them would
1146
+ * let the first broken module in a repo make every other module read-only.
1147
+ */
1148
+ function blockingErrorsIn(_x7, _x8, _x9, _x0, _x1) {
1149
+ return _blockingErrorsIn.apply(this, arguments);
1150
+ }
1151
+ function _blockingErrorsIn() {
1152
+ _blockingErrorsIn = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(ops, state, sources, moduleFilePath, pendingFiles) {
1153
+ var validation, fileErrors, bySourcePath, add, _i, _Object$values, moduleErrors, _i2, _Object$entries, _moduleErrors$validat, _Object$entries$_i, path, errors, _i3, _Object$entries2, _Object$entries2$_i, _path, _errors, blocking, located, _i4, _Object$entries3, _Object$entries3$_i, _path2, _errors2, _iterator, _step, error;
1154
+ return _regenerator().w(function (_context3) {
1155
+ while (1) switch (_context3.n) {
1156
+ case 0:
1157
+ _context3.n = 1;
1158
+ return ops.validateSources(state.schemas, sources,
1159
+ // Every module, deliberately -- `patchesByModule` is a FILTER on which
1160
+ // modules get validated, not context for validating them. Passing the
1161
+ // analysis from before this write skips the very module being written
1162
+ // whenever it had no pending patch, so the first change to a module went
1163
+ // unchecked; and a change that breaks a `keyOf` or a router in a *different*
1164
+ // module reports its error there, which a filtered run never visits.
1165
+ undefined);
1166
+ case 1:
1167
+ validation = _context3.v;
1168
+ _context3.n = 2;
1169
+ return ops.validateFiles(state.schemas, sources, validation.files, // The pending upload wins over the analysis, and has to: it is the newer
1170
+ // answer to "where are this path's bytes", and the analysis predates it.
1171
+ _objectSpread2(_objectSpread2({}, state.analysis.fileLastUpdatedByPatchId), pendingFiles));
1172
+ case 2:
1173
+ fileErrors = _context3.v;
1174
+ // Merged rather than overwritten: a path can pick up an error from validation
1175
+ // and another from its file.
1176
+ bySourcePath = {};
1177
+ add = function add(path, errors) {
1178
+ var _bySourcePath$path;
1179
+ bySourcePath[path] = ((_bySourcePath$path = bySourcePath[path]) !== null && _bySourcePath$path !== void 0 ? _bySourcePath$path : []).concat(errors);
1180
+ };
1181
+ for (_i = 0, _Object$values = Object.values(validation.errors); _i < _Object$values.length; _i++) {
1182
+ moduleErrors = _Object$values[_i];
1183
+ for (_i2 = 0, _Object$entries = Object.entries((_moduleErrors$validat = moduleErrors.validations) !== null && _moduleErrors$validat !== void 0 ? _moduleErrors$validat : {}); _i2 < _Object$entries.length; _i2++) {
1184
+ _Object$entries$_i = _slicedToArray(_Object$entries[_i2], 2), path = _Object$entries$_i[0], errors = _Object$entries$_i[1];
1185
+ add(path, errors);
1186
+ }
1187
+ }
1188
+ for (_i3 = 0, _Object$entries2 = Object.entries(fileErrors); _i3 < _Object$entries2.length; _i3++) {
1189
+ _Object$entries2$_i = _slicedToArray(_Object$entries2[_i3], 2), _path = _Object$entries2$_i[0], _errors = _Object$entries2$_i[1];
1190
+ add(_path, _errors);
1191
+ }
1192
+ blocking = filterBlockingValidationErrors(bySourcePath, state.serializedSchemas, sources);
1193
+ located = [];
1194
+ _i4 = 0, _Object$entries3 = Object.entries(blocking);
1195
+ case 3:
1196
+ if (!(_i4 < _Object$entries3.length)) {
1197
+ _context3.n = 6;
1198
+ break;
1199
+ }
1200
+ _Object$entries3$_i = _slicedToArray(_Object$entries3[_i4], 2), _path2 = _Object$entries3$_i[0], _errors2 = _Object$entries3$_i[1];
1201
+ if (_path2.startsWith(moduleFilePath)) {
1202
+ _context3.n = 4;
1203
+ break;
1204
+ }
1205
+ return _context3.a(3, 5);
1206
+ case 4:
1207
+ _iterator = _createForOfIteratorHelper(_errors2);
1208
+ try {
1209
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
1210
+ error = _step.value;
1211
+ located.push({
1212
+ path: _path2,
1213
+ message: error.message
1214
+ });
1215
+ }
1216
+ } catch (err) {
1217
+ _iterator.e(err);
1218
+ } finally {
1219
+ _iterator.f();
1220
+ }
1221
+ case 5:
1222
+ _i4++;
1223
+ _context3.n = 3;
1224
+ break;
1225
+ case 6:
1226
+ return _context3.a(2, located);
1227
+ }
1228
+ }, _callee3);
1229
+ }));
1230
+ return _blockingErrorsIn.apply(this, arguments);
1231
+ }
1232
+ function describeErrors(errors) {
1233
+ return errors.map(function (e) {
1234
+ return "".concat(e.path, ": ").concat(e.message);
1235
+ }).join("; ");
1236
+ }
1237
+
1238
+ /**
1239
+ * What to do when the change would leave the content invalid.
1240
+ *
1241
+ * `"reject"` for a tool that is editing existing content: an agent should not be
1242
+ * able to break a site, and a rejected patch stores nothing.
1243
+ *
1244
+ * `"report"` for a tool whose whole purpose is to create something incomplete.
1245
+ * `empty_at_path` scaffolds an entry the caller is then expected to fill in, so
1246
+ * on most real schemas — anything with a non-empty string — the value it creates
1247
+ * is invalid by construction. Rejecting that would make the tool useless on
1248
+ * exactly the schemas it exists for, so instead the patch is saved and the
1249
+ * remaining errors come back as a to-do list. This mirrors the Studio, where
1250
+ * creating an empty entry is normal and the errors show until it is filled in.
1251
+ */
1252
+
1253
+ /**
1254
+ * Put the bytes a `file` op refers to where the patch can find them.
1255
+ *
1256
+ * Called with the ids the patch is about to be written under, because that is
1257
+ * the whole reason this is a hook rather than something the caller does first:
1258
+ * an upload is keyed by the patch id, and the patch id is minted here — once
1259
+ * per attempt, so a conflict retry uploads under the id that actually lands
1260
+ * rather than orphaning the bytes under the one that did not.
1261
+ *
1262
+ * Uploads run BEFORE `createPatch`, which is the order the Studio uses and the
1263
+ * only one that works: a `file` op carries a hash, not data, so a patch stored
1264
+ * before its bytes points at nothing, and a reader that arrives in between sees
1265
+ * a broken image rather than a missing one.
1266
+ */
1267
+
1268
+ /**
1269
+ * Files uploaded for a patch that does not exist yet.
1270
+ *
1271
+ * Deliberately the analysis's own type rather than a lookalike: the whole point
1272
+ * is that these merge over `fileLastUpdatedByPatchId`, and a second shape that
1273
+ * happens to fit today would drift the first time a field is added there.
1274
+ */
1275
+
1276
+ /**
1277
+ * What a successful write reports back.
1278
+ *
1279
+ * Named, rather than left as the `Json` that `ValToolResult` widens it to, so
1280
+ * that a tool building on `savePatch` — the image tool adds its own fields to
1281
+ * this — can spread it without re-narrowing `Json` and without an assertion.
1282
+ */
1283
+
1284
+ /**
1285
+ * Upload, validate, then save — and retry once if someone else got there first.
1286
+ *
1287
+ * The retry exists because the parent ref is derived from a read that happened
1288
+ * before the write. A conflict means the chain moved underneath us, and
1289
+ * re-deriving is usually enough. Once only: a loop here would be an agent
1290
+ * fighting a human editor in the Studio, and losing slowly is worse than
1291
+ * failing clearly.
1292
+ *
1293
+ * The order of the first two is the part that is easy to get backwards. A
1294
+ * patch's `file` op carries a hash, not bytes, so validation asks the store
1295
+ * where those bytes are — and if they are not there yet, the check that was
1296
+ * supposed to say "this image is fine" says "this image is missing" and rejects
1297
+ * the write that would have uploaded it. So the bytes go up first, and the
1298
+ * upload says where it put them.
1299
+ *
1300
+ * The cost is that a REJECTED write leaves an upload no patch refers to. That
1301
+ * is a state the patch store already expects and already collects — see
1302
+ * `dropStaleUploads` in `patchStore.ts`, which exists because a browser can
1303
+ * abandon an upload the same way — so it is bounded rather than untidy, and it
1304
+ * is the cheaper of the two failures by a distance: the other one is a write
1305
+ * that cannot succeed at all.
1306
+ */
1307
+ function savePatch(_x10, _x11, _x12) {
1308
+ return _savePatch.apply(this, arguments);
1309
+ }
1310
+ function _savePatch() {
1311
+ _savePatch = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(deps, moduleFilePath, patch) {
1312
+ var _options$onInvalid, _ctx$auth;
1313
+ var options,
1314
+ ops,
1315
+ ctx,
1316
+ state,
1317
+ onInvalid,
1318
+ unapplied,
1319
+ patchId,
1320
+ parentRef,
1321
+ pendingFiles,
1322
+ uploaded,
1323
+ speculative,
1324
+ unresolved,
1325
+ authorId,
1326
+ attempt,
1327
+ saved,
1328
+ _args4 = arguments,
1329
+ _t2,
1330
+ _t3;
1331
+ return _regenerator().w(function (_context4) {
1332
+ while (1) switch (_context4.n) {
1333
+ case 0:
1334
+ options = _args4.length > 3 && _args4[3] !== undefined ? _args4[3] : {};
1335
+ ops = deps.ops, ctx = deps.ctx, state = deps.state;
1336
+ onInvalid = (_options$onInvalid = options.onInvalid) !== null && _options$onInvalid !== void 0 ? _options$onInvalid : "reject";
1337
+ unapplied = state.unappliedPatches[moduleFilePath];
1338
+ if (!(unapplied && unapplied.length > 0)) {
1339
+ _context4.n = 1;
1340
+ break;
1341
+ }
1342
+ return _context4.a(2, {
1343
+ status: "error",
1344
+ code: "internal",
1345
+ message: "Cannot write to ".concat(moduleFilePath, ": it has ").concat(unapplied.length, " pending change").concat(unapplied.length === 1 ? "" : "s", " that will not apply, so what is stored is not what publishing would produce. Resolve or discard ").concat(unapplied.map(function (u) {
1346
+ return u.patchId;
1347
+ }).join(", "), " first -- get_patches shows them.")
1348
+ });
1349
+ case 1:
1350
+ // Minted once for the whole call, not once per attempt: an id is not
1351
+ // registered anywhere until a patch carries it, so a conflicted attempt has
1352
+ // not spent it — and reusing it keeps the upload below valid instead of
1353
+ // orphaning it under an id nothing will ever refer to.
1354
+ patchId = mintPatchId();
1355
+ _context4.n = 2;
1356
+ return deriveParentRef(ops, state.patches);
1357
+ case 2:
1358
+ parentRef = _context4.v;
1359
+ pendingFiles = {};
1360
+ if (!options.uploadFiles) {
1361
+ _context4.n = 5;
1362
+ break;
1363
+ }
1364
+ _context4.n = 3;
1365
+ return options.uploadFiles({
1366
+ patchId: patchId,
1367
+ parentRef: parentRef
1368
+ });
1369
+ case 3:
1370
+ uploaded = _context4.v;
1371
+ if (!(uploaded.status === "error")) {
1372
+ _context4.n = 4;
1373
+ break;
1374
+ }
1375
+ return _context4.a(2, uploaded.result);
1376
+ case 4:
1377
+ pendingFiles = uploaded.files;
1378
+ case 5:
1379
+ _context4.n = 6;
1380
+ return validateSpeculatively(ops, state, moduleFilePath, patch, pendingFiles);
1381
+ case 6:
1382
+ speculative = _context4.v;
1383
+ if (!(speculative.status === "unapplicable")) {
1384
+ _context4.n = 7;
1385
+ break;
1386
+ }
1387
+ return _context4.a(2, speculative.result);
1388
+ case 7:
1389
+ unresolved = null;
1390
+ if (!(speculative.status === "invalid")) {
1391
+ _context4.n = 9;
1392
+ break;
1393
+ }
1394
+ if (!(onInvalid === "reject")) {
1395
+ _context4.n = 8;
1396
+ break;
1397
+ }
1398
+ return _context4.a(2, {
1399
+ status: "error",
1400
+ code: "validation-failed",
1401
+ message: "The change was rejected and nothing was saved, because it would leave the content invalid: ".concat(speculative.errors)
1402
+ });
1403
+ case 8:
1404
+ unresolved = speculative.errors;
1405
+ case 9:
1406
+ /**
1407
+ * The verified profile, or null when there was nothing to verify.
1408
+ *
1409
+ * An author is written only when somebody checked it. On the token path the
1410
+ * host verified a signature over a key it does not hold, so the profile is
1411
+ * checked rather than claimed, and the backend has no token of its own to
1412
+ * attribute from — the call reaches it under the app's API key. If this
1413
+ * stayed null there, every edit made through a signed-in editor's own session
1414
+ * would land with no author at all, which is worse than useless on a CMS
1415
+ * whose review screen is organised by who changed what.
1416
+ *
1417
+ * Null is what local filesystem mode gets, where there is no credential to
1418
+ * resolve, exactly as the Studio does locally. It is also what the removed
1419
+ * personal-access-token path got, and for a reason worth keeping in view: an
1420
+ * id derived from a credential the app cannot resolve is an unverified claim
1421
+ * dressed up as a checked one. Should another unverified credential ever
1422
+ * reach here, null remains its only honest author.
1423
+ */
1424
+ authorId = ((_ctx$auth = ctx.auth) === null || _ctx$auth === void 0 ? void 0 : _ctx$auth.type) === "verified-profile" ? ctx.auth.profileId : null;
1425
+ attempt = 0;
1426
+ case 10:
1427
+ if (!(attempt < 2)) {
1428
+ _context4.n = 18;
1429
+ break;
1430
+ }
1431
+ if (!(attempt > 0)) {
1432
+ _context4.n = 13;
1433
+ break;
1434
+ }
1435
+ _t2 = deriveParentRef;
1436
+ _t3 = ops;
1437
+ _context4.n = 11;
1438
+ return ops.fetchPatches({
1439
+ excludePatchOps: true
1440
+ });
1441
+ case 11:
1442
+ _context4.n = 12;
1443
+ return _t2(_t3, _context4.v);
1444
+ case 12:
1445
+ parentRef = _context4.v;
1446
+ case 13:
1447
+ _context4.n = 14;
1448
+ return ops.createPatch(moduleFilePath, patch, patchId, parentRef, ctx.sessionId, authorId);
1449
+ case 14:
1450
+ saved = _context4.v;
1451
+ if (!result.isOk(saved)) {
1452
+ _context4.n = 15;
1453
+ break;
1454
+ }
1455
+ return _context4.a(2, {
1456
+ status: "ok",
1457
+ data: {
1458
+ patchId: saved.value.patchId,
1459
+ moduleFilePath: moduleFilePath,
1460
+ createdAt: saved.value.createdAt,
1461
+ unresolvedValidationErrors: unresolved
1462
+ }
1463
+ });
1464
+ case 15:
1465
+ if (!(saved.error.errorType === "patch-head-conflict")) {
1466
+ _context4.n = 16;
1467
+ break;
1468
+ }
1469
+ return _context4.a(3, 17);
1470
+ case 16:
1471
+ return _context4.a(2, {
1472
+ status: "error",
1473
+ code: "internal",
1474
+ // Note the nesting: createPatch wraps the underlying flat error as
1475
+ // `{ errorType: "other", error: <that> }`.
1476
+ message: saved.error.error.message
1477
+ });
1478
+ case 17:
1479
+ attempt++;
1480
+ _context4.n = 10;
1481
+ break;
1482
+ case 18:
1483
+ return _context4.a(2, {
1484
+ status: "error",
1485
+ code: "conflict",
1486
+ message: "Another change was saved while this one was being written, twice in a row. Read the content again before retrying — it has moved."
1487
+ });
1488
+ }
1489
+ }, _callee4);
1490
+ }));
1491
+ return _savePatch.apply(this, arguments);
1492
+ }
1493
+
1494
+ /**
1495
+ * The tools that change content.
1496
+ *
1497
+ * Every one of them goes through {@link savePatch}, so they all inherit the same
1498
+ * guarantees: the change is validated against the real schemas before anything
1499
+ * is stored, a rejected change stores nothing, and a lost race with another
1500
+ * writer is retried once and then reported rather than looped on.
1501
+ *
1502
+ * Images are not here. The Studio's image tools work from a handle into Val's
1503
+ * AI session store — bytes the browser got from the vision system — and MCP has
1504
+ * no equivalent, so they need a different affordance (a local file path, or
1505
+ * inline base64) rather than a port. `docs/plans/mcp.md` Part B has the reasoning.
1506
+ */
1507
+
1508
+ var ModuleFilePathSchema$1 = z.string().describe('Path of the Val module, e.g. "/content/pages.val.ts".');
1509
+ function writeTools() {
1510
+ return [defineTool({
1511
+ name: "create_patch",
1512
+ title: "Create patch",
1513
+ description: "Change content in a Val module by applying JSON Patch operations. The change is validated first and is rejected outright if it would make the content invalid. Text and JSON values only — not files or images.",
1514
+ inputSchema: z.object({
1515
+ moduleFilePath: ModuleFilePathSchema$1,
1516
+ patch: z.array(z.unknown()).describe('JSON Patch operations, e.g. [{"op":"replace","path":["title"],"value":"New title"}]. Paths are arrays of keys, not slash-separated strings.')
1517
+ }),
1518
+ annotations: {
1519
+ idempotentHint: false
1520
+ }
1521
+ }, /*#__PURE__*/function () {
1522
+ var _ref2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(_ref, deps) {
1523
+ var moduleFilePath, patch, rejected, parsed;
1524
+ return _regenerator().w(function (_context) {
1525
+ while (1) switch (_context.n) {
1526
+ case 0:
1527
+ moduleFilePath = _ref.moduleFilePath, patch = _ref.patch;
1528
+ // Before parsing, not after: a file op that is also malformed should be
1529
+ // told that files are not supported, rather than handed a schema error
1530
+ // about the shape of a thing it was never going to be allowed to do.
1531
+ rejected = rejectFileOps(patch);
1532
+ if (!rejected) {
1533
+ _context.n = 1;
1534
+ break;
1535
+ }
1536
+ return _context.a(2, rejected);
1537
+ case 1:
1538
+ parsed = safeParsePatch(patch);
1539
+ if (!(parsed.kind !== "ok")) {
1540
+ _context.n = 2;
1541
+ break;
1542
+ }
1543
+ return _context.a(2, fromBuildResult$1(parsed));
1544
+ case 2:
1545
+ return _context.a(2, savePatch(deps, moduleFilePath, parsed.patch));
1546
+ }
1547
+ }, _callee);
1548
+ }));
1549
+ return function (_x, _x2) {
1550
+ return _ref2.apply(this, arguments);
1551
+ };
1552
+ }()), defineTool({
1553
+ name: "duplicate_source",
1554
+ title: "Duplicate source",
1555
+ description: "Copy the value at one path in a module to another path. Use this to add an entry modelled on an existing one, rather than composing it field by field.",
1556
+ inputSchema: z.object({
1557
+ moduleFilePath: ModuleFilePathSchema$1,
1558
+ sourcePath: z.array(z.string()).describe("Path of the value to copy."),
1559
+ destinationPath: z.array(z.string()).describe("Path to copy it to. Must not already exist.")
1560
+ }),
1561
+ annotations: {
1562
+ idempotentHint: false
1563
+ }
1564
+ }, /*#__PURE__*/function () {
1565
+ var _ref4 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(_ref3, deps) {
1566
+ var moduleFilePath, sourcePath, destinationPath, modulePath, schema, built;
1567
+ return _regenerator().w(function (_context2) {
1568
+ while (1) switch (_context2.n) {
1569
+ case 0:
1570
+ moduleFilePath = _ref3.moduleFilePath, sourcePath = _ref3.sourcePath, destinationPath = _ref3.destinationPath;
1571
+ modulePath = moduleFilePath;
1572
+ schema = deps.state.serializedSchemas[modulePath];
1573
+ if (schema) {
1574
+ _context2.n = 1;
1575
+ break;
1576
+ }
1577
+ return _context2.a(2, err("not-found", "No Val module at ".concat(JSON.stringify(modulePath), ".")));
1578
+ case 1:
1579
+ built = buildDuplicatePatch({
1580
+ sourcePath: sourcePath,
1581
+ destinationPath: destinationPath
1582
+ }, schema, deps.state.sources[modulePath]);
1583
+ if (!(built.kind !== "ok")) {
1584
+ _context2.n = 2;
1585
+ break;
1586
+ }
1587
+ return _context2.a(2, fromBuildResult$1(built));
1588
+ case 2:
1589
+ return _context2.a(2, savePatch(deps, modulePath, built.patch));
1590
+ }
1591
+ }, _callee2);
1592
+ }));
1593
+ return function (_x3, _x4) {
1594
+ return _ref4.apply(this, arguments);
1595
+ };
1596
+ }()), defineTool({
1597
+ name: "empty_at_path",
1598
+ title: "Create an empty value at a path",
1599
+ description: "Create a new, schema-correct empty value at a path — an empty entry in a record or array, for instance. Prefer this over composing one by hand: it derives the shape from the schema, including required fields. The value it creates is usually not yet publishable; the result lists what still needs filling in, which you can then do with create_patch.",
1600
+ inputSchema: z.object({
1601
+ moduleFilePath: ModuleFilePathSchema$1,
1602
+ destinationPath: z.array(z.string()).describe("Path to create the empty value at.")
1603
+ }),
1604
+ annotations: {
1605
+ idempotentHint: false
1606
+ }
1607
+ }, /*#__PURE__*/function () {
1608
+ var _ref6 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(_ref5, deps) {
1609
+ var moduleFilePath, destinationPath, modulePath, schema, built;
1610
+ return _regenerator().w(function (_context3) {
1611
+ while (1) switch (_context3.n) {
1612
+ case 0:
1613
+ moduleFilePath = _ref5.moduleFilePath, destinationPath = _ref5.destinationPath;
1614
+ modulePath = moduleFilePath;
1615
+ schema = deps.state.serializedSchemas[modulePath];
1616
+ if (schema) {
1617
+ _context3.n = 1;
1618
+ break;
1619
+ }
1620
+ return _context3.a(2, err("not-found", "No Val module at ".concat(JSON.stringify(modulePath), ".")));
1621
+ case 1:
1622
+ built = buildEmptyAtPathPatch({
1623
+ destinationPath: destinationPath
1624
+ }, schema, deps.state.sources[modulePath]);
1625
+ if (!(built.kind !== "ok")) {
1626
+ _context3.n = 2;
1627
+ break;
1628
+ }
1629
+ return _context3.a(2, fromBuildResult$1(built));
1630
+ case 2:
1631
+ return _context3.a(2, savePatch(deps, modulePath, built.patch, {
1632
+ onInvalid: "report"
1633
+ }));
1634
+ }
1635
+ }, _callee3);
1636
+ }));
1637
+ return function (_x5, _x6) {
1638
+ return _ref6.apply(this, arguments);
1639
+ };
1640
+ }()), defineTool({
1641
+ name: "remove_image_gallery_entry",
1642
+ title: "Remove an image gallery entry",
1643
+ description: "Remove one image from an image gallery module by its file path. This deletes the entry and the file it refers to.",
1644
+ inputSchema: z.object({
1645
+ moduleFilePath: ModuleFilePathSchema$1.describe("The gallery module, i.e. one declared with s.images() or s.files()."),
1646
+ filePath: z.string().describe('The gallery key to remove, e.g. "/public/val/photo_a1b2c.jpg".')
1647
+ }),
1648
+ // Destructive: it removes content and the underlying file, so a host
1649
+ // that asks for confirmation should ask here.
1650
+ annotations: {
1651
+ destructiveHint: true,
1652
+ idempotentHint: false
1653
+ }
1654
+ }, /*#__PURE__*/function () {
1655
+ var _ref8 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(_ref7, deps) {
1656
+ var moduleFilePath, filePath, modulePath, schema, built;
1657
+ return _regenerator().w(function (_context4) {
1658
+ while (1) switch (_context4.n) {
1659
+ case 0:
1660
+ moduleFilePath = _ref7.moduleFilePath, filePath = _ref7.filePath;
1661
+ modulePath = moduleFilePath;
1662
+ schema = deps.state.serializedSchemas[modulePath];
1663
+ if (schema) {
1664
+ _context4.n = 1;
1665
+ break;
1666
+ }
1667
+ return _context4.a(2, err("not-found", "No Val module at ".concat(JSON.stringify(modulePath), ".")));
1668
+ case 1:
1669
+ built = buildRemoveImageGalleryEntryPatch({
1670
+ filePath: filePath
1671
+ }, schema, deps.state.sources[modulePath]);
1672
+ if (!(built.kind !== "ok")) {
1673
+ _context4.n = 2;
1674
+ break;
1675
+ }
1676
+ return _context4.a(2, fromBuildResult$1(built));
1677
+ case 2:
1678
+ return _context4.a(2, savePatch(deps, modulePath, built.patch));
1679
+ }
1680
+ }, _callee4);
1681
+ }));
1682
+ return function (_x7, _x8) {
1683
+ return _ref8.apply(this, arguments);
1684
+ };
1685
+ }())];
1686
+ }
1687
+
1688
+ /**
1689
+ * Turn a helper's build failure into a tool error.
1690
+ *
1691
+ * `wrong-tool` is worth keeping distinct: the helpers can tell that the caller
1692
+ * reached for the wrong tool and which one it should have used, and passing that
1693
+ * through is what lets a model correct itself in one step instead of retrying
1694
+ * the same call.
1695
+ */
1696
+ function fromBuildResult$1(built) {
1697
+ if (built.kind === "wrong-tool") {
1698
+ return {
1699
+ status: "error",
1700
+ code: "invalid-args",
1701
+ message: "".concat(built.reason, " Use the ").concat(built.suggestedTool, " tool instead.")
1702
+ };
1703
+ }
1704
+ return {
1705
+ status: "error",
1706
+ code: "invalid-args",
1707
+ message: built.message
1708
+ };
1709
+ }
1710
+
1711
+ /**
1712
+ * File operations are refused rather than half-supported.
1713
+ *
1714
+ * A `file` op carries binary content that has to be uploaded before the patch
1715
+ * is synced — a two-phase flow this pass does not implement. Letting one through
1716
+ * would store a patch referring to bytes that were never uploaded, which fails
1717
+ * later and a long way from the cause.
1718
+ *
1719
+ * Takes the unparsed patch, so this answer does not depend on the op being
1720
+ * otherwise well formed. All it needs is the caller's own claim about what the
1721
+ * op is.
1722
+ */
1723
+ function rejectFileOps(patch) {
1724
+ var hasFileOp = patch.some(function (op) {
1725
+ return _typeof(op) === "object" && op !== null && "op" in op && op.op === "file";
1726
+ });
1727
+ if (!hasFileOp) {
1728
+ return null;
1729
+ }
1730
+ return {
1731
+ status: "error",
1732
+ code: "unsupported",
1733
+ message: "This patch contains a file operation. Uploading files is not supported over MCP yet — only text and JSON values can be changed."
1734
+ };
1735
+ }
1736
+
1737
+ /**
1738
+ * The public surface of Val's server-side tool registry.
1739
+ *
1740
+ * Types only, deliberately: this file is the contract that the MCP hosts, the
1741
+ * CLI's stdio transport and the tools themselves are all written against, and
1742
+ * keeping it free of implementation means those can be built in any order
1743
+ * without one of them owning the shape.
1744
+ *
1745
+ * The design this implements is `docs/plans/mcp.md`, Part A. Two constraints
1746
+ * from it are load-bearing and easy to break by accident:
1747
+ *
1748
+ * 1. **Nothing here may import an MCP SDK.** That is what lets hosts other
1749
+ * than the template consume these tools, and it is not hypothetical
1750
+ * hygiene — the TypeScript SDK reorganised itself at v2.0.0, and a registry
1751
+ * coupled to it would have moved with it.
1752
+ * 2. **The result type is deliberately not MCP's `CallToolResult`.** Each host
1753
+ * adapts {@link ValToolResult} at its own edge, which is also where an
1754
+ * error becomes an in-band `isError` result the model can recover from
1755
+ * rather than a transport failure.
1756
+ */
1757
+
1758
+ /** Why a tool call failed, in a form a host can map onto its own errors. */
1759
+
1760
+ /**
1761
+ * The same definition with `inputSchema` as JSON Schema, for hosts that want the
1762
+ * wire shape rather than a Standard Schema.
1763
+ *
1764
+ * Typed as whatever zod's own converter produces, so deriving it needs no cast
1765
+ * and no second hand-written description of the same input.
1766
+ */
1767
+
1768
+ /**
1769
+ * How the caller was established, and there is one acceptable answer: the host
1770
+ * **checked a signature**.
1771
+ *
1772
+ * A union of one, deliberately. It carried a second variant — a personal access
1773
+ * token relayed to the backend unchecked, on the reasoning that the app cannot
1774
+ * resolve one and the backend can. The reasoning held; the shape did not. A
1775
+ * credential the host cannot check is one it also cannot refuse, so accepting
1776
+ * one made "a deployed endpoint that authenticates nobody" a supported
1777
+ * configuration, and it let a host serve these tools without ever being told
1778
+ * where callers should authorize. The discriminant stays so that adding a
1779
+ * second *verified* kind stays a one-line change at every call site.
1780
+ */
1781
+
1782
+ /**
1783
+ * Who is calling, established once per request by the host.
1784
+ *
1785
+ * `null` means local fs mode, where there is no credential to hold and patches
1786
+ * are written with no author, exactly as the Studio does locally (D.1). In
1787
+ * proxy mode `null` is refused rather than falling back to the app's own API
1788
+ * key: that key can do more than any single user, and quietly substituting it
1789
+ * would turn a missing credential into full access.
1790
+ */
1791
+
1792
+ /**
1793
+ * Brand a verified subject as an {@link AuthorId}.
1794
+ *
1795
+ * `AuthorId` is a branded string so that an id cannot be conjured from any
1796
+ * string that happens to be lying around — which is exactly the mistake this
1797
+ * type is guarding against. That makes one assertion unavoidable at the boundary
1798
+ * where a real id enters the system, so it lives here, once, with a name that
1799
+ * says what makes it legitimate: the caller has *verified* this subject, not
1800
+ * received it.
1801
+ *
1802
+ * Do not reach for this to satisfy a type. If you are holding a string you did
1803
+ * not verify, the honest value is `null`.
1804
+ */
1805
+ function authorIdFromVerifiedSubject(subject) {
1806
+ return subject;
1807
+ }
1808
+
1809
+ /** Read access. Every call needs it, the writes included. */
1810
+ var VAL_SCOPE_READ = "val:read";
1811
+ /** Write access. Needed *in addition* by any tool not marked `readOnlyHint`. */
1812
+ var VAL_SCOPE_WRITE = "val:write";
1813
+
1814
+ /**
1815
+ * The failure half of {@link ValToolResult}, on its own.
1816
+ *
1817
+ * Named because several internal steps can only fail — speculative validation
1818
+ * rejecting a patch, an upload that did not land — and typing those as the
1819
+ * whole union means the compiler cannot see that an early return is in fact an
1820
+ * error return.
1821
+ */
1822
+
1823
+ var _excluded = ["handler"],
1824
+ _excluded2 = ["handler", "inputSchema"];
1825
+ /**
1826
+ * Val's server-side tool registry.
1827
+ *
1828
+ * This is the piece Val did not have: the Studio's chat tools are defined *and
1829
+ * executed in the browser*, against its client stores, so nothing here could be
1830
+ * re-exposed. These tools run against {@link ValOps} instead, which is what lets
1831
+ * an MCP server — or a stdio transport, or anything else — drive Val content
1832
+ * without a browser.
1833
+ *
1834
+ * Transport-agnostic on purpose: nothing under `tools/` imports an MCP SDK. A
1835
+ * host adapts {@link ValToolResult} at its own edge.
1836
+ */
1837
+ function createValTools(valModules, options) {
1838
+ var extraTools = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
1839
+ var resolveOps = createOpsResolver(valModules, options);
1840
+ var tools = [].concat(_toConsumableArray(readTools()), _toConsumableArray(writeTools()), _toConsumableArray(extraTools));
1841
+ var byName = new Map();
1842
+ var _iterator = _createForOfIteratorHelper(tools),
1843
+ _step;
1844
+ try {
1845
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
1846
+ var tool = _step.value;
1847
+ if (byName.has(tool.name)) {
1848
+ // Refused rather than resolved either way. A host that shadows
1849
+ // `get_source` with something else would be a genuinely confusing
1850
+ // afternoon for whoever debugs the agent afterwards, and "last one wins"
1851
+ // is not a rule anybody can see from the call site.
1852
+ throw new Error("Val: two tools are registered as ".concat(JSON.stringify(tool.name), ". Extra tools must not reuse a built-in tool's name."));
1853
+ }
1854
+ byName.set(tool.name, tool);
1855
+ }
1856
+ } catch (err) {
1857
+ _iterator.e(err);
1858
+ } finally {
1859
+ _iterator.f();
1860
+ }
1861
+ return {
1862
+ list: function list() {
1863
+ return tools.map(function (_ref) {
1864
+ _ref.handler;
1865
+ var definition = _objectWithoutProperties(_ref, _excluded);
1866
+ return definition;
1867
+ });
1868
+ },
1869
+ listJsonSchema: function listJsonSchema() {
1870
+ return tools.map(function (_ref2) {
1871
+ _ref2.handler;
1872
+ var inputSchema = _ref2.inputSchema,
1873
+ rest = _objectWithoutProperties(_ref2, _excluded2);
1874
+ return _objectSpread2(_objectSpread2({}, rest), {}, {
1875
+ // zod 4 derives this itself, so there is no JSON-Schema-to-zod
1876
+ // converter anywhere in the stack and no second description of the
1877
+ // same input to keep in step.
1878
+ inputSchema: z.toJSONSchema(inputSchema, {
1879
+ io: "input"
1880
+ })
1881
+ });
1882
+ });
1883
+ },
1884
+ call: function call(name, args, ctx) {
1885
+ return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
1886
+ var tool, parsed, insufficient, resolved, ops, state, deps, _t;
1887
+ return _regenerator().w(function (_context) {
1888
+ while (1) switch (_context.p = _context.n) {
1889
+ case 0:
1890
+ tool = byName.get(name);
1891
+ if (tool) {
1892
+ _context.n = 1;
1893
+ break;
1894
+ }
1895
+ return _context.a(2, {
1896
+ status: "error",
1897
+ code: "unknown-tool",
1898
+ message: "No tool named ".concat(JSON.stringify(name), ". Available: ").concat(tools.map(function (t) {
1899
+ return t.name;
1900
+ }).join(", "))
1901
+ });
1902
+ case 1:
1903
+ parsed = tool.inputSchema.safeParse(args !== null && args !== void 0 ? args : {});
1904
+ if (parsed.success) {
1905
+ _context.n = 2;
1906
+ break;
1907
+ }
1908
+ return _context.a(2, {
1909
+ status: "error",
1910
+ code: "invalid-args",
1911
+ message: describeZodError(parsed.error)
1912
+ });
1913
+ case 2:
1914
+ insufficient = refuseInsufficientScope(tool, ctx);
1915
+ if (!insufficient) {
1916
+ _context.n = 3;
1917
+ break;
1918
+ }
1919
+ return _context.a(2, insufficient);
1920
+ case 3:
1921
+ resolved = resolveOps(ctx);
1922
+ if (!(resolved.status === "error")) {
1923
+ _context.n = 4;
1924
+ break;
1925
+ }
1926
+ return _context.a(2, resolved.result);
1927
+ case 4:
1928
+ ops = resolved.ops;
1929
+ _context.p = 5;
1930
+ _context.n = 6;
1931
+ return loadState(ops);
1932
+ case 6:
1933
+ state = _context.v;
1934
+ if (!(state.status === "error")) {
1935
+ _context.n = 7;
1936
+ break;
1937
+ }
1938
+ return _context.a(2, state.result);
1939
+ case 7:
1940
+ deps = {
1941
+ ops: ops,
1942
+ config: options,
1943
+ ctx: ctx,
1944
+ state: state.state
1945
+ };
1946
+ _context.n = 8;
1947
+ return tool.handler(parsed.data, deps);
1948
+ case 8:
1949
+ return _context.a(2, _context.v);
1950
+ case 9:
1951
+ _context.p = 9;
1952
+ _t = _context.v;
1953
+ return _context.a(2, {
1954
+ status: "error",
1955
+ code: "internal",
1956
+ message: _t instanceof Error ? _t.message : String(_t)
1957
+ });
1958
+ }
1959
+ }, _callee, null, [[5, 9]]);
1960
+ }))();
1961
+ },
1962
+ dispose: function dispose() {
1963
+ return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
1964
+ return _regenerator().w(function (_context2) {
1965
+ while (1) switch (_context2.n) {
1966
+ case 0:
1967
+ return _context2.a(2);
1968
+ }
1969
+ }, _callee2);
1970
+ }))();
1971
+ } // Nothing to release today: ValOps holds no handle that needs closing, and
1972
+ // the fs watcher it can start is owned by the Studio's server. Kept in the
1973
+ // contract so hosts wire up teardown now rather than when it starts to
1974
+ // matter.
1975
+ };
1976
+ }
1977
+
1978
+ /**
1979
+ * Pick the data layer for a call, which in proxy mode means picking whose
1980
+ * credential the backend will see.
1981
+ *
1982
+ * This is the one place authorization is decided, and in proxy mode there is
1983
+ * exactly one credential it will act on: an access token whose signature,
1984
+ * issuer, audience and expiry the host verified against the authorization
1985
+ * server's published key. Anything less is refused here rather than forwarded.
1986
+ *
1987
+ * There used to be a second route — the caller's personal access token, passed
1988
+ * through unread on the reasoning that the backend, not the app, is the
1989
+ * authority on what it may do. That was true, and it was still the wrong shape:
1990
+ * it made an unauthenticated bearer token on a deployed endpoint a supported
1991
+ * configuration, and it meant `initValMcp` had a path where an app served MCP
1992
+ * without ever being told where to authorize. A host that has not verified
1993
+ * anything now gets a refusal that names the missing `oauth` config.
1994
+ *
1995
+ * What has *not* changed is why a verified token does not become the app's own
1996
+ * API key by some other name. The app authenticates to the backend with its own
1997
+ * key here, and who did what travels as the patch's `authorId` — so the
1998
+ * profile has to be one the host checked cryptographically, never one it was
1999
+ * handed. An `authenticate()` that decided a credential's rights inside the app
2000
+ * would make every bug in it full access to every project that key can reach.
2001
+ */
2002
+ function createOpsResolver(valModules, options) {
2003
+ if (options.mode === "fs") {
2004
+ // One instance, built once: fs mode is a developer's own working tree, so
2005
+ // there is no credential to vary by and no reason to re-evaluate modules.
2006
+ var ops = createValOps(valModules, options);
2007
+ return function (ctx) {
2008
+ if (ctx.auth) {
2009
+ // Refused rather than ignored. A host that thinks it is passing a
2010
+ // credential should not silently get local filesystem access instead —
2011
+ // and the difference matters, because fs mode writes straight to disk
2012
+ // with no backend permission check at all.
2013
+ //
2014
+ // A verified access token is not something the caller chose to send: it
2015
+ // only exists because this app advertised an authorization server, so
2016
+ // the developer seeing this did not do anything wrong — a config file
2017
+ // did, and naming it is the difference between a two-minute fix and an
2018
+ // afternoon.
2019
+ return {
2020
+ status: "error",
2021
+ result: {
2022
+ status: "error",
2023
+ code: "unsupported",
2024
+ message: "This Val project is running in local filesystem mode, so there is nothing to authenticate against, but it is configured with an `oauth` issuer and is therefore asking clients for an access token it cannot use. Remove the `oauth` config (or `VAL_OAUTH_ISSUER` from your local `.env`) for local development."
2025
+ }
2026
+ };
2027
+ }
2028
+ return {
2029
+ status: "ok",
2030
+ ops: ops
2031
+ };
2032
+ };
2033
+ }
2034
+
2035
+ /**
2036
+ * One instance for every verified caller, and that is correct rather than a
2037
+ * shortcut: this instance authenticates with the app's own API key, so there
2038
+ * is nothing per-caller in it to keep apart. Who did what travels as the
2039
+ * patch's `authorId` instead — see `writePath`.
2040
+ *
2041
+ * One instance is also all proxy mode keeps now. It could not share while a
2042
+ * personal access token reached this function: each token needed its own
2043
+ * `ValOpsHttp` to hold it, each of those cached the project's evaluated
2044
+ * modules, and the bounded cache that kept that memory in check turned an
2045
+ * eviction into a re-evaluation of every module on the next call.
2046
+ */
2047
+ var sharedOps = null;
2048
+ return function (ctx) {
2049
+ var _ctx$auth;
2050
+ if (((_ctx$auth = ctx.auth) === null || _ctx$auth === void 0 ? void 0 : _ctx$auth.type) !== "verified-profile") {
2051
+ return {
2052
+ status: "error",
2053
+ result: {
2054
+ status: "error",
2055
+ code: "forbidden",
2056
+ message: "This Val project talks to the Val content backend, so every call needs an access token from the Val authorization server. If this endpoint is not asking clients to authorize, it has no `oauth` config — give `initValMcp` one, or run the project in local filesystem mode for development."
2057
+ }
2058
+ };
2059
+ }
2060
+ if (!options.apiKey) {
2061
+ // Proxy mode is inferred from the api key being present, so this is
2062
+ // unreachable through `initHandlerOptions`. It stays because the
2063
+ // alternative to refusing is building ops with no credential at all.
2064
+ return {
2065
+ status: "error",
2066
+ result: {
2067
+ status: "error",
2068
+ code: "forbidden",
2069
+ message: "This Val project has no API key configured, so a verified access token cannot be exchanged for backend access."
2070
+ }
2071
+ };
2072
+ }
2073
+ if (!sharedOps) {
2074
+ sharedOps = createValOps(valModules, options);
2075
+ }
2076
+ return {
2077
+ status: "ok",
2078
+ ops: sharedOps
2079
+ };
2080
+ };
2081
+ }
2082
+ /**
2083
+ * The content as the caller should see it, loaded once per call.
2084
+ *
2085
+ * Pending patches are applied, because an agent looking at a project mid-edit
2086
+ * should see what the Studio would show rather than the last published state.
2087
+ *
2088
+ * Deliberately not cached across calls. In fs mode a save recomputes the base
2089
+ * sha within the same process, so a cached view would go stale silently — and
2090
+ * the cost of being wrong here is an agent writing a patch against content that
2091
+ * has already moved.
2092
+ *
2093
+ * Exported for `toolsFixture`, not for consumers: `tools/index.ts` re-exports by
2094
+ * name, so this stays inside the package. A test that assembled its own state
2095
+ * would be asserting against a view no real call ever sees.
2096
+ */
2097
+ function loadState(_x) {
2098
+ return _loadState.apply(this, arguments);
2099
+ }
2100
+ function _loadState() {
2101
+ _loadState = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(ops) {
2102
+ var patches, analysis, sourcesRes, _yield$Promise$all, _yield$Promise$all2, schemas, serializedSchemas;
2103
+ return _regenerator().w(function (_context3) {
2104
+ while (1) switch (_context3.n) {
2105
+ case 0:
2106
+ _context3.n = 1;
2107
+ return ops.fetchPatches({
2108
+ excludePatchOps: false
2109
+ });
2110
+ case 1:
2111
+ patches = _context3.v;
2112
+ if (!patches.unauthorized) {
2113
+ _context3.n = 2;
2114
+ break;
2115
+ }
2116
+ return _context3.a(2, {
2117
+ status: "error",
2118
+ result: {
2119
+ status: "error",
2120
+ code: "forbidden",
2121
+ message: "Not authorized to read this project's pending changes. Check that the credential is valid and has access."
2122
+ }
2123
+ });
2124
+ case 2:
2125
+ if (!patches.networkError) {
2126
+ _context3.n = 3;
2127
+ break;
2128
+ }
2129
+ return _context3.a(2, {
2130
+ status: "error",
2131
+ result: {
2132
+ status: "error",
2133
+ code: "internal",
2134
+ message: "Could not reach the Val content backend."
2135
+ }
2136
+ });
2137
+ case 3:
2138
+ if (!patches.error) {
2139
+ _context3.n = 4;
2140
+ break;
2141
+ }
2142
+ return _context3.a(2, {
2143
+ status: "error",
2144
+ result: {
2145
+ status: "error",
2146
+ code: "internal",
2147
+ message: patches.error.message
2148
+ }
2149
+ });
2150
+ case 4:
2151
+ analysis = ops.analyzePatches(patches.patches); // getSourcesWithPatchesApplied, not getSources(analysis): the latter returns
2152
+ // only the modules that had patches, and validating that subset reports
2153
+ // spurious errors for anything that looks across modules, like keyOf or a
2154
+ // router.
2155
+ _context3.n = 5;
2156
+ return ops.getSourcesWithPatchesApplied(_objectSpread2(_objectSpread2({}, analysis), patches));
2157
+ case 5:
2158
+ sourcesRes = _context3.v;
2159
+ _context3.n = 6;
2160
+ return Promise.all([ops.getSchemas(), ops.getSerializedSchemas()]);
2161
+ case 6:
2162
+ _yield$Promise$all = _context3.v;
2163
+ _yield$Promise$all2 = _slicedToArray(_yield$Promise$all, 2);
2164
+ schemas = _yield$Promise$all2[0];
2165
+ serializedSchemas = _yield$Promise$all2[1];
2166
+ return _context3.a(2, {
2167
+ status: "ok",
2168
+ state: {
2169
+ schemas: schemas,
2170
+ serializedSchemas: serializedSchemas,
2171
+ sources: sourcesRes.sources,
2172
+ patches: patches,
2173
+ analysis: analysis,
2174
+ // Which modules hold a pending patch that would not apply. Carried rather
2175
+ // than discarded because their `sources` silently lack that change: the
2176
+ // content here is not what publishing would produce, so a write against
2177
+ // it would be based on a state that does not exist. See
2178
+ // `unappliedPatchesFor`.
2179
+ unappliedPatches: sourcesRes.errors
2180
+ }
2181
+ });
2182
+ }
2183
+ }, _callee3);
2184
+ }));
2185
+ return _loadState.apply(this, arguments);
2186
+ }
2187
+ function describeZodError(error) {
2188
+ return error.issues.map(function (issue) {
2189
+ var path = issue.path.join(".");
2190
+ return path ? "".concat(path, ": ").concat(issue.message) : issue.message;
2191
+ }).join("; ");
2192
+ }
2193
+
2194
+ /**
2195
+ * Refuse a call the token was not granted, before anything is attempted.
2196
+ *
2197
+ * Derived from `readOnlyHint` rather than from a second list of tool names,
2198
+ * because a second list is a thing that drifts. The derivation also fails in
2199
+ * the safe direction: a tool that forgets the hint is treated as a write and
2200
+ * demands the wider scope, rather than a write slipping through as a read.
2201
+ *
2202
+ * The early return is a call carrying no verified credential, and there is no
2203
+ * scope to check because nothing granted one. In Val's own host that means
2204
+ * local filesystem mode, where a project writing a developer's own working tree
2205
+ * has no wider authority to withhold. A host assembling its own context can
2206
+ * also reach it with an unauthenticated proxy-mode call — refused a few lines
2207
+ * later, by `resolveOps`, for the credential rather than the scope. Every other
2208
+ * caller arrives as a verified profile, carrying the scopes its token was
2209
+ * issued with.
2210
+ */
2211
+ function refuseInsufficientScope(tool, ctx) {
2212
+ var _ctx$auth2, _tool$annotations;
2213
+ if (((_ctx$auth2 = ctx.auth) === null || _ctx$auth2 === void 0 ? void 0 : _ctx$auth2.type) !== "verified-profile") {
2214
+ return null;
2215
+ }
2216
+ // Read is needed by every call, including the writes: a tool that changes
2217
+ // content reads it first, and `ValToolAuth` says as much. Checking only the
2218
+ // wider scope would let a write-but-not-read token through here — today's
2219
+ // verifier refuses such a token before this point, but `createValTools` is
2220
+ // exported and another host may not.
2221
+ var needed = (_tool$annotations = tool.annotations) !== null && _tool$annotations !== void 0 && _tool$annotations.readOnlyHint ? [VAL_SCOPE_READ] : [VAL_SCOPE_READ, VAL_SCOPE_WRITE];
2222
+ var granted = ctx.auth.scopes;
2223
+ var missing = needed.filter(function (scope) {
2224
+ return !granted.includes(scope);
2225
+ });
2226
+ if (missing.length === 0) {
2227
+ return null;
2228
+ }
2229
+ return {
2230
+ status: "error",
2231
+ code: "forbidden",
2232
+ message: "This access token does not have the ".concat(missing.join(" and "), " scope, which ").concat(tool.name, " requires. Granted: ").concat(granted.length > 0 ? granted.join(" ") : "(none)", ".")
2233
+ };
2234
+ }
2235
+
2236
+ /**
2237
+ * Where a remote image goes, and what its ref has to say.
2238
+ *
2239
+ * Only one thing here needs the network: a project's `publicProjectId` and its
2240
+ * list of buckets, which `getSettings` answers. Everything else is arithmetic
2241
+ * over bytes the caller already handed us.
2242
+ *
2243
+ * And **no credential comes from the MCP caller**, in either mode. The rule is
2244
+ * `resolveRemoteFileAuth` in `@valbuild/server`, shared with the Studio's own
2245
+ * api routes: the app's api key where there is one, and otherwise — local
2246
+ * development — the developer's own `val login` token off disk. That is the
2247
+ * same precondition `val validate --fix` has, and the same one the Studio has
2248
+ * when it uploads a remote image from a laptop.
2249
+ *
2250
+ * What this does NOT do is upload anything to the content host. A remote
2251
+ * image's bytes go into the patch store like any other pending file, and the
2252
+ * push to `remote.val.build` happens at publish, from
2253
+ * `ValOpsFS.saveOrUploadFiles(mode: "upload-remote")`. See
2254
+ * `docs/plans/mcp-remote-images.md` Part A.
2255
+ */
2256
+
2257
+ /**
2258
+ * Resolves the project's remote-file settings, once.
2259
+ *
2260
+ * Cached for the life of the process because the answer does not move — a
2261
+ * project's public id never changes and its bucket list changes when someone
2262
+ * adds a bucket, which is not something an agent's upload should pay a network
2263
+ * round trip to notice. Held as the promise rather than the value so that two
2264
+ * uploads arriving together make one request rather than two.
2265
+ */
2266
+
2267
+ function createRemoteSettingsLoader(options) {
2268
+ var inFlight = null;
2269
+ return function () {
2270
+ if (inFlight === null) {
2271
+ inFlight = load(options).then(function (result) {
2272
+ if (result.status !== "success") {
2273
+ // Not cached: a missing `val login` is a thing a developer fixes
2274
+ // while the dev server keeps running, and caching the refusal would
2275
+ // mean they had to restart it to find out they had.
2276
+ inFlight = null;
2277
+ }
2278
+ return result;
2279
+ });
2280
+ }
2281
+ return inFlight;
2282
+ };
2283
+ }
2284
+ function load(_x) {
2285
+ return _load.apply(this, arguments);
2286
+ }
2287
+ /**
2288
+ * Which bucket the next remote file goes in.
2289
+ *
2290
+ * Round-robin from a per-process counter, which is what `val validate --fix`
2291
+ * does. It spreads a project's files rather than balancing anything, and the
2292
+ * bucket is baked into the ref, so where a given file lands does not matter as
2293
+ * long as it is remembered — which the ref does. A restarted server starts the
2294
+ * rotation again, and that is fine for the same reason.
2295
+ */
2296
+ function _load() {
2297
+ _load = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(options) {
2298
+ var auth, settings, buckets;
2299
+ return _regenerator().w(function (_context) {
2300
+ while (1) switch (_context.n) {
2301
+ case 0:
2302
+ if (options.project) {
2303
+ _context.n = 1;
2304
+ break;
2305
+ }
2306
+ return _context.a(2, err("unsupported", "This Val project is not connected to Val Build, so there is nowhere to upload a remote image to. Set `project` in val.config (or the VAL_PROJECT environment variable), or use a schema without `remote: true`."));
2307
+ case 1:
2308
+ _context.n = 2;
2309
+ return resolveRemoteFileAuth(options);
2310
+ case 2:
2311
+ auth = _context.v;
2312
+ if (!(auth.status === "error")) {
2313
+ _context.n = 3;
2314
+ break;
2315
+ }
2316
+ return _context.a(2, err("forbidden", auth.errorCode === "pat-error" ?
2317
+ // The CLI's own wording, near enough: this is the same missing file,
2318
+ // and someone who has seen it once should recognise it.
2319
+ "This project stores its images remotely, and uploading one needs you to be logged in. Run `npx val login` in the project directory, then try again." : "Could not work out which credential to upload remote files with: ".concat(auth.message)));
2320
+ case 3:
2321
+ _context.n = 4;
2322
+ return getSettings(options.project, auth.auth);
2323
+ case 4:
2324
+ settings = _context.v;
2325
+ if (settings.success) {
2326
+ _context.n = 5;
2327
+ break;
2328
+ }
2329
+ return _context.a(2, err("internal", "Could not read this project's remote file settings: ".concat(settings.message)));
2330
+ case 5:
2331
+ buckets = settings.data.remoteFileBuckets.map(function (b) {
2332
+ return b.bucket;
2333
+ });
2334
+ if (!(buckets.length === 0)) {
2335
+ _context.n = 6;
2336
+ break;
2337
+ }
2338
+ return _context.a(2, err("internal", "The project ".concat(options.project, " has no remote file buckets configured, so there is nowhere to put a remote image.")));
2339
+ case 6:
2340
+ return _context.a(2, {
2341
+ status: "success",
2342
+ publicProjectId: settings.data.publicProjectId,
2343
+ buckets: buckets
2344
+ });
2345
+ }
2346
+ }, _callee);
2347
+ }));
2348
+ return _load.apply(this, arguments);
2349
+ }
2350
+ var bucketCounter = 0;
2351
+ function nextBucket(buckets) {
2352
+ bucketCounter += 1;
2353
+ return buckets[bucketCounter % buckets.length];
2354
+ }
2355
+ function resolveRemoteUploadTarget(_x2) {
2356
+ return _resolveRemoteUploadTarget.apply(this, arguments);
2357
+ }
2358
+
2359
+ /**
2360
+ * The remote ref for these bytes, under this schema.
2361
+ *
2362
+ * The validation hash is the delicate part, and the reason `schema` is a
2363
+ * parameter rather than something derived here: it is baked into the ref, and
2364
+ * the validator recomputes it from the schema it finds at the path. Hand this
2365
+ * the wrong schema and the upload succeeds and then never validates — see
2366
+ * `docs/plans/mcp-remote-images.md` Part D, and `remoteImageSchemaFor`, which
2367
+ * is where a gallery's synthesized schema comes from.
2368
+ */
2369
+ function _resolveRemoteUploadTarget() {
2370
+ _resolveRemoteUploadTarget = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(loadSettings) {
2371
+ var settings, coreVersion;
2372
+ return _regenerator().w(function (_context2) {
2373
+ while (1) switch (_context2.n) {
2374
+ case 0:
2375
+ _context2.n = 1;
2376
+ return loadSettings();
2377
+ case 1:
2378
+ settings = _context2.v;
2379
+ if (!(settings.status !== "success")) {
2380
+ _context2.n = 2;
2381
+ break;
2382
+ }
2383
+ return _context2.a(2, settings);
2384
+ case 2:
2385
+ coreVersion = Internal.VERSION.core;
2386
+ if (coreVersion) {
2387
+ _context2.n = 3;
2388
+ break;
2389
+ }
2390
+ return _context2.a(2, err("internal", "Could not get @valbuild/core package version"));
2391
+ case 3:
2392
+ return _context2.a(2, {
2393
+ status: "success",
2394
+ target: {
2395
+ publicProjectId: settings.publicProjectId,
2396
+ bucket: nextBucket(settings.buckets),
2397
+ coreVersion: coreVersion,
2398
+ // Read the way `val validate --fix` reads it. Deliberately not from
2399
+ // `ValConfig`, which has no such field: the remote host is where Val's
2400
+ // own file service lives rather than something a project configures, and
2401
+ // the env var is the escape hatch for pointing at another one.
2402
+ remoteHost: process.env.VAL_REMOTE_HOST || DEFAULT_VAL_REMOTE_HOST
2403
+ }
2404
+ });
2405
+ }
2406
+ }, _callee2);
2407
+ }));
2408
+ return _resolveRemoteUploadTarget.apply(this, arguments);
2409
+ }
2410
+ function buildRemoteRef(input) {
2411
+ var target = input.target;
2412
+ var fileHash = Internal.remote.getFileHash(Buffer.from(input.bytes));
2413
+ return Internal.remote.createRemoteRef(target.remoteHost, {
2414
+ publicProjectId: target.publicProjectId,
2415
+ coreVersion: target.coreVersion,
2416
+ bucket: target.bucket,
2417
+ validationHash: Internal.remote.getValidationHash(target.coreVersion, input.schema, input.fileExt, input.metadata, fileHash, new TextEncoder()),
2418
+ fileHash: fileHash,
2419
+ filePath: input.filePath
2420
+ });
2421
+ }
2422
+
2423
+ /**
2424
+ * Uploading an image over MCP.
2425
+ *
2426
+ * Not part of the default tool set, and constructed separately for a reason
2427
+ * that is about dependencies rather than about design: reading the dimensions
2428
+ * out of a JPEG and converting it to WebP needs an image library, and `sharp`
2429
+ * is a native dependency that would otherwise be installed by every project
2430
+ * that installs Val. So the host builds this one and hands it in — see
2431
+ * `createValTools`'s `extraTools`, and `@valbuild/mcp/sharp`.
2432
+ *
2433
+ * What it deliberately does NOT do: remote files. `s.image({ remote: true })`
2434
+ * uploads straight to Val's content host through a presigned nonce, which is
2435
+ * the one path in local mode that needs a personal access token
2436
+ * (`docs/plans/mcp.md` D.1). Refused with a message that says so rather than
2437
+ * half-implemented, because a remote ref whose bytes were never uploaded fails
2438
+ * a long way from here.
2439
+ */
2440
+
2441
+ var ModuleFilePathSchema = z.string().describe('Path of the Val module, e.g. "/content/pages.val.ts".');
2442
+
2443
+ /**
2444
+ * How big an inline image may be, in bytes of decoded binary.
2445
+ *
2446
+ * Base64 arrives inside a JSON-RPC message that the host has already buffered,
2447
+ * so this is not the memory bound it looks like — the bound is on how much of
2448
+ * a model's context an image is worth spending, and on not writing a 200MB
2449
+ * file into `.val/patches` because an argument was malformed. A file on disk
2450
+ * is not subject to it: nothing had to carry those bytes to get here.
2451
+ */
2452
+ var MAX_INLINE_BYTES = 20 * 1024 * 1024;
2453
+ function createValImageTools(processor, options) {
2454
+ /**
2455
+ * One settings loader per config, made on first use.
2456
+ *
2457
+ * Not made here, because the config arrives with the call: the host builds
2458
+ * these tools and hands them to `createValTools`, which is what knows how the
2459
+ * project is configured. Keyed by identity rather than remade every call so
2460
+ * that a gallery of twenty images asks the content host once — and so that
2461
+ * the same tools handed to two registries do not share one answer.
2462
+ */
2463
+ var loader = null;
2464
+ var loaderConfig = null;
2465
+ var settingsFor = function settingsFor(config) {
2466
+ if (options !== null && options !== void 0 && options.loadRemoteSettings) {
2467
+ return options.loadRemoteSettings;
2468
+ }
2469
+ if (loader === null || loaderConfig !== config) {
2470
+ loader = createRemoteSettingsLoader(config);
2471
+ loaderConfig = config;
2472
+ }
2473
+ return loader;
2474
+ };
2475
+ return [defineTool({
2476
+ name: "upload_image",
2477
+ title: "Upload an image",
2478
+ description: "Upload an image file and put it in an image field, or add it to an image gallery module (one declared with s.images()). Give it either imageFilePath — a path to a file on the machine this app runs on — or imageBase64. The image is re-encoded first if the schema asks for that. Works for remotely stored images too (s.image({ remote: true })), which need the project to be connected to Val Build.",
2479
+ inputSchema: z.object({
2480
+ moduleFilePath: ModuleFilePathSchema,
2481
+ path: z.array(z.string())["default"]([]).describe('Path of the image field within the module, e.g. ["hero","image"]. Leave empty when the module itself is an image gallery — the image is then added to it under a generated key.'),
2482
+ imageFilePath: z.string().optional().describe("Absolute path to an image file on the machine running this app. Prefer this over imageBase64: it does not have to travel through the conversation."),
2483
+ imageBase64: z.string().optional().describe("The image itself, as a data URL or bare base64. Use only when there is no file to point at."),
2484
+ filename: z.string().optional().describe("Name to base the stored filename on. Defaults to the name of imageFilePath. A content hash is appended either way, and the extension is taken from the bytes rather than from this."),
2485
+ alt: z.string().optional().describe("Alt text describing the image, for screen readers.")
2486
+ }),
2487
+ annotations: {
2488
+ idempotentHint: false
2489
+ }
2490
+ }, /*#__PURE__*/function () {
2491
+ var _ref = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(args, deps) {
2492
+ var modulePath, moduleSchema, target, read, prepared, metadata, fileHash, dataUrl, generated, storedPath, resolvedRef, file, saved, _t, _t2;
2493
+ return _regenerator().w(function (_context) {
2494
+ while (1) switch (_context.n) {
2495
+ case 0:
2496
+ modulePath = args.moduleFilePath;
2497
+ moduleSchema = deps.state.serializedSchemas[modulePath];
2498
+ if (moduleSchema) {
2499
+ _context.n = 1;
2500
+ break;
2501
+ }
2502
+ return _context.a(2, err("not-found", "No Val module at ".concat(JSON.stringify(modulePath), ".")));
2503
+ case 1:
2504
+ target = resolveTarget(deps, moduleSchema, args.path);
2505
+ if (!(target.status === "error")) {
2506
+ _context.n = 2;
2507
+ break;
2508
+ }
2509
+ return _context.a(2, target.result);
2510
+ case 2:
2511
+ if (!(target.altRequired && args.alt === undefined)) {
2512
+ _context.n = 3;
2513
+ break;
2514
+ }
2515
+ return _context.a(2, err("invalid-args", "This gallery requires alt text for every image, so pass `alt` — a short description of what the image shows, for people using a screen reader."));
2516
+ case 3:
2517
+ _context.n = 4;
2518
+ return readInputImage(args);
2519
+ case 4:
2520
+ read = _context.v;
2521
+ if (!(read.status === "error")) {
2522
+ _context.n = 5;
2523
+ break;
2524
+ }
2525
+ return _context.a(2, read.result);
2526
+ case 5:
2527
+ _context.n = 6;
2528
+ return prepareImage(processor, read.bytes, read.filename, target.encode, target.accept);
2529
+ case 6:
2530
+ prepared = _context.v;
2531
+ if (!(prepared.status === "error")) {
2532
+ _context.n = 7;
2533
+ break;
2534
+ }
2535
+ return _context.a(2, prepared.result);
2536
+ case 7:
2537
+ metadata = _objectSpread2({
2538
+ width: prepared.width,
2539
+ height: prepared.height,
2540
+ mimeType: prepared.mimeType
2541
+ }, args.alt !== undefined ? {
2542
+ alt: args.alt
2543
+ } : {});
2544
+ fileHash = Internal.getSHA256Hash(prepared.bytes);
2545
+ dataUrl = "data:".concat(prepared.mimeType, ";base64,").concat(Buffer.from(prepared.bytes).toString("base64"));
2546
+ generated = Internal.createFilename(dataUrl, prepared.filename, metadata, fileHash);
2547
+ if (generated) {
2548
+ _context.n = 8;
2549
+ break;
2550
+ }
2551
+ return _context.a(2, err("internal", "Could not derive a filename for the uploaded image."));
2552
+ case 8:
2553
+ // Where the bytes live, always: a `/public/...` path in the patch
2554
+ // store. For a remote image the CONTENT points somewhere else, but the
2555
+ // bytes still land here and stay here until publish pushes them.
2556
+ storedPath = "".concat(target.directory, "/").concat(generated);
2557
+ _context.n = 9;
2558
+ return resolveRef(target, settingsFor(deps.config), {
2559
+ bytes: prepared.bytes,
2560
+ storedPath: storedPath,
2561
+ generated: generated,
2562
+ metadata: metadata
2563
+ });
2564
+ case 9:
2565
+ resolvedRef = _context.v;
2566
+ if (!(resolvedRef.status === "error")) {
2567
+ _context.n = 10;
2568
+ break;
2569
+ }
2570
+ return _context.a(2, resolvedRef);
2571
+ case 10:
2572
+ file = {
2573
+ ref: resolvedRef.ref,
2574
+ storedPath: storedPath,
2575
+ remote: target.remote !== null,
2576
+ dataUrl: dataUrl,
2577
+ metadata: metadata
2578
+ };
2579
+ if (!(target.kind === "gallery")) {
2580
+ _context.n = 12;
2581
+ break;
2582
+ }
2583
+ _context.n = 11;
2584
+ return uploadToGallery(deps, modulePath, target.gallerySchema, file);
2585
+ case 11:
2586
+ _t = _context.v;
2587
+ _context.n = 17;
2588
+ break;
2589
+ case 12:
2590
+ if (!target.galleryBacked) {
2591
+ _context.n = 14;
2592
+ break;
2593
+ }
2594
+ _context.n = 13;
2595
+ return uploadToGalleryBackedField(deps, {
2596
+ fieldModule: modulePath,
2597
+ fieldPath: args.path,
2598
+ galleryModule: target.referencedModule,
2599
+ file: file
2600
+ });
2601
+ case 13:
2602
+ _t2 = _context.v;
2603
+ _context.n = 16;
2604
+ break;
2605
+ case 14:
2606
+ _context.n = 15;
2607
+ return uploadToField(deps, modulePath, args.path, file);
2608
+ case 15:
2609
+ _t2 = _context.v;
2610
+ case 16:
2611
+ _t = _t2;
2612
+ case 17:
2613
+ saved = _t;
2614
+ if (!(saved.status === "error")) {
2615
+ _context.n = 18;
2616
+ break;
2617
+ }
2618
+ return _context.a(2, saved);
2619
+ case 18:
2620
+ return _context.a(2, {
2621
+ status: "ok",
2622
+ data: _objectSpread2(_objectSpread2({}, saved.data), {}, {
2623
+ filePath: file.ref,
2624
+ remote: file.remote,
2625
+ width: prepared.width,
2626
+ height: prepared.height,
2627
+ mimeType: prepared.mimeType,
2628
+ // Said out loud rather than left to be inferred from the mime
2629
+ // type: `encode` silently keeps the original when the conversion
2630
+ // would have made the file bigger, and "why is this still a PNG"
2631
+ // is otherwise unanswerable from the result.
2632
+ reEncoded: prepared.reEncoded
2633
+ })
2634
+ });
2635
+ }
2636
+ }, _callee);
2637
+ }));
2638
+ return function (_x, _x2) {
2639
+ return _ref.apply(this, arguments);
2640
+ };
2641
+ }())];
2642
+ }
2643
+
2644
+ /**
2645
+ * What the content will point at: a local path, or a remote ref.
2646
+ *
2647
+ * The remote branch is the only thing in this tool that talks to the network,
2648
+ * and all it asks for is which project and which bucket. The bytes do not go to
2649
+ * the content host here — they go to the patch store like any other pending
2650
+ * file, and publish pushes them. See `docs/plans/mcp-remote-images.md` Part A.
2651
+ */
2652
+ function resolveRef(_x3, _x4, _x5) {
2653
+ return _resolveRef.apply(this, arguments);
2654
+ }
2655
+ /**
2656
+ * Where the image is going, and what the schema there says about it.
2657
+ *
2658
+ * Two shapes, and they are genuinely different rather than two spellings of
2659
+ * one: a gallery is a record keyed by file path, so an upload ADDS an entry
2660
+ * under a key it generates, while a field is a single value, so an upload
2661
+ * REPLACES it. Resolving both here keeps that difference in one place instead
2662
+ * of scattered through the handler.
2663
+ */
2664
+ function _resolveRef() {
2665
+ _resolveRef = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(target, loadSettings, file) {
2666
+ var relative, resolved;
2667
+ return _regenerator().w(function (_context3) {
2668
+ while (1) switch (_context3.n) {
2669
+ case 0:
2670
+ if (!(target.remote === null)) {
2671
+ _context3.n = 1;
2672
+ break;
2673
+ }
2674
+ return _context3.a(2, {
2675
+ status: "ok",
2676
+ ref: file.storedPath
2677
+ });
2678
+ case 1:
2679
+ relative = file.storedPath.startsWith("/public/") ? file.storedPath.slice(1) : null;
2680
+ if (!(relative === null)) {
2681
+ _context3.n = 2;
2682
+ break;
2683
+ }
2684
+ return _context3.a(2, err("invalid-args", "A remote image has to be stored under /public, and this schema's directory is ".concat(JSON.stringify(target.directory), ".")));
2685
+ case 2:
2686
+ _context3.n = 3;
2687
+ return resolveRemoteUploadTarget(loadSettings);
2688
+ case 3:
2689
+ resolved = _context3.v;
2690
+ if (!(resolved.status !== "success")) {
2691
+ _context3.n = 4;
2692
+ break;
2693
+ }
2694
+ return _context3.a(2, resolved);
2695
+ case 4:
2696
+ return _context3.a(2, {
2697
+ status: "ok",
2698
+ ref: buildRemoteRef({
2699
+ target: resolved.target,
2700
+ bytes: file.bytes,
2701
+ filePath: relative,
2702
+ fileExt: getFileExt(file.generated),
2703
+ metadata: file.metadata,
2704
+ schema: target.remote.schema
2705
+ })
2706
+ });
2707
+ }
2708
+ }, _callee3);
2709
+ }));
2710
+ return _resolveRef.apply(this, arguments);
2711
+ }
2712
+ /**
2713
+ * That this image is stored remotely, and the schema its ref is hashed against.
2714
+ *
2715
+ * The schema is carried rather than looked up later because getting it wrong is
2716
+ * silent: the validation hash is baked into the ref, the validator recomputes it
2717
+ * from the schema it finds at the path, and a mismatch means a file that uploads
2718
+ * and then never validates. See `docs/plans/mcp-remote-images.md` Part D.
2719
+ */
2720
+ /** The default in `createFilePatch`, and the same default here. */
2721
+ var DEFAULT_DIRECTORY = "/public/val";
2722
+ function resolveTarget(deps, moduleSchema, fieldPath) {
2723
+ var _ref2, _schema$options$direc, _schema$options, _schema$options$accep, _schema$options2, _schema$options3;
2724
+ if (fieldPath.length === 0) {
2725
+ var _moduleSchema$directo;
2726
+ if (moduleSchema.type !== "record" || moduleSchema.mediaType !== "images") {
2727
+ return {
2728
+ status: "error",
2729
+ result: err("invalid-args", "The module is not an image gallery (it is a ".concat(moduleSchema.type, "), so an image cannot be added to it directly. Give a path to the image field within it."))
2730
+ };
2731
+ }
2732
+ return {
2733
+ status: "ok",
2734
+ kind: "gallery",
2735
+ gallerySchema: moduleSchema,
2736
+ directory: (_moduleSchema$directo = moduleSchema.directory) !== null && _moduleSchema$directo !== void 0 ? _moduleSchema$directo : DEFAULT_DIRECTORY,
2737
+ accept: moduleSchema.accept,
2738
+ encode: resolveEncodeSettings(undefined, moduleSchema.encode),
2739
+ altRequired: altIsRequired(moduleSchema),
2740
+ remote: moduleSchema.remote ? {
2741
+ schema: galleryEntryImageSchema(moduleSchema)
2742
+ } : null
2743
+ };
2744
+ }
2745
+ var resolved = resolveSerializedSchemaAtPath(moduleSchema, fieldPath);
2746
+ if (resolved.kind === "gallery-traversed") {
2747
+ return {
2748
+ status: "error",
2749
+ result: err("invalid-args", "That path addresses an entry inside an image gallery. Upload to the gallery module itself, with an empty path — the key is generated from the image.")
2750
+ };
2751
+ }
2752
+ if (resolved.kind !== "leaf") {
2753
+ return {
2754
+ status: "error",
2755
+ result: err("not-found", "No image field at ".concat(JSON.stringify(fieldPath.join(".")), " in this module."))
2756
+ };
2757
+ }
2758
+ var schema = resolved.schema;
2759
+ if (schema.type !== "image") {
2760
+ return {
2761
+ status: "error",
2762
+ result: err("invalid-args", "The value at ".concat(JSON.stringify(fieldPath.join(".")), " is a ").concat(schema.type, ", not an image."))
2763
+ };
2764
+ }
2765
+ // A gallery-backed field (`s.image(galleryVal)`) serializes with EMPTY
2766
+ // options, so `accept`, `directory` and `encode` all have to fall through to
2767
+ // the gallery or it would never honour what the gallery asked for. The
2768
+ // field's own option wins where it has one — this is `ImageField`'s
2769
+ // resolution order, and the two must not disagree.
2770
+ var referencedModule = schema.referencedModule;
2771
+ var gallerySchema = referencedModule ? deps.state.serializedSchemas[referencedModule] : undefined;
2772
+ var galleryRecord = (gallerySchema === null || gallerySchema === void 0 ? void 0 : gallerySchema.type) === "record" ? gallerySchema : undefined;
2773
+ // Where the bytes end up is what decides which schema the ref is hashed
2774
+ // against. A gallery-backed field's image is a GALLERY entry — the field only
2775
+ // points at it — so it is the gallery's synthesized schema either way, and a
2776
+ // plain field is hashed against its own. `s.image(gallery)` has no `remote`
2777
+ // option of its own, so a gallery-backed field is remote exactly when its
2778
+ // gallery is.
2779
+ var remote = galleryRecord ? galleryRecord.remote ? {
2780
+ schema: galleryEntryImageSchema(galleryRecord)
2781
+ } : null : schema.remote ? {
2782
+ schema: schema
2783
+ } : null;
2784
+ return {
2785
+ status: "ok",
2786
+ kind: "field",
2787
+ referencedModule: referencedModule,
2788
+ galleryBacked: referencedModule !== undefined,
2789
+ directory: (_ref2 = (_schema$options$direc = (_schema$options = schema.options) === null || _schema$options === void 0 ? void 0 : _schema$options.directory) !== null && _schema$options$direc !== void 0 ? _schema$options$direc : galleryRecord === null || galleryRecord === void 0 ? void 0 : galleryRecord.directory) !== null && _ref2 !== void 0 ? _ref2 : DEFAULT_DIRECTORY,
2790
+ accept: (_schema$options$accep = (_schema$options2 = schema.options) === null || _schema$options2 === void 0 ? void 0 : _schema$options2.accept) !== null && _schema$options$accep !== void 0 ? _schema$options$accep : galleryRecord === null || galleryRecord === void 0 ? void 0 : galleryRecord.accept,
2791
+ encode: resolveEncodeSettings((_schema$options3 = schema.options) === null || _schema$options3 === void 0 ? void 0 : _schema$options3.encode, galleryRecord === null || galleryRecord === void 0 ? void 0 : galleryRecord.encode),
2792
+ // Only a gallery has an alt schema to satisfy. A plain `s.image()` field
2793
+ // stores `alt` if it is given and is fine without it.
2794
+ altRequired: galleryRecord ? altIsRequired(galleryRecord) : false,
2795
+ remote: remote
2796
+ };
2797
+ }
2798
+
2799
+ /**
2800
+ * The image schema a gallery ENTRY is validated as.
2801
+ *
2802
+ * A gallery's item schema is an object (width/height/mimeType/alt), but the
2803
+ * remote ref's validation hash is computed over a `SerializedImageSchema` — so
2804
+ * one has to be synthesized, carrying the `accept` and `directory` of the record
2805
+ * that holds the entry. This is `handleRemoteGalleryFileUpload`'s synthesis, and
2806
+ * it has to stay identical to it: the CLI's `--fix` writes refs this way and the
2807
+ * check that validates them reads them the same way, so a third shape here would
2808
+ * bake a hash that can never match.
2809
+ */
2810
+ function galleryEntryImageSchema(gallery) {
2811
+ return {
2812
+ type: "image",
2813
+ opt: false,
2814
+ options: _objectSpread2(_objectSpread2({}, gallery.accept ? {
2815
+ accept: gallery.accept
2816
+ } : {}), gallery.directory ? {
2817
+ directory: gallery.directory
2818
+ } : {})
2819
+ };
2820
+ }
2821
+
2822
+ /**
2823
+ * Does this gallery insist on alt text?
2824
+ *
2825
+ * `s.images()` gives `alt` a nullable string by default, and an entry written
2826
+ * without one stores `null`. A gallery that passed its own — `s.images({ alt:
2827
+ * s.string().minLength(4) })`, which the example app does — has made it
2828
+ * required, and an upload with no `alt` cannot satisfy it however it is
2829
+ * written. Asked here so the caller is told what to pass, rather than shown a
2830
+ * validation error about a null it never chose.
2831
+ */
2832
+ function altIsRequired(gallerySchema) {
2833
+ if (gallerySchema.type !== "record") {
2834
+ return false;
2835
+ }
2836
+ return gallerySchema.alt !== undefined && !gallerySchema.alt.opt;
2837
+ }
2838
+
2839
+ /** The bytes to upload, from whichever of the two arguments was given. */
2840
+ function readInputImage(_x6) {
2841
+ return _readInputImage.apply(this, arguments);
2842
+ }
2843
+ function _readInputImage() {
2844
+ _readInputImage = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(args) {
2845
+ var _args$imageBase, _args$filename2;
2846
+ var hasPath, hasBase64, _args$filename, _bytes, base64, comma, payload, bytes, _t3;
2847
+ return _regenerator().w(function (_context4) {
2848
+ while (1) switch (_context4.p = _context4.n) {
2849
+ case 0:
2850
+ hasPath = args.imageFilePath !== undefined;
2851
+ hasBase64 = args.imageBase64 !== undefined;
2852
+ if (!(hasPath === hasBase64)) {
2853
+ _context4.n = 1;
2854
+ break;
2855
+ }
2856
+ return _context4.a(2, {
2857
+ status: "error",
2858
+ result: err("invalid-args", hasPath ? "Give either imageFilePath or imageBase64, not both." : "Give either imageFilePath — a path to a file on the machine this app runs on — or imageBase64.")
2859
+ });
2860
+ case 1:
2861
+ if (!(args.imageFilePath !== undefined)) {
2862
+ _context4.n = 5;
2863
+ break;
2864
+ }
2865
+ _context4.p = 2;
2866
+ _context4.n = 3;
2867
+ return fs.readFile(args.imageFilePath);
2868
+ case 3:
2869
+ _bytes = _context4.v;
2870
+ return _context4.a(2, {
2871
+ status: "ok",
2872
+ bytes: new Uint8Array(_bytes),
2873
+ filename: (_args$filename = args.filename) !== null && _args$filename !== void 0 ? _args$filename : path.basename(args.imageFilePath)
2874
+ });
2875
+ case 4:
2876
+ _context4.p = 4;
2877
+ _t3 = _context4.v;
2878
+ return _context4.a(2, {
2879
+ status: "error",
2880
+ result: err("not-found", "Could not read ".concat(JSON.stringify(args.imageFilePath), ": ").concat(_t3 instanceof Error ? _t3.message : String(_t3)))
2881
+ });
2882
+ case 5:
2883
+ base64 = (_args$imageBase = args.imageBase64) !== null && _args$imageBase !== void 0 ? _args$imageBase : ""; // A data URL and a bare base64 string are both accepted, because a model
2884
+ // asked for "base64" produces either and neither is wrong.
2885
+ comma = base64.startsWith("data:") ? base64.indexOf(",") : -1;
2886
+ payload = comma === -1 ? base64 : base64.slice(comma + 1);
2887
+ _context4.p = 6;
2888
+ bytes = Buffer.from(payload, "base64");
2889
+ _context4.n = 8;
2890
+ break;
2891
+ case 7:
2892
+ _context4.p = 7;
2893
+ _context4.v;
2894
+ return _context4.a(2, {
2895
+ status: "error",
2896
+ result: err("invalid-args", "imageBase64 is not valid base64.")
2897
+ });
2898
+ case 8:
2899
+ if (!(bytes.length === 0)) {
2900
+ _context4.n = 9;
2901
+ break;
2902
+ }
2903
+ return _context4.a(2, {
2904
+ status: "error",
2905
+ result: err("invalid-args", "imageBase64 decoded to no bytes at all.")
2906
+ });
2907
+ case 9:
2908
+ if (!(bytes.length > MAX_INLINE_BYTES)) {
2909
+ _context4.n = 10;
2910
+ break;
2911
+ }
2912
+ return _context4.a(2, {
2913
+ status: "error",
2914
+ result: err("invalid-args", "That image is ".concat(Math.round(bytes.length / (1024 * 1024)), "MB, over the ").concat(MAX_INLINE_BYTES / (1024 * 1024), "MB limit for an inline image. Write it to a file and pass imageFilePath instead."))
2915
+ });
2916
+ case 10:
2917
+ return _context4.a(2, {
2918
+ status: "ok",
2919
+ bytes: new Uint8Array(bytes),
2920
+ filename: (_args$filename2 = args.filename) !== null && _args$filename2 !== void 0 ? _args$filename2 : null
2921
+ });
2922
+ }
2923
+ }, _callee4, null, [[6, 7], [2, 4]]);
2924
+ }));
2925
+ return _readInputImage.apply(this, arguments);
2926
+ }
2927
+ /**
2928
+ * Read the image, and re-encode it when the schema asked for that.
2929
+ *
2930
+ * The conversion happens BEFORE the caller hashes anything, which is the whole
2931
+ * reason it is here rather than after the patch is built: the SHA-256 becomes
2932
+ * the filename suffix, the mime type picks the extension, and the width and
2933
+ * height are stored — so converting afterwards would make every one of those
2934
+ * describe a file that was never uploaded. Same rule as the browser's
2935
+ * `readImageFromFile`, and the decisions are literally the same functions.
2936
+ */
2937
+ function prepareImage(_x7, _x8, _x9, _x0, _x1) {
2938
+ return _prepareImage.apply(this, arguments);
2939
+ }
2940
+ /**
2941
+ * Refuse an image the schema says it does not store — checked on the FINAL
2942
+ * bytes, never on the ones that arrived.
2943
+ *
2944
+ * The order is the whole point, and getting it backwards breaks the one
2945
+ * combination `accept` and `encode` exist to serve together:
2946
+ * `s.image({ accept: "image/webp", encode: { type: "webp" } })` means "I store
2947
+ * webp, and I will convert what you give me". Checking the SOURCE against
2948
+ * `accept` refuses the PNG that the conversion was there to turn into a webp —
2949
+ * so the check belongs after the conversion, on what is actually stored.
2950
+ *
2951
+ * That the check happens at all is the one thing here the Studio does not do,
2952
+ * and it is not a difference of opinion about `accept`. `ImageSchema` reports a
2953
+ * mismatch with `fixes: ["image:check-metadata"]`, which
2954
+ * `partitionValidationErrors` treats as server-repairable and therefore
2955
+ * non-blocking — so nothing downstream would refuse this. The Studio does not
2956
+ * need it to: its file picker carries `accept`, so a person cannot choose one.
2957
+ * An agent has no picker, and this is it.
2958
+ */
2959
+ function _prepareImage() {
2960
+ _prepareImage = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5(processor, bytes, filename, settings, accept) {
2961
+ var original, asIs, targetMimeType, resizeTo, encoded, converted, _t5;
2962
+ return _regenerator().w(function (_context5) {
2963
+ while (1) switch (_context5.p = _context5.n) {
2964
+ case 0:
2965
+ _context5.p = 0;
2966
+ _context5.n = 1;
2967
+ return processor.read(bytes);
2968
+ case 1:
2969
+ original = _context5.v;
2970
+ _context5.n = 3;
2971
+ break;
2972
+ case 2:
2973
+ _context5.p = 2;
2974
+ _t5 = _context5.v;
2975
+ return _context5.a(2, {
2976
+ status: "error",
2977
+ result: err("internal", "Could not read the image: ".concat(_t5 instanceof Error ? _t5.message : String(_t5)))
2978
+ });
2979
+ case 3:
2980
+ if (original) {
2981
+ _context5.n = 4;
2982
+ break;
2983
+ }
2984
+ return _context5.a(2, {
2985
+ status: "error",
2986
+ result: err("invalid-args", "Those bytes are not an image, or not one this app's image library can read.")
2987
+ });
2988
+ case 4:
2989
+ asIs = {
2990
+ status: "ok",
2991
+ bytes: bytes,
2992
+ width: original.width,
2993
+ height: original.height,
2994
+ mimeType: original.mimeType,
2995
+ filename: filename,
2996
+ reEncoded: false
2997
+ };
2998
+ if (!(settings === null)) {
2999
+ _context5.n = 5;
3000
+ break;
3001
+ }
3002
+ return _context5.a(2, refuseUnaccepted(asIs, accept));
3003
+ case 5:
3004
+ targetMimeType = ENCODE_MIME_TYPE_OF[settings.type];
3005
+ if (isMimeTypeAccepted(targetMimeType, accept)) {
3006
+ _context5.n = 6;
3007
+ break;
3008
+ }
3009
+ return _context5.a(2, refuseUnaccepted(asIs, accept));
3010
+ case 6:
3011
+ resizeTo = fitWithin(original.width, original.height, settings.maxWidth, settings.maxHeight);
3012
+ if (!isSkippedSource(original.mimeType, targetMimeType, resizeTo !== null)) {
3013
+ _context5.n = 7;
3014
+ break;
3015
+ }
3016
+ return _context5.a(2, refuseUnaccepted(asIs, accept));
3017
+ case 7:
3018
+ _context5.p = 7;
3019
+ _context5.n = 8;
3020
+ return processor.encode(bytes, {
3021
+ mimeType: targetMimeType,
3022
+ quality: settings.quality,
3023
+ resizeTo: resizeTo
3024
+ });
3025
+ case 8:
3026
+ encoded = _context5.v;
3027
+ _context5.n = 10;
3028
+ break;
3029
+ case 9:
3030
+ _context5.p = 9;
3031
+ _context5.v;
3032
+ return _context5.a(2, refuseUnaccepted(asIs, accept));
3033
+ case 10:
3034
+ if (encoded) {
3035
+ _context5.n = 11;
3036
+ break;
3037
+ }
3038
+ return _context5.a(2, refuseUnaccepted(asIs, accept));
3039
+ case 11:
3040
+ _context5.p = 11;
3041
+ _context5.n = 12;
3042
+ return processor.read(encoded.bytes);
3043
+ case 12:
3044
+ converted = _context5.v;
3045
+ _context5.n = 14;
3046
+ break;
3047
+ case 13:
3048
+ _context5.p = 13;
3049
+ _context5.v;
3050
+ return _context5.a(2, refuseUnaccepted(asIs, accept));
3051
+ case 14:
3052
+ if (!(!converted || !chooseEncoded({
3053
+ originalSize: bytes.length,
3054
+ encodedSize: encoded.bytes.length,
3055
+ encodedType: converted.mimeType,
3056
+ targetMimeType: targetMimeType,
3057
+ needsDownscale: resizeTo !== null
3058
+ }))) {
3059
+ _context5.n = 15;
3060
+ break;
3061
+ }
3062
+ return _context5.a(2, refuseUnaccepted(asIs, accept));
3063
+ case 15:
3064
+ return _context5.a(2, refuseUnaccepted({
3065
+ status: "ok",
3066
+ bytes: encoded.bytes,
3067
+ width: converted.width,
3068
+ height: converted.height,
3069
+ mimeType: converted.mimeType,
3070
+ filename: filename === null ? null : withExtension(filename, settings.type),
3071
+ reEncoded: true
3072
+ }, accept));
3073
+ }
3074
+ }, _callee5, null, [[11, 13], [7, 9], [0, 2]]);
3075
+ }));
3076
+ return _prepareImage.apply(this, arguments);
3077
+ }
3078
+ function refuseUnaccepted(prepared, accept) {
3079
+ if (isMimeTypeAccepted(prepared.mimeType, accept)) {
3080
+ return prepared;
3081
+ }
3082
+ return {
3083
+ status: "error",
3084
+ result: err("validation-failed", "This field only accepts ".concat(accept, ", and the image ").concat(prepared.reEncoded ? "converts to" : "is", " ").concat(prepared.mimeType, ". Convert it yourself first, or add `encode` to the schema so Val converts uploads for you."))
3085
+ };
3086
+ }
3087
+
3088
+ /**
3089
+ * An uploaded image, and where it is going to live.
3090
+ *
3091
+ * Two paths, because a remote image has two. `ref` is what the content refers
3092
+ * to — a `/public/...` path locally, a `remote.val.build` URL remotely — and it
3093
+ * is what the patch stores. `storedPath` is where the BYTES go, which is the
3094
+ * patch store in both cases and always a `/public/...` path: a remote ref is a
3095
+ * URL that encodes one, and the store keys files by the path, not the URL.
3096
+ *
3097
+ * They are the same string for a local image, and separating them anyway is
3098
+ * what stops the remote branch being a set of ad-hoc splits at each use.
3099
+ */
3100
+ /** Add the image to a gallery module: it is a record keyed by the file path. */
3101
+ function uploadToGallery(_x10, _x11, _x12, _x13) {
3102
+ return _uploadToGallery.apply(this, arguments);
3103
+ }
3104
+ /** Put the image in a field of its own: `replace` the value, `file` carries the bytes. */
3105
+ function _uploadToGallery() {
3106
+ _uploadToGallery = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee6(deps, galleryModule, gallerySchema, file) {
3107
+ var built, _splitFileOps, patchOps, uploadFiles;
3108
+ return _regenerator().w(function (_context6) {
3109
+ while (1) switch (_context6.n) {
3110
+ case 0:
3111
+ built = buildImageGalleryPatch({
3112
+ filePath: file.ref,
3113
+ imageKey: file.dataUrl,
3114
+ metadata: file.metadata
3115
+ }, gallerySchema);
3116
+ if (!(built.kind !== "ok")) {
3117
+ _context6.n = 1;
3118
+ break;
3119
+ }
3120
+ return _context6.a(2, fromBuildResult(built));
3121
+ case 1:
3122
+ _splitFileOps = splitFileOps(built.patch, deps, file), patchOps = _splitFileOps.patchOps, uploadFiles = _splitFileOps.uploadFiles;
3123
+ return _context6.a(2, savePatch(deps, galleryModule, patchOps, {
3124
+ uploadFiles: uploadFiles
3125
+ }));
3126
+ }
3127
+ }, _callee6);
3128
+ }));
3129
+ return _uploadToGallery.apply(this, arguments);
3130
+ }
3131
+ function uploadToField(_x14, _x15, _x16, _x17) {
3132
+ return _uploadToField.apply(this, arguments);
3133
+ }
3134
+ /**
3135
+ * `s.image(galleryVal)`: two modules, and the gallery goes first.
3136
+ *
3137
+ * The field carries only the path — the dimensions and the mime type live in
3138
+ * the gallery, and a second copy on the field is how two copies of one fact get
3139
+ * to disagree. So the bytes and the metadata are written to the gallery, and
3140
+ * then the field is pointed at them.
3141
+ *
3142
+ * The order is not a preference. `s.image(gallery)` validates that the gallery
3143
+ * HAS an entry for the path the field names, so a field written first refers to
3144
+ * something that does not exist yet and the write is refused — correctly, by
3145
+ * the same check that would catch a typo'd path.
3146
+ */
3147
+ function _uploadToField() {
3148
+ _uploadToField = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7(deps, fieldModule, fieldPath, file) {
3149
+ var built, _splitFileOps2, patchOps, uploadFiles;
3150
+ return _regenerator().w(function (_context7) {
3151
+ while (1) switch (_context7.n) {
3152
+ case 0:
3153
+ built = safeParsePatch([{
3154
+ op: "replace",
3155
+ path: fieldPath,
3156
+ value: _objectSpread2({
3157
+ path: file.ref
3158
+ }, file.metadata)
3159
+ }, {
3160
+ op: "file",
3161
+ path: fieldPath,
3162
+ filePath: file.ref,
3163
+ value: file.dataUrl,
3164
+ metadata: file.metadata,
3165
+ remote: false
3166
+ }]);
3167
+ if (!(built.kind !== "ok")) {
3168
+ _context7.n = 1;
3169
+ break;
3170
+ }
3171
+ return _context7.a(2, fromBuildResult(built));
3172
+ case 1:
3173
+ _splitFileOps2 = splitFileOps(built.patch, deps, file), patchOps = _splitFileOps2.patchOps, uploadFiles = _splitFileOps2.uploadFiles;
3174
+ return _context7.a(2, savePatch(deps, fieldModule, patchOps, {
3175
+ uploadFiles: uploadFiles
3176
+ }));
3177
+ }
3178
+ }, _callee7);
3179
+ }));
3180
+ return _uploadToField.apply(this, arguments);
3181
+ }
3182
+ function uploadToGalleryBackedField(_x18, _x19) {
3183
+ return _uploadToGalleryBackedField.apply(this, arguments);
3184
+ }
3185
+ /**
3186
+ * Take the bytes out of the patch, and hand back the upload that puts them
3187
+ * back.
3188
+ *
3189
+ * A patch that reaches the store must never carry binary data: the server
3190
+ * never reads a `file` op's value as data, so base64 left in one produces NO
3191
+ * file and fails silently — the patch applies, the source points at a path,
3192
+ * and nothing is there. The hash that replaces it is the only thing in the
3193
+ * patch that says WHICH bytes the op meant. This is `splitPatchFileOps` from
3194
+ * the Studio, in the one place server-side that needs it.
3195
+ */
3196
+ function _uploadToGalleryBackedField() {
3197
+ _uploadToGalleryBackedField = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8(deps, target) {
3198
+ var gallerySchema, savedEntry, reloaded, fresh, built, savedField;
3199
+ return _regenerator().w(function (_context8) {
3200
+ while (1) switch (_context8.n) {
3201
+ case 0:
3202
+ gallerySchema = deps.state.serializedSchemas[target.galleryModule];
3203
+ if (gallerySchema) {
3204
+ _context8.n = 1;
3205
+ break;
3206
+ }
3207
+ return _context8.a(2, err("not-found", "This field takes its images from ".concat(target.galleryModule, ", and there is no Val module there.")));
3208
+ case 1:
3209
+ _context8.n = 2;
3210
+ return uploadToGallery(deps, target.galleryModule, gallerySchema, target.file);
3211
+ case 2:
3212
+ savedEntry = _context8.v;
3213
+ if (!(savedEntry.status === "error")) {
3214
+ _context8.n = 3;
3215
+ break;
3216
+ }
3217
+ return _context8.a(2, savedEntry);
3218
+ case 3:
3219
+ _context8.n = 4;
3220
+ return loadState(deps.ops);
3221
+ case 4:
3222
+ reloaded = _context8.v;
3223
+ if (!(reloaded.status === "error")) {
3224
+ _context8.n = 5;
3225
+ break;
3226
+ }
3227
+ return _context8.a(2, reloaded.result);
3228
+ case 5:
3229
+ fresh = _objectSpread2(_objectSpread2({}, deps), {}, {
3230
+ state: reloaded.state
3231
+ });
3232
+ built = safeParsePatch([{
3233
+ op: "replace",
3234
+ path: target.fieldPath,
3235
+ // No `file` op and no metadata: the bytes are already in the gallery
3236
+ // patch above, and the gallery is where everything about them lives.
3237
+ value: {
3238
+ path: target.file.ref
3239
+ }
3240
+ }]);
3241
+ if (!(built.kind !== "ok")) {
3242
+ _context8.n = 6;
3243
+ break;
3244
+ }
3245
+ return _context8.a(2, fromBuildResult(built));
3246
+ case 6:
3247
+ _context8.n = 7;
3248
+ return savePatch(fresh, target.fieldModule, built.patch, {
3249
+ onInvalid: "report"
3250
+ });
3251
+ case 7:
3252
+ savedField = _context8.v;
3253
+ if (!(savedField.status === "error")) {
3254
+ _context8.n = 8;
3255
+ break;
3256
+ }
3257
+ return _context8.a(2, {
3258
+ status: "error",
3259
+ code: savedField.code,
3260
+ // The image IS in the gallery, so reporting only that the second write
3261
+ // failed would leave the caller thinking nothing happened.
3262
+ message: "The image was uploaded and added to the gallery ".concat(target.galleryModule, ", but ").concat(target.fieldModule, " could not be pointed at it: ").concat(savedField.message)
3263
+ });
3264
+ case 8:
3265
+ return _context8.a(2, savedField);
3266
+ }
3267
+ }, _callee8);
3268
+ }));
3269
+ return _uploadToGalleryBackedField.apply(this, arguments);
3270
+ }
3271
+ function splitFileOps(patch, deps, file) {
3272
+ var ref = file.ref,
3273
+ storedPath = file.storedPath,
3274
+ remote = file.remote,
3275
+ dataUrl = file.dataUrl,
3276
+ metadata = file.metadata;
3277
+ var textEncoder = new TextEncoder();
3278
+ var patchOps = patch.map(function (op) {
3279
+ return op.op === "file" && typeof op.value === "string" ? _objectSpread2(_objectSpread2({}, op), {}, {
3280
+ value: Internal.getSHA256Hash(textEncoder.encode(op.value))
3281
+ }) : op;
3282
+ });
3283
+ var uploadFiles = /*#__PURE__*/function () {
3284
+ var _ref4 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(_ref3) {
3285
+ var patchId, parentRef, saved;
3286
+ return _regenerator().w(function (_context2) {
3287
+ while (1) switch (_context2.n) {
3288
+ case 0:
3289
+ patchId = _ref3.patchId, parentRef = _ref3.parentRef;
3290
+ _context2.n = 1;
3291
+ return deps.ops.saveBase64EncodedBinaryFileFromPatch(storedPath, parentRef, patchId, dataUrl, "image", metadata);
3292
+ case 1:
3293
+ saved = _context2.v;
3294
+ if (!saved.error) {
3295
+ _context2.n = 2;
3296
+ break;
3297
+ }
3298
+ return _context2.a(2, {
3299
+ status: "error",
3300
+ result: err("internal", "Could not store the uploaded image: ".concat(saved.error.message))
3301
+ });
3302
+ case 2:
3303
+ return _context2.a(2, {
3304
+ status: "ok",
3305
+ // Keyed by the REF, which is what `analyzePatches` will key the real
3306
+ // patch by once it exists — the file op carries the ref, not the path.
3307
+ files: _defineProperty({}, ref, {
3308
+ patchId: patchId,
3309
+ remote: remote,
3310
+ isDelete: false
3311
+ })
3312
+ });
3313
+ }
3314
+ }, _callee2);
3315
+ }));
3316
+ return function uploadFiles(_x20) {
3317
+ return _ref4.apply(this, arguments);
3318
+ };
3319
+ }();
3320
+ return {
3321
+ patchOps: patchOps,
3322
+ uploadFiles: uploadFiles
3323
+ };
3324
+ }
3325
+ function fromBuildResult(built) {
3326
+ if (built.kind === "wrong-tool") {
3327
+ return err("invalid-args", "".concat(built.reason, " Use the ").concat(built.suggestedTool, " tool instead."));
3328
+ }
3329
+ return err("invalid-args", built.message);
3330
+ }
3331
+
3332
+ var VERSION = function () {
3333
+ try {
3334
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
3335
+ return require("../package.json").version;
3336
+ } catch (_unused) {
3337
+ return null;
3338
+ }
3339
+ }();
3340
+
3341
+ /**
3342
+ * Verifying an OAuth access token, which is the whole of what makes this app a
3343
+ * resource server rather than a relay.
3344
+ *
3345
+ * The token is issued by Val's authorization server and presented by an MCP
3346
+ * client. This app holds no signing key for it and cannot mint one — it fetches
3347
+ * the issuer's *public* keys and checks a signature. That asymmetry is the
3348
+ * point: a verified `sub` is a fact about the token rather than a claim by
3349
+ * whoever sent it, which is what lets the tools attribute a patch to that
3350
+ * profile at all.
3351
+ *
3352
+ * ## Why this is not `jose`
3353
+ *
3354
+ * `jose` was the first choice and was rejected on a fact rather than a
3355
+ * preference: version 6 is ESM-only (`"type": "module"`, no CJS export). This
3356
+ * package is built by preconstruct and `require`d by Next.js server code, so an
3357
+ * ESM-only dependency here is a runtime failure in consumers' apps, not a build
3358
+ * inconvenience. Adding it would also put a dependency in every install of
3359
+ * `@valbuild/next` for one function.
3360
+ *
3361
+ * The actual cryptography is still not hand-rolled — `node:crypto` does the
3362
+ * ECDSA and the JWK import. What is written here is the JWS envelope and the
3363
+ * claim checks, and the rules that keep that safe are worth stating because
3364
+ * this repository has already shipped the counterexample (`decodeJwt`: `exp`
3365
+ * never checked, a non-constant-time compare, verification skippable):
3366
+ *
3367
+ * - **`alg` is pinned**, not read from the token. The header is only consulted
3368
+ * for `kid`. A verifier that honours the token's own `alg` can be handed
3369
+ * `HS256` and will treat the *published* public key as a shared secret.
3370
+ * - **Nothing is read from the payload before the signature verifies.** Claims
3371
+ * from an unverified token are attacker input.
3372
+ * - **Keys come only from the configured issuer's JWKS**, never from the token.
3373
+ * The key set is cached, but a token naming a `kid` the cache does not hold
3374
+ * provokes one rate-limited refetch rather than a refusal — see
3375
+ * {@link UNKNOWN_KID_REFETCH_INTERVAL_MS}. Without that, this server's own
3376
+ * cache turns any key rotation into an outage lasting the rest of the TTL.
3377
+ * - **ECDSA JWS signatures are raw `r||s`** (RFC 7518), not DER, which is what
3378
+ * `dsaEncoding: "ieee-p1363"` below is for. Omit it and every valid signature
3379
+ * is rejected — or worse, a future change makes it accept the wrong thing.
3380
+ */
3381
+
3382
+ var DEFAULT_CLOCK_TOLERANCE_SECONDS = 60;
3383
+ /** How long a fetched key set is reused before it is fetched again. */
3384
+ var JWKS_TTL_MS = 5 * 60 * 1000;
3385
+ /**
3386
+ * How long to wait before re-fetching after a failure.
3387
+ *
3388
+ * Shorter than the success TTL so a key rotation recovers quickly, but not zero:
3389
+ * an unreachable issuer must not turn every tool call into another request to
3390
+ * it.
3391
+ */
3392
+ var JWKS_ERROR_TTL_MS = 30 * 1000;
3393
+ /**
3394
+ * The floor between two refetches provoked by an unknown `kid`.
3395
+ *
3396
+ * A token naming a key this process has not seen is the one case where the TTL
3397
+ * is the wrong answer: the issuer may have just rotated, and refusing for the
3398
+ * rest of the five minutes turns a rotation into an outage. So an unknown `kid`
3399
+ * bypasses the TTL — but only this often, because the `kid` comes from the
3400
+ * token and the token comes from whoever is calling. Without a floor, a stream
3401
+ * of random `kid`s would be a way to make this server hammer its own issuer.
3402
+ *
3403
+ * Note what the floor costs when it bites: a refusal, for a token that would
3404
+ * have verified, for at most this long. That is the same failure the TTL used
3405
+ * to guarantee for five minutes, so the trade only ever improves.
3406
+ */
3407
+ var UNKNOWN_KID_REFETCH_INTERVAL_MS = 30 * 1000;
3408
+ /**
3409
+ * One cache per issuer, and it has to outlive the request or it is not a cache:
3410
+ * a fetch per tool call would put a network round trip in front of every read.
3411
+ */
3412
+ var jwksCache = new Map();
3413
+ /** Concurrent misses share one fetch rather than starting several. */
3414
+ var inFlight = new Map();
3415
+ /**
3416
+ * When an unknown `kid` last made us go back to the issuer, per issuer.
3417
+ *
3418
+ * Separate from the cache because it is a rate limit rather than a cache: it
3419
+ * records an *attempt*, so a refetch that found nothing new still spends the
3420
+ * window. Keyed by issuer, not by `kid`, or an attacker would simply use a new
3421
+ * one each time.
3422
+ */
3423
+ var unknownKidRefetchAtMs = new Map();
3424
+ function jwksUrl(issuer) {
3425
+ // Not discovered from the issuer's metadata document, deliberately:
3426
+ // discovery would mean one more request on the hot path and one more thing
3427
+ // that can be pointed elsewhere. The location is fixed by convention and by
3428
+ // Val's own authorization server.
3429
+ return new URL("/.well-known/jwks.json", issuer).toString();
3430
+ }
3431
+ function loadJwks(_x, _x2) {
3432
+ return _loadJwks.apply(this, arguments);
3433
+ }
3434
+ /**
3435
+ * Go back to the issuer because the token named a key we do not have.
3436
+ *
3437
+ * Rate-limited, and the limit is the whole reason this is not just a call to
3438
+ * {@link fetchJwks}: the `kid` that triggers it is attacker-controlled. Returns
3439
+ * `null` when the window has not elapsed, which the caller reads as "nothing
3440
+ * new to try" rather than as a failure.
3441
+ *
3442
+ * The limit is on *starting* a fetch, not on benefiting from one. A fetch
3443
+ * already in flight is joined whatever the window says, because joining it
3444
+ * costs the issuer nothing — and the case that matters is precisely a burst:
3445
+ * at a rotation, many requests arrive at once carrying the same new `kid`, and
3446
+ * refusing all but the first would be the outage this whole path exists to
3447
+ * prevent, merely shortened from five minutes to thirty seconds.
3448
+ *
3449
+ * Only ever called with a successfully-fetched key set in hand. An error entry
3450
+ * has its own, shorter TTL and its own recovery, and letting an unknown `kid`
3451
+ * shortcut it would hand an unreachable issuer a retry storm.
3452
+ */
3453
+ function _loadJwks() {
3454
+ _loadJwks = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(config, nowMs) {
3455
+ var url, cached, age, ttl, _t;
3456
+ return _regenerator().w(function (_context) {
3457
+ while (1) switch (_context.n) {
3458
+ case 0:
3459
+ url = jwksUrl(config.issuer);
3460
+ cached = jwksCache.get(url);
3461
+ if (!cached) {
3462
+ _context.n = 1;
3463
+ break;
3464
+ }
3465
+ age = cached.status === "keys" ? nowMs - cached.fetchedAtMs : nowMs - cached.failedAtMs;
3466
+ ttl = cached.status === "keys" ? JWKS_TTL_MS : JWKS_ERROR_TTL_MS;
3467
+ if (!(age < ttl)) {
3468
+ _context.n = 1;
3469
+ break;
3470
+ }
3471
+ return _context.a(2, {
3472
+ entry: cached,
3473
+ fromCache: true
3474
+ });
3475
+ case 1:
3476
+ _context.n = 2;
3477
+ return fetchJwks(config, nowMs);
3478
+ case 2:
3479
+ _t = _context.v;
3480
+ return _context.a(2, {
3481
+ entry: _t,
3482
+ fromCache: false
3483
+ });
3484
+ }
3485
+ }, _callee);
3486
+ }));
3487
+ return _loadJwks.apply(this, arguments);
3488
+ }
3489
+ function refetchForUnknownKid(_x3, _x4) {
3490
+ return _refetchForUnknownKid.apply(this, arguments);
3491
+ }
3492
+ /**
3493
+ * Fetch the key set, ignoring whatever is cached, and cache the result.
3494
+ *
3495
+ * Shares {@link inFlight} with every other caller, so a forced refetch that
3496
+ * lands during an ordinary miss joins it rather than opening a second request.
3497
+ * Only the caller that *starts* a fetch writes the cache — a joiner returns the
3498
+ * shared promise above — so `keepCacheOnError` is a property of the fetch, not a
3499
+ * race between callers.
3500
+ */
3501
+ function _refetchForUnknownKid() {
3502
+ _refetchForUnknownKid = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(config, nowMs) {
3503
+ var url, existing, lastAttemptMs;
3504
+ return _regenerator().w(function (_context2) {
3505
+ while (1) switch (_context2.n) {
3506
+ case 0:
3507
+ url = jwksUrl(config.issuer);
3508
+ existing = inFlight.get(url);
3509
+ if (!existing) {
3510
+ _context2.n = 1;
3511
+ break;
3512
+ }
3513
+ return _context2.a(2, existing);
3514
+ case 1:
3515
+ lastAttemptMs = unknownKidRefetchAtMs.get(url);
3516
+ if (!(lastAttemptMs !== undefined && nowMs - lastAttemptMs < UNKNOWN_KID_REFETCH_INTERVAL_MS)) {
3517
+ _context2.n = 2;
3518
+ break;
3519
+ }
3520
+ return _context2.a(2, null);
3521
+ case 2:
3522
+ // Recorded before the await, so that once this fetch has finished the window
3523
+ // is already closed against the next unknown `kid`.
3524
+ unknownKidRefetchAtMs.set(url, nowMs);
3525
+ return _context2.a(2, fetchJwks(config, nowMs, {
3526
+ keepCacheOnError: true
3527
+ }));
3528
+ }
3529
+ }, _callee2);
3530
+ }));
3531
+ return _refetchForUnknownKid.apply(this, arguments);
3532
+ }
3533
+ function fetchJwks(_x5, _x6) {
3534
+ return _fetchJwks.apply(this, arguments);
3535
+ }
3536
+ function _fetchJwks() {
3537
+ _fetchJwks = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4(config, nowMs) {
3538
+ var _config$fetchImpl;
3539
+ var _ref,
3540
+ _ref$keepCacheOnError,
3541
+ keepCacheOnError,
3542
+ url,
3543
+ existing,
3544
+ fetchImpl,
3545
+ pending,
3546
+ entry,
3547
+ cached,
3548
+ _args4 = arguments;
3549
+ return _regenerator().w(function (_context4) {
3550
+ while (1) switch (_context4.p = _context4.n) {
3551
+ case 0:
3552
+ _ref = _args4.length > 2 && _args4[2] !== undefined ? _args4[2] : {}, _ref$keepCacheOnError = _ref.keepCacheOnError, keepCacheOnError = _ref$keepCacheOnError === void 0 ? false : _ref$keepCacheOnError;
3553
+ url = jwksUrl(config.issuer);
3554
+ existing = inFlight.get(url);
3555
+ if (!existing) {
3556
+ _context4.n = 1;
3557
+ break;
3558
+ }
3559
+ return _context4.a(2, existing);
3560
+ case 1:
3561
+ fetchImpl = (_config$fetchImpl = config.fetchImpl) !== null && _config$fetchImpl !== void 0 ? _config$fetchImpl : fetch;
3562
+ pending = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() {
3563
+ var res, body, keys;
3564
+ return _regenerator().w(function (_context3) {
3565
+ while (1) switch (_context3.p = _context3.n) {
3566
+ case 0:
3567
+ _context3.p = 0;
3568
+ _context3.n = 1;
3569
+ return fetchImpl(url, {
3570
+ headers: {
3571
+ accept: "application/json"
3572
+ }
3573
+ });
3574
+ case 1:
3575
+ res = _context3.v;
3576
+ if (res.ok) {
3577
+ _context3.n = 2;
3578
+ break;
3579
+ }
3580
+ return _context3.a(2, {
3581
+ status: "error",
3582
+ failedAtMs: nowMs
3583
+ });
3584
+ case 2:
3585
+ _context3.n = 3;
3586
+ return res.json();
3587
+ case 3:
3588
+ body = _context3.v;
3589
+ keys = readKeys(body);
3590
+ if (!(keys === null)) {
3591
+ _context3.n = 4;
3592
+ break;
3593
+ }
3594
+ return _context3.a(2, {
3595
+ status: "error",
3596
+ failedAtMs: nowMs
3597
+ });
3598
+ case 4:
3599
+ return _context3.a(2, {
3600
+ status: "keys",
3601
+ keys: keys,
3602
+ fetchedAtMs: nowMs
3603
+ });
3604
+ case 5:
3605
+ _context3.p = 5;
3606
+ _context3.v;
3607
+ return _context3.a(2, {
3608
+ status: "error",
3609
+ failedAtMs: nowMs
3610
+ });
3611
+ }
3612
+ }, _callee3, null, [[0, 5]]);
3613
+ }))();
3614
+ inFlight.set(url, pending);
3615
+ _context4.p = 2;
3616
+ _context4.n = 3;
3617
+ return pending;
3618
+ case 3:
3619
+ entry = _context4.v;
3620
+ cached = jwksCache.get(url);
3621
+ if (!(entry.status === "error" && keepCacheOnError && (cached === null || cached === void 0 ? void 0 : cached.status) === "keys")) {
3622
+ _context4.n = 4;
3623
+ break;
3624
+ }
3625
+ return _context4.a(2, entry);
3626
+ case 4:
3627
+ jwksCache.set(url, entry);
3628
+ return _context4.a(2, entry);
3629
+ case 5:
3630
+ _context4.p = 5;
3631
+ inFlight["delete"](url);
3632
+ return _context4.f(5);
3633
+ case 6:
3634
+ return _context4.a(2);
3635
+ }
3636
+ }, _callee4, null, [[2,, 5, 6]]);
3637
+ }));
3638
+ return _fetchJwks.apply(this, arguments);
3639
+ }
3640
+ function readKeys(body) {
3641
+ if (_typeof(body) !== "object" || body === null || !("keys" in body)) {
3642
+ return null;
3643
+ }
3644
+ // `in` narrows the property into the type, so no assertion is needed to read
3645
+ // it — and the `Array.isArray` below is what actually establishes the shape.
3646
+ var keys = body.keys;
3647
+ if (!Array.isArray(keys)) {
3648
+ return null;
3649
+ }
3650
+ return keys.filter(function (key) {
3651
+ return _typeof(key) === "object" && key !== null;
3652
+ });
3653
+ }
3654
+
3655
+ /**
3656
+ * Read `Authorization: Bearer …`.
3657
+ *
3658
+ * Exported because the refusal needs to know whether a token was presented at
3659
+ * all: RFC 6750 distinguishes "no credential" — a bare `401`, which is an
3660
+ * invitation to authenticate — from "a bad credential", and a client that gets
3661
+ * the second when it deserved the first will not start the authorization flow.
3662
+ */
3663
+ function readBearerToken(request) {
3664
+ var _match$;
3665
+ var header = request.headers.get("authorization");
3666
+ if (!header) {
3667
+ return null;
3668
+ }
3669
+ var match = /^Bearer\s+(.+)$/i.exec(header.trim());
3670
+ var token = match === null || match === void 0 || (_match$ = match[1]) === null || _match$ === void 0 ? void 0 : _match$.trim();
3671
+ return token ? token : null;
3672
+ }
3673
+ function verifyValAccessToken(_x7, _x8) {
3674
+ return _verifyValAccessToken.apply(this, arguments);
3675
+ }
3676
+ function _verifyValAccessToken() {
3677
+ _verifyValAccessToken = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee5(request, config) {
3678
+ var _config$clockToleranc;
3679
+ var token, parts, _parts, encodedHeader, encodedPayload, encodedSignature, header, kid, nowMs, loaded, candidates, refetched, signature, signedData, verified, payload, tolerance, nowSeconds, subject, scopes;
3680
+ return _regenerator().w(function (_context5) {
3681
+ while (1) switch (_context5.n) {
3682
+ case 0:
3683
+ token = readBearerToken(request);
3684
+ if (!(token === null)) {
3685
+ _context5.n = 1;
3686
+ break;
3687
+ }
3688
+ return _context5.a(2, {
3689
+ status: "refused",
3690
+ error: "invalid_request",
3691
+ description: "This Val MCP endpoint needs an access token. Authorize with the Val authorization server and present it as `Authorization: Bearer`."
3692
+ });
3693
+ case 1:
3694
+ parts = token.split(".");
3695
+ if (!(parts.length !== 3)) {
3696
+ _context5.n = 2;
3697
+ break;
3698
+ }
3699
+ return _context5.a(2, invalidToken("The access token is not a JWS."));
3700
+ case 2:
3701
+ _parts = _slicedToArray(parts, 3), encodedHeader = _parts[0], encodedPayload = _parts[1], encodedSignature = _parts[2];
3702
+ header = decodeJsonSegment(encodedHeader);
3703
+ if (!(header === null)) {
3704
+ _context5.n = 3;
3705
+ break;
3706
+ }
3707
+ return _context5.a(2, invalidToken("The access token's header could not be read."));
3708
+ case 3:
3709
+ if (!(header.alg !== "ES256")) {
3710
+ _context5.n = 4;
3711
+ break;
3712
+ }
3713
+ return _context5.a(2, invalidToken("The access token is not signed with ES256, which is the only algorithm this server accepts."));
3714
+ case 4:
3715
+ kid = typeof header.kid === "string" ? header.kid : null;
3716
+ nowMs = Date.now();
3717
+ _context5.n = 5;
3718
+ return loadJwks(config, nowMs);
3719
+ case 5:
3720
+ loaded = _context5.v;
3721
+ if (!(loaded.entry.status === "error")) {
3722
+ _context5.n = 6;
3723
+ break;
3724
+ }
3725
+ 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."));
3726
+ case 6:
3727
+ candidates = loaded.entry.keys.filter(function (key) {
3728
+ return isVerifyingP256Key(key, kid);
3729
+ });
3730
+ if (!(candidates.length === 0 && kid !== null && loaded.fromCache)) {
3731
+ _context5.n = 9;
3732
+ break;
3733
+ }
3734
+ _context5.n = 7;
3735
+ return refetchForUnknownKid(config, nowMs);
3736
+ case 7:
3737
+ refetched = _context5.v;
3738
+ if (!(refetched !== null)) {
3739
+ _context5.n = 9;
3740
+ break;
3741
+ }
3742
+ if (!(refetched.status === "error")) {
3743
+ _context5.n = 8;
3744
+ break;
3745
+ }
3746
+ 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."));
3747
+ case 8:
3748
+ candidates = refetched.keys.filter(function (key) {
3749
+ return isVerifyingP256Key(key, kid);
3750
+ });
3751
+ case 9:
3752
+ if (!(candidates.length === 0)) {
3753
+ _context5.n = 10;
3754
+ break;
3755
+ }
3756
+ return _context5.a(2, invalidToken("The access token was signed with a key the Val authorization server does not publish."));
3757
+ case 10:
3758
+ signature = decodeBase64Url(encodedSignature);
3759
+ if (!(signature === null)) {
3760
+ _context5.n = 11;
3761
+ break;
3762
+ }
3763
+ return _context5.a(2, invalidToken("The access token's signature could not be read."));
3764
+ case 11:
3765
+ signedData = Buffer.from("".concat(encodedHeader, ".").concat(encodedPayload), "ascii"); // Every published key is tried when the token names no `kid`, so a rotation
3766
+ // that has not yet propagated to clients still verifies. With a `kid` the
3767
+ // filter above leaves one — or, if this process had not yet seen that key,
3768
+ // the one the refetch just found.
3769
+ verified = candidates.some(function (key) {
3770
+ return verifyWithJwk(key, signedData, signature);
3771
+ });
3772
+ if (verified) {
3773
+ _context5.n = 12;
3774
+ break;
3775
+ }
3776
+ return _context5.a(2, invalidToken("The access token's signature could not be verified."));
3777
+ case 12:
3778
+ // Only now: everything below reads the payload, and before this line it was
3779
+ // attacker input.
3780
+ payload = decodeJsonSegment(encodedPayload);
3781
+ if (!(payload === null)) {
3782
+ _context5.n = 13;
3783
+ break;
3784
+ }
3785
+ return _context5.a(2, invalidToken("The access token's payload could not be read."));
3786
+ case 13:
3787
+ tolerance = (_config$clockToleranc = config.clockToleranceSeconds) !== null && _config$clockToleranc !== void 0 ? _config$clockToleranc : DEFAULT_CLOCK_TOLERANCE_SECONDS;
3788
+ nowSeconds = Math.floor(nowMs / 1000);
3789
+ if (!(typeof payload.exp !== "number")) {
3790
+ _context5.n = 14;
3791
+ break;
3792
+ }
3793
+ return _context5.a(2, invalidToken("The access token has no expiry."));
3794
+ case 14:
3795
+ if (!(payload.exp + tolerance <= nowSeconds)) {
3796
+ _context5.n = 15;
3797
+ break;
3798
+ }
3799
+ return _context5.a(2, invalidToken("The access token has expired. Refresh it and try again."));
3800
+ case 15:
3801
+ if (!(typeof payload.nbf === "number" && payload.nbf - tolerance > nowSeconds)) {
3802
+ _context5.n = 16;
3803
+ break;
3804
+ }
3805
+ return _context5.a(2, invalidToken("The access token is not valid yet."));
3806
+ case 16:
3807
+ if (!(payload.iss !== config.issuer)) {
3808
+ _context5.n = 17;
3809
+ break;
3810
+ }
3811
+ return _context5.a(2, invalidToken("The access token was not issued by this server's authorization server (iss claim)."));
3812
+ case 17:
3813
+ if (audienceMatches(payload.aud, config.resource)) {
3814
+ _context5.n = 18;
3815
+ break;
3816
+ }
3817
+ return _context5.a(2, invalidToken("The access token is not valid for this server (aud claim)."));
3818
+ case 18:
3819
+ subject = payload.sub;
3820
+ if (!(typeof subject !== "string" || subject.length === 0)) {
3821
+ _context5.n = 19;
3822
+ break;
3823
+ }
3824
+ return _context5.a(2, invalidToken("The access token has no subject."));
3825
+ case 19:
3826
+ scopes = readScopes(payload.scope);
3827
+ if (scopes.includes(VAL_SCOPE_READ)) {
3828
+ _context5.n = 20;
3829
+ break;
3830
+ }
3831
+ return _context5.a(2, {
3832
+ status: "refused",
3833
+ error: "insufficient_scope",
3834
+ description: "The access token does not have the ".concat(VAL_SCOPE_READ, " scope, so it cannot read any content.")
3835
+ });
3836
+ case 20:
3837
+ return _context5.a(2, {
3838
+ status: "ok",
3839
+ auth: {
3840
+ type: "verified-profile",
3841
+ profileId: authorIdFromVerifiedSubject(subject),
3842
+ scopes: scopes
3843
+ }
3844
+ });
3845
+ }
3846
+ }, _callee5);
3847
+ }));
3848
+ return _verifyValAccessToken.apply(this, arguments);
3849
+ }
3850
+ function invalidToken(description) {
3851
+ // Described by class, never by echoing the token or a raw error: a
3852
+ // verification failure message is a place credentials leak into logs.
3853
+ return {
3854
+ status: "refused",
3855
+ error: "invalid_token",
3856
+ description: description
3857
+ };
3858
+ }
3859
+
3860
+ /**
3861
+ * `aud` is a string or an array of strings (RFC 7519 section 4.1.3).
3862
+ *
3863
+ * A match on any member is a match, which is the spec's own rule — a token may
3864
+ * legitimately be addressed to several resources.
3865
+ */
3866
+ function audienceMatches(claim, resource) {
3867
+ if (typeof claim === "string") {
3868
+ return claim === resource;
3869
+ }
3870
+ if (Array.isArray(claim)) {
3871
+ return claim.some(function (entry) {
3872
+ return entry === resource;
3873
+ });
3874
+ }
3875
+ return false;
3876
+ }
3877
+
3878
+ /**
3879
+ * `scope` is a space-delimited string (RFC 6749 section 3.3).
3880
+ *
3881
+ * Anything else is read as no scopes rather than coerced. A token whose scope
3882
+ * claim is the wrong shape is a token we do not understand, and understanding
3883
+ * it generously is how a write gets authorized by an array someone sent.
3884
+ */
3885
+ function readScopes(claim) {
3886
+ if (typeof claim !== "string") {
3887
+ return [];
3888
+ }
3889
+ return claim.split(" ").filter(function (scope) {
3890
+ return scope.length > 0;
3891
+ });
3892
+ }
3893
+ function isVerifyingP256Key(key, kid) {
3894
+ if (key.kty !== "EC" || key.crv !== "P-256") {
3895
+ return false;
3896
+ }
3897
+ if (typeof key.x !== "string" || typeof key.y !== "string") {
3898
+ return false;
3899
+ }
3900
+ // A key published for encryption is not a key to verify signatures with, and
3901
+ // an `alg` that disagrees with what we verify is a key meant for something
3902
+ // else.
3903
+ if (key.use !== undefined && key.use !== "sig") {
3904
+ return false;
3905
+ }
3906
+ if (key.alg !== undefined && key.alg !== "ES256") {
3907
+ return false;
3908
+ }
3909
+ if (kid !== null && typeof key.kid === "string" && key.kid !== kid) {
3910
+ return false;
3911
+ }
3912
+ return true;
3913
+ }
3914
+ function verifyWithJwk(key, signedData, signature) {
3915
+ try {
3916
+ var publicKey = createPublicKey({
3917
+ key: {
3918
+ kty: "EC",
3919
+ crv: "P-256",
3920
+ x: String(key.x),
3921
+ y: String(key.y)
3922
+ },
3923
+ format: "jwk"
3924
+ });
3925
+ return verify("sha256", signedData,
3926
+ // `ieee-p1363` because a JWS ECDSA signature is the raw `r||s` pair, while
3927
+ // node defaults to DER for EC keys. Getting this wrong rejects every
3928
+ // valid signature.
3929
+ {
3930
+ key: publicKey,
3931
+ dsaEncoding: "ieee-p1363"
3932
+ }, signature);
3933
+ } catch (_unused) {
3934
+ // A malformed key in an otherwise good key set should not take down
3935
+ // verification against the other keys.
3936
+ return false;
3937
+ }
3938
+ }
3939
+ function decodeBase64Url(segment) {
3940
+ if (segment === undefined || !/^[A-Za-z0-9_-]*$/.test(segment)) {
3941
+ return null;
3942
+ }
3943
+ try {
3944
+ return Buffer.from(segment, "base64url");
3945
+ } catch (_unused2) {
3946
+ return null;
3947
+ }
3948
+ }
3949
+ function decodeJsonSegment(segment) {
3950
+ var decoded = decodeBase64Url(segment);
3951
+ if (decoded === null) {
3952
+ return null;
3953
+ }
3954
+ try {
3955
+ var parsed = JSON.parse(decoded.toString("utf8"));
3956
+ if (_typeof(parsed) !== "object" || parsed === null || Array.isArray(parsed)) {
3957
+ return null;
3958
+ }
3959
+ return _objectSpread2({}, parsed);
3960
+ } catch (_unused3) {
3961
+ return null;
3962
+ }
3963
+ }
3964
+
3965
+ /**
3966
+ * The one document an MCP client needs before it can authorize: RFC 9728
3967
+ * Protected Resource Metadata, served by the *resource* server.
3968
+ *
3969
+ * This is how a client discovers where to authorize. It asks the resource — this
3970
+ * app — and the resource names its authorization server. Which is why this
3971
+ * belongs here and the RFC 8414 *authorization server* metadata does not: that
3972
+ * document lives at the issuer, describes the issuer's own endpoints, and is
3973
+ * served by the issuer. An app serving a copy would be asserting the issuer's
3974
+ * configuration on its behalf, and would be wrong the moment the issuer changed
3975
+ * anything.
3976
+ *
3977
+ * The flow, so the split reads as a whole:
3978
+ *
3979
+ * 1. client → `{app}/api/mcp` with no token → `401` naming this document
3980
+ * 2. client → `{app}/.well-known/oauth-protected-resource` → the issuer
3981
+ * 3. client → `{issuer}/.well-known/oauth-authorization-server` → endpoints
3982
+ * 4. client → issuer's `/authorize`, then `/token`
3983
+ * 5. client → `{app}/api/mcp` with the token
3984
+ */
3985
+
3986
+ var CORS_HEADERS = {
3987
+ // The document is public and contains no secrets — it exists to be read by
3988
+ // clients whose origin we cannot know in advance, so `*` is the correct value
3989
+ // rather than a lazy one. Note there is no `Access-Control-Allow-Credentials`:
3990
+ // with it, `*` would be rejected by browsers, and this document is never
3991
+ // fetched with credentials.
3992
+ "Access-Control-Allow-Origin": "*",
3993
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
3994
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, MCP-Protocol-Version",
3995
+ "Access-Control-Max-Age": "3600"
3996
+ };
3997
+ function createValMcpMetadata(oauth, scopesSupported) {
3998
+ var document = {
3999
+ // The resource identifier, which MUST be the value clients send as
4000
+ // `resource` and the value that arrives back in `aud`. Same string as the
4001
+ // audience this app verifies against — one value, so the two cannot drift.
4002
+ resource: oauth.resource,
4003
+ authorization_servers: [oauth.issuer],
4004
+ scopes_supported: scopesSupported,
4005
+ bearer_methods_supported: ["header"]
4006
+ };
4007
+ var body = JSON.stringify(document);
4008
+ return {
4009
+ GET: function GET() {
4010
+ return new Response(body, {
4011
+ status: 200,
4012
+ headers: _objectSpread2({
4013
+ "Content-Type": "application/json",
4014
+ // Cacheable: it changes only when the app is reconfigured, and a
4015
+ // client that re-reads it on every authorization costs a round trip
4016
+ // for nothing.
4017
+ "Cache-Control": "public, max-age=3600"
4018
+ }, CORS_HEADERS)
4019
+ });
4020
+ },
4021
+ OPTIONS: function OPTIONS() {
4022
+ return new Response(null, {
4023
+ status: 204,
4024
+ headers: CORS_HEADERS
4025
+ });
4026
+ }
4027
+ };
4028
+ }
4029
+
4030
+ /**
4031
+ * The `WWW-Authenticate` value for a refusal (RFC 6750 section 3, RFC 9728
4032
+ * section 5.1).
4033
+ *
4034
+ * `resource_metadata` is the load-bearing parameter: it is how a client that has
4035
+ * never seen this server learns where to authorize. A `401` without it is a dead
4036
+ * end — the client knows it needs a token and has no way to find out from where.
4037
+ */
4038
+ function wwwAuthenticate(oauth, scopesSupported, refusal) {
4039
+ var metadataUrl = new URL("/.well-known/oauth-protected-resource", oauth.resource).toString();
4040
+ var params = ["resource_metadata=\"".concat(metadataUrl, "\""), "scope=\"".concat(scopesSupported.join(" "), "\"")];
4041
+ if (refusal) {
4042
+ params.push("error=\"".concat(refusal.error, "\""));
4043
+ params.push("error_description=\"".concat(headerSafe(refusal.description), "\""));
4044
+ }
4045
+ return "Bearer ".concat(params.join(", "));
4046
+ }
4047
+
4048
+ /**
4049
+ * Make a string safe to put inside a quoted header parameter.
4050
+ *
4051
+ * Three classes go, and the third is the one that matters most:
4052
+ *
4053
+ * - a **quote** would close the parameter early;
4054
+ * - a **backslash** would start an escape the rest of the value does not
4055
+ * finish;
4056
+ * - a **CR or LF** would end the header line, which is response splitting — an
4057
+ * attacker-influenced description could inject a header of their own, or a
4058
+ * whole second response.
4059
+ *
4060
+ * The descriptions passed here today are all literals from this package and
4061
+ * contain none of it. That is a property of today's callers rather than of the
4062
+ * type, and this function exists so it stays true when a future one interpolates
4063
+ * something from a request.
4064
+ */
4065
+ function headerSafe(value) {
4066
+ // eslint-disable-next-line no-control-regex -- the point is to remove them
4067
+ return value.replace(/["\\]/g, "").replace(/[\u0000-\u001f\u007f]/g, " ");
4068
+ }
4069
+
4070
+ /**
4071
+ * Val's tools over MCP, and the two checks that have to happen before a request
4072
+ * gets to them.
4073
+ *
4074
+ * Nothing here imports an MCP SDK. The app owns the transport — which SDK, which
4075
+ * route, which framework — and this owns the parts that must not be re-decided
4076
+ * per app: whether the request is allowed to reach the tools at all, and whose
4077
+ * credential it carries. `docs/plans/mcp.md` Part A has the reasoning; the short
4078
+ * version is that the SDK reorganised itself once already, and the security
4079
+ * checks should not move when it does again.
4080
+ */
4081
+
4082
+ function initValMcp(valModules, config, opts) {
4083
+ var _opts$route, _opts$versions$core, _opts$versions, _opts$versions$next, _opts$versions2;
4084
+ var route = (_opts$route = opts === null || opts === void 0 ? void 0 : opts.route) !== null && _opts$route !== void 0 ? _opts$route : "/api/val";
4085
+ var coreVersion = (_opts$versions$core = opts === null || opts === void 0 || (_opts$versions = opts.versions) === null || _opts$versions === void 0 ? void 0 : _opts$versions.core) !== null && _opts$versions$core !== void 0 ? _opts$versions$core : Internal.VERSION.core;
4086
+ if (!coreVersion) {
4087
+ throw new Error("Could not get @valbuild/core package version");
4088
+ }
4089
+ var hostVersion = (_opts$versions$next = opts === null || opts === void 0 || (_opts$versions2 = opts.versions) === null || _opts$versions2 === void 0 ? void 0 : _opts$versions2.next) !== null && _opts$versions$next !== void 0 ? _opts$versions$next : VERSION;
4090
+ if (!hostVersion) {
4091
+ throw new Error("Could not get @valbuild/mcp package version");
4092
+ }
4093
+
4094
+ // Resolved once at module-eval time, awaited per request. The no-op catch is
4095
+ // load bearing for the same reason it is in createValApiRouter: a config error
4096
+ // on a promise with no handler attached becomes an unhandledRejection and
4097
+ // takes the dev server down, and the error is reported per request below
4098
+ // anyway.
4099
+ var setupPromise = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
4100
+ var options;
4101
+ return _regenerator().w(function (_context) {
4102
+ while (1) switch (_context.n) {
4103
+ case 0:
4104
+ _context.n = 1;
4105
+ return initHandlerOptions(route, _objectSpread2(_objectSpread2({}, config), {}, {
4106
+ versions: {
4107
+ core: coreVersion,
4108
+ next: hostVersion
4109
+ }
4110
+ }), config);
4111
+ case 1:
4112
+ options = _context.v;
4113
+ return _context.a(2, {
4114
+ mode: options.mode,
4115
+ tools: createValTools(valModules, _objectSpread2(_objectSpread2({}, options), {}, {
4116
+ formatter: opts === null || opts === void 0 ? void 0 : opts.formatter
4117
+ }), opts === null || opts === void 0 ? void 0 : opts.extraTools)
4118
+ });
4119
+ }
4120
+ }, _callee);
4121
+ }))();
4122
+ setupPromise["catch"](function () {
4123
+ // handled per request
4124
+ });
4125
+ var oauth = opts === null || opts === void 0 ? void 0 : opts.oauth;
4126
+ var scopesSupported = [VAL_SCOPE_READ, VAL_SCOPE_WRITE];
4127
+ return {
4128
+ valMcpMetadata: oauth ? createValMcpMetadata(oauth, scopesSupported) : null,
4129
+ valMcpTools: function valMcpTools() {
4130
+ return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
4131
+ return _regenerator().w(function (_context2) {
4132
+ while (1) switch (_context2.n) {
4133
+ case 0:
4134
+ _context2.n = 1;
4135
+ return setupPromise;
4136
+ case 1:
4137
+ return _context2.a(2, _context2.v.tools);
4138
+ }
4139
+ }, _callee2);
4140
+ }))();
4141
+ },
4142
+ valMcpAuthorize: function valMcpAuthorize(request) {
4143
+ return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() {
4144
+ var setup, refusal, sessionId, verified, status, _t;
4145
+ return _regenerator().w(function (_context3) {
4146
+ while (1) switch (_context3.p = _context3.n) {
4147
+ case 0:
4148
+ _context3.p = 0;
4149
+ _context3.n = 1;
4150
+ return setupPromise;
4151
+ case 1:
4152
+ setup = _context3.v;
4153
+ _context3.n = 3;
4154
+ break;
4155
+ case 2:
4156
+ _context3.p = 2;
4157
+ _t = _context3.v;
4158
+ return _context3.a(2, {
4159
+ status: "refused",
4160
+ response: jsonResponse(500, {
4161
+ error: "Val: could not start the Val MCP server",
4162
+ details: _t instanceof Error ? _t.message : String(_t)
4163
+ })
4164
+ });
4165
+ case 3:
4166
+ if (!(request === undefined)) {
4167
+ _context3.n = 4;
4168
+ break;
4169
+ }
4170
+ return _context3.a(2, {
4171
+ status: "refused",
4172
+ response: jsonResponse(401, {
4173
+ error: "Val: this MCP server needs the HTTP request to authorize a call, and none was available."
4174
+ })
4175
+ });
4176
+ case 4:
4177
+ refusal = refuseUnsafeRequest(request, setup.mode);
4178
+ if (!refusal) {
4179
+ _context3.n = 5;
4180
+ break;
4181
+ }
4182
+ return _context3.a(2, {
4183
+ status: "refused",
4184
+ response: refusal
4185
+ });
4186
+ case 5:
4187
+ // Not the MCP session id, in any branch below. Val's patch `sessionId`
4188
+ // names a Val AI session, and putting an unrelated id in it would claim a
4189
+ // relationship that does not exist.
4190
+ sessionId = null;
4191
+ if (oauth) {
4192
+ _context3.n = 8;
4193
+ break;
4194
+ }
4195
+ if (!(setup.mode !== "fs")) {
4196
+ _context3.n = 6;
4197
+ break;
4198
+ }
4199
+ return _context3.a(2, {
4200
+ status: "refused",
4201
+ response: jsonResponse(500, {
4202
+ error: "Val: this MCP endpoint has no `oauth` config, and this project is configured to talk to the Val content backend. Pass `oauth` to `initValMcp` with the authorization server's URL and this endpoint's own URL, so callers authorize as themselves."
4203
+ })
4204
+ });
4205
+ case 6:
4206
+ if (!(readBearerToken(request) !== null)) {
4207
+ _context3.n = 7;
4208
+ break;
4209
+ }
4210
+ return _context3.a(2, {
4211
+ status: "refused",
4212
+ response: jsonResponse(400, {
4213
+ error: "Val: this project is running in local filesystem mode, where there is nothing to authenticate against — the tools read and write this working tree directly. Remove the credential from this MCP client's configuration: a token sent here is checked by nothing."
4214
+ })
4215
+ });
4216
+ case 7:
4217
+ return _context3.a(2, {
4218
+ status: "ok",
4219
+ tools: setup.tools,
4220
+ ctx: {
4221
+ auth: null,
4222
+ sessionId: sessionId
4223
+ }
4224
+ });
4225
+ case 8:
4226
+ _context3.n = 9;
4227
+ return verifyValAccessToken(request, oauth);
4228
+ case 9:
4229
+ verified = _context3.v;
4230
+ if (!(verified.status === "refused")) {
4231
+ _context3.n = 10;
4232
+ break;
4233
+ }
4234
+ // 401 for a missing or bad token, 403 once the token is good but does
4235
+ // not carry the scope: RFC 6750 section 3.1, and the distinction is
4236
+ // what tells a client whether to authorize again or to give up.
4237
+ status = verified.error === "insufficient_scope" ? 403 : 401;
4238
+ return _context3.a(2, {
4239
+ status: "refused",
4240
+ response: new Response(JSON.stringify({
4241
+ error: verified.error,
4242
+ error_description: verified.description
4243
+ }), {
4244
+ status: status,
4245
+ headers: {
4246
+ "Content-Type": "application/json",
4247
+ "WWW-Authenticate": wwwAuthenticate(oauth, scopesSupported, {
4248
+ error: verified.error,
4249
+ description: verified.description
4250
+ })
4251
+ }
4252
+ })
4253
+ });
4254
+ case 10:
4255
+ return _context3.a(2, {
4256
+ status: "ok",
4257
+ tools: setup.tools,
4258
+ ctx: {
4259
+ auth: verified.auth,
4260
+ sessionId: sessionId
4261
+ }
4262
+ });
4263
+ }
4264
+ }, _callee3, null, [[0, 2]]);
4265
+ }))();
4266
+ }
4267
+ };
4268
+ }
4269
+
4270
+ /**
4271
+ * The two ways this route is dangerous, both refused here.
4272
+ *
4273
+ * 1. **Local filesystem mode outside development.** In fs mode there is no
4274
+ * credential and no backend: the tools read and write the running process's
4275
+ * own working tree, and every permission check Val has lives on the other
4276
+ * side of a backend that is not in this path. Exposed on a deployed host,
4277
+ * that is unauthenticated write access to the site's content for anyone who
4278
+ * can reach the port. There is no configuration that makes it safe, so there
4279
+ * is no flag to turn this off — a project that wants MCP in production wants
4280
+ * proxy mode with an `oauth` config, where every call carries an access
4281
+ * token this app verified itself.
4282
+ *
4283
+ * 2. **A browser driving the local server.** A page on any origin can `fetch`
4284
+ * `http://localhost:3000/api/mcp` while a developer has the app running, and
4285
+ * with DNS rebinding it can do so with a `Host` of its own choosing. Neither
4286
+ * needs a credential in fs mode. So a cross-origin `Origin` is refused, and
4287
+ * in fs mode the request must actually be addressed to a loopback host.
4288
+ *
4289
+ * MCP clients are not browsers and send no `Origin`, so the check costs them
4290
+ * nothing.
4291
+ */
4292
+ function refuseUnsafeRequest(request, mode) {
4293
+ if (mode === "fs" && process.env.NODE_ENV !== "development") {
4294
+ return jsonResponse(403, {
4295
+ 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."
4296
+ });
4297
+ }
4298
+ var host = requestHost(request);
4299
+ var origin = request.headers.get("origin");
4300
+ if (origin !== null) {
4301
+ // `Origin: null` is refused along with the rest. It is the *opaque* origin —
4302
+ // a sandboxed iframe, a `file://` page, some redirects — so it cannot be
4303
+ // compared to anything, and "cannot be compared" has to mean refuse: a page
4304
+ // that would fail the check can otherwise pass it by arranging to have no
4305
+ // origin at all. Absent entirely is the case that is allowed, and that is
4306
+ // the one MCP clients produce.
4307
+ var originHost = origin === "null" ? null : hostOf(origin);
4308
+ if (originHost === null || host === null || originHost !== host) {
4309
+ return jsonResponse(403, {
4310
+ error: "Val: refusing a cross-origin MCP request from ".concat(JSON.stringify(origin), ". MCP clients do not send an Origin header; a browser does.")
4311
+ });
4312
+ }
4313
+ }
4314
+ if (mode === "fs") {
4315
+ var hostname = host === null ? null : hostnameOf(host);
4316
+ if (hostname === null || !LOOPBACK_HOSTNAMES.has(hostname)) {
4317
+ return jsonResponse(403, {
4318
+ 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.")
4319
+ });
4320
+ }
4321
+ }
4322
+ return null;
4323
+ }
4324
+ var LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
4325
+
4326
+ /**
4327
+ * Which host the request was addressed to, as `hostname:port`.
4328
+ *
4329
+ * `Host` only, and deliberately **not** `X-Forwarded-Host`. The forwarded header
4330
+ * is what a client asked for behind a proxy, but nothing stops a client sending
4331
+ * it directly — so preferring it hands an attacker the value both checks below
4332
+ * are decided on. `Host`, by contrast, a browser sets from the URL and page
4333
+ * script cannot override, which is exactly the property the loopback check
4334
+ * depends on.
4335
+ *
4336
+ * The cost is that behind a proxy that rewrites `Host`, a *browser* request
4337
+ * whose `Origin` is the public name no longer matches. That is acceptable: such
4338
+ * a request carries no access token, so proxy mode refuses it anyway, and a
4339
+ * non-browser MCP client sends no `Origin` and never reaches the comparison.
4340
+ * Trusting the forwarded header would need an explicit trusted-proxy
4341
+ * configuration, which is a bigger thing than this needs.
4342
+ */
4343
+ function requestHost(request) {
4344
+ var host = request.headers.get("host");
4345
+ if (host) {
4346
+ return host.trim().toLowerCase();
4347
+ }
4348
+ // Last resort: the URL the framework saw.
4349
+ try {
4350
+ return new URL(request.url).host.toLowerCase();
4351
+ } catch (_unused) {
4352
+ return null;
4353
+ }
4354
+ }
4355
+ function hostOf(origin) {
4356
+ try {
4357
+ return new URL(origin).host.toLowerCase();
4358
+ } catch (_unused2) {
4359
+ return null;
4360
+ }
4361
+ }
4362
+
4363
+ /** Strips the port, keeping IPv6 brackets — `[::1]:3000` is hostname `[::1]`. */
4364
+ function hostnameOf(host) {
4365
+ if (host.startsWith("[")) {
4366
+ var end = host.indexOf("]");
4367
+ return end === -1 ? host : host.slice(0, end + 1);
4368
+ }
4369
+ var colon = host.indexOf(":");
4370
+ return colon === -1 ? host : host.slice(0, colon);
4371
+ }
4372
+ function jsonResponse(status, body) {
4373
+ return new Response(JSON.stringify(body), {
4374
+ status: status,
4375
+ headers: {
4376
+ "Content-Type": "application/json"
4377
+ }
4378
+ });
4379
+ }
4380
+
4381
+ export { VAL_SCOPE_READ, VAL_SCOPE_WRITE, authorIdFromVerifiedSubject, createValImageTools, createValTools, defineTool, err, initValMcp, ok, savePatch };