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