@valbuild/server 0.123.2 → 0.124.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.
@@ -1402,6 +1402,43 @@ function resolveExistingJsonPath(moduleFilePath, importPath) {
1402
1402
  * only way to avoid loading every entry twice.
1403
1403
  */
1404
1404
 
1405
+ /**
1406
+ * The part of a `.jsonValues()` record this file calls. Named structurally
1407
+ * because the value may not be an instance of *this* copy of `RecordSchema` —
1408
+ * see {@link isJsonValuesRecord}.
1409
+ */
1410
+
1411
+ /**
1412
+ * Is this a `.jsonValues()` record?
1413
+ *
1414
+ * NOT `schema instanceof RecordSchema`, and the distinction is the whole point.
1415
+ * `loadValModules` evaluates the project's `*.val.ts` with a `require` rooted at
1416
+ * the PROJECT, deliberately, so the schemas are built by whatever
1417
+ * `@valbuild/core` the project resolves. When the CLI is itself installed in
1418
+ * that project, that is this very module and `instanceof` holds. When it is run
1419
+ * through `npx` / `pnpm dlx`, the tool gets its own copy of `@valbuild/core`
1420
+ * beside the project's, and `instanceof` is false for a record that is a record
1421
+ * in every way that matters — same class name, same serialized schema, same
1422
+ * methods, different realm.
1423
+ *
1424
+ * That failed OPEN: the whole check returned "no errors", so
1425
+ * `npx @valbuild/cli validate` called a module with un-extracted entries VALID
1426
+ * and a CI gate on it was green for the wrong reason. `loadValModules` avoids
1427
+ * constructor checks for exactly this reason and says so; this one was the
1428
+ * exception.
1429
+ *
1430
+ * `validateJsonEntryContent` is declared by `RecordSchema` and by nothing else,
1431
+ * so it is the brand — and checking it first keeps the cheap short-circuit
1432
+ * `instanceof` gave us, rather than serializing every non-record schema.
1433
+ */
1434
+ function isJsonValuesRecord(schema) {
1435
+ if (!("validateJsonEntryContent" in schema) || typeof schema.validateJsonEntryContent !== "function") {
1436
+ return false;
1437
+ }
1438
+ const serialized = schema["executeSerialize"]();
1439
+ return serialized.type === "record" && serialized.jsonValues === true;
1440
+ }
1441
+
1405
1442
  /**
1406
1443
  * Validates the content of every `.jsonValues()` entry in a module by loading
1407
1444
  * each backing `*.val.json` (via its lazy import thunk) and checking it against
@@ -1459,10 +1496,7 @@ async function validateJsonValuesEntries(schema, source, modulePath) {
1459
1496
  errors: out,
1460
1497
  loadedEntries
1461
1498
  };
1462
- if (!(schema instanceof core.RecordSchema)) {
1463
- return res;
1464
- }
1465
- if (!schema["executeSerialize"]().jsonValues) {
1499
+ if (!isJsonValuesRecord(schema)) {
1466
1500
  return res;
1467
1501
  }
1468
1502
  if (source === null || typeof source !== "object" || Array.isArray(source)) {
@@ -4084,6 +4118,17 @@ class ValOps {
4084
4118
 
4085
4119
  /** One file's bytes as they were at one commit. */
4086
4120
 
4121
+ /**
4122
+ * Where a module lives in the REPOSITORY, as `getFileAtCommit` wants it.
4123
+ *
4124
+ * A `ModuleFilePath` is project-relative (`/app/page.val.ts`); a git path is
4125
+ * repository-relative and carries the project root in front of it
4126
+ * (`examples/next/app/page.val.ts`). Only the ops know that root, which is
4127
+ * why this is here rather than computed by the history functions - and why a
4128
+ * history function that needs to read a module's own file at a commit has to
4129
+ * ask instead of concatenating.
4130
+ */
4131
+
4087
4132
  // #endregion history
4088
4133
  }
4089
4134
  function isOnlyFileCheckValidationError(validationError) {
@@ -6729,6 +6774,14 @@ class ValOpsFS extends ValOps {
6729
6774
  kind: "not-supported-in-fs-mode"
6730
6775
  });
6731
6776
  }
6777
+ gitPathOfModule(
6778
+ // Unused: fs mode has no repository to be relative to. Named anyway, so
6779
+ // this reads as the same method the base class declares.
6780
+ _moduleFilePath) {
6781
+ return fp.result.err({
6782
+ kind: "not-supported-in-fs-mode"
6783
+ });
6784
+ }
6732
6785
  // #endregion history
6733
6786
  }
6734
6787
  class FSOpsHost {
@@ -8520,6 +8573,19 @@ class ValOpsHttp extends ValOps {
8520
8573
  }
8521
8574
  return fp.result.ok(res.value.files);
8522
8575
  }
8576
+
8577
+ /**
8578
+ * `root` in front, and never a doubled or missing slash.
8579
+ *
8580
+ * `root` is "" for a project at the repository root and something like
8581
+ * `examples/next` otherwise; a ModuleFilePath always starts with "/". Joining
8582
+ * them by hand at each call site is how one of these ends up with "//" in the
8583
+ * middle, which GitHub answers with a 404 that reads like a missing file.
8584
+ */
8585
+ gitPathOfModule(moduleFilePath) {
8586
+ const joined = `${this.root}/${moduleFilePath}`.split("/").filter(part => part !== "").join("/");
8587
+ return fp.result.ok(joined);
8588
+ }
8523
8589
  async getFileAtCommit(commitSha, filePath, remote) {
8524
8590
  const params = new URLSearchParams({
8525
8591
  path: filePath
@@ -8979,6 +9045,129 @@ async function getModuleAtCommit(ops, commitSha, moduleFilePath) {
8979
9045
  });
8980
9046
  }
8981
9047
 
9048
+ /**
9049
+ * One `.jsonValues()` entry's content, as it was at a commit.
9050
+ *
9051
+ * The history pane renders a commit through the real field components, and a
9052
+ * `.jsonValues()` record's source is only `{_type:"json"}` markers - the
9053
+ * content lives in a separate `*.val.json` per entry, fetched on demand. The
9054
+ * live Studio fetches it from `GET /json`; the commit pane had nothing to
9055
+ * fetch from, so every entry stayed a marker and rendered as though the field
9056
+ * were empty. Which is worse than an error: an empty value is a claim, and it
9057
+ * was a false one.
9058
+ *
9059
+ * ## Why this needs the module's own `.val.ts`
9060
+ *
9061
+ * There is no derivable relationship between a record key and its entry file.
9062
+ * From the example app:
9063
+ *
9064
+ * "/support/getting-started": c.json(() => import("./content/getting-started.val.json"))
9065
+ * "/support/faq": c.json(() => import("./content/faq.val.json"))
9066
+ *
9067
+ * The key is a route and the path is whatever the author wrote. The only place
9068
+ * the two are related is the `import()` literal inside the `.val.ts`, which is
9069
+ * why `findJsonEntryFilePathsInSource` reads them out of the AST too. The
9070
+ * commit archive stores the module's SOURCE (the markers) and its schema, not
9071
+ * its text - so the text is fetched from git at that commit and parsed here.
9072
+ *
9073
+ * Two round trips per entry, then: the module, and the entry. The module's
9074
+ * parse is worth caching per (commit, module) by the caller; nothing here does,
9075
+ * because a `ValOps` is not a cache and the endpoint above it is the layer that
9076
+ * knows how long a commit stays interesting.
9077
+ */
9078
+ async function getJsonEntryAtCommit(ops, commitSha, moduleFilePath, key) {
9079
+ const moduleGitPath = ops.gitPathOfModule(moduleFilePath);
9080
+ if (fp.result.isErr(moduleGitPath)) {
9081
+ return moduleGitPath;
9082
+ }
9083
+ const moduleRes = await ops.getFileAtCommit(commitSha, moduleGitPath.value, false);
9084
+ if (fp.result.isErr(moduleRes)) {
9085
+ return moduleRes;
9086
+ }
9087
+ const entryPath = findEntryImportPath(moduleFilePath, moduleGitPath.value, moduleRes.value.toString("utf-8"), key);
9088
+ if (fp.result.isErr(entryPath)) {
9089
+ return entryPath;
9090
+ }
9091
+ // The entry path resolved above is PROJECT-relative, like the module path it
9092
+ // came from; the git path needs the root in front of it, and the ops are what
9093
+ // know the root. Reusing gitPathOfModule rather than re-deriving the prefix:
9094
+ // a `.val.json` is not a module, but "project-relative path -> repo path" is
9095
+ // the same question and one implementation of it is the point.
9096
+ const entryGitPath = ops.gitPathOfModule(entryPath.value);
9097
+ if (fp.result.isErr(entryGitPath)) {
9098
+ return entryGitPath;
9099
+ }
9100
+ const entryRes = await ops.getFileAtCommit(commitSha, entryGitPath.value, false);
9101
+ if (fp.result.isErr(entryRes)) {
9102
+ return entryRes;
9103
+ }
9104
+ try {
9105
+ return fp.result.ok(JSON.parse(entryRes.value.toString("utf-8")));
9106
+ } catch (err) {
9107
+ return fp.result.err({
9108
+ kind: "file-unavailable",
9109
+ gitPath: entryGitPath.value,
9110
+ message: `not valid JSON at this commit: ${err instanceof Error ? err.message : String(err)}`
9111
+ });
9112
+ }
9113
+ }
9114
+
9115
+ /**
9116
+ * The `import()` path a key's entry is behind, from the module's text.
9117
+ *
9118
+ * Exported for its own test: this is the part with a real chance of being
9119
+ * wrong, and it is pure.
9120
+ */
9121
+ function findEntryImportPath(moduleFilePath,
9122
+ /**
9123
+ * The same module, repository-relative, for error reporting only.
9124
+ *
9125
+ * A `gitPath` in a HistoryError is a REPOSITORY path - that is what every
9126
+ * other history helper reports, and what a reader can paste into a `git show`
9127
+ * - so reporting the project-relative ModuleFilePath here named a file that
9128
+ * does not exist at that path for any project not rooted at the repo root.
9129
+ */
9130
+ moduleGitPath, valTsSource, key) {
9131
+ const sourceFile = ts__default["default"].createSourceFile(moduleFilePath, valTsSource, ts__default["default"].ScriptTarget.ES2015, true);
9132
+ let analysis;
9133
+ try {
9134
+ analysis = analyzeValModule(sourceFile);
9135
+ } catch (err) {
9136
+ // `file-unavailable` rather than a kind of its own: the module's TEXT is
9137
+ // the file we could not use, and adding a wire kind for "unparseable"
9138
+ // would be a new case every reader has to handle to say the same thing.
9139
+ return fp.result.err({
9140
+ kind: "file-unavailable",
9141
+ gitPath: moduleGitPath,
9142
+ message: `could not parse the module at this commit: ${err instanceof Error ? err.message : String(err)}`
9143
+ });
9144
+ }
9145
+ if (fp.result.isErr(analysis)) {
9146
+ return fp.result.err({
9147
+ kind: "file-unavailable",
9148
+ gitPath: moduleGitPath,
9149
+ message: "could not read the module at this commit"
9150
+ });
9151
+ }
9152
+ const entries = analyzeJsonValuesEntries(analysis.value.source);
9153
+ const entry = entries.get(key);
9154
+ if (entry === undefined) {
9155
+ /*
9156
+ * Reported, not treated as empty. A key with no entry at this commit means
9157
+ * the entry was added later - so "there is nothing to show for it here" is
9158
+ * the true answer, and rendering an empty field instead would say the
9159
+ * author had left it blank.
9160
+ */
9161
+ return fp.result.err({
9162
+ kind: "file-unavailable",
9163
+ gitPath: moduleGitPath,
9164
+ message: `'${key}' had no entry at this commit`
9165
+ });
9166
+ }
9167
+ const moduleDir = path__namespace["default"].posix.dirname(moduleFilePath);
9168
+ return fp.result.ok(path__namespace["default"].posix.join(moduleDir, entry.importPath));
9169
+ }
9170
+
8982
9171
  const host = process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST;
8983
9172
  const SettingsSchema = z.z.object({
8984
9173
  publicProjectId: z.z.string(),
@@ -12274,12 +12463,60 @@ const ValServer = (valModules, options, callbacks) => {
12274
12463
  };
12275
12464
  }
12276
12465
  },
12466
+ "/history/json": {
12467
+ GET: async req => {
12468
+ // Authenticated, for the reasons on /history/files below.
12469
+ const auth = getAuth(req.cookies);
12470
+ if (auth.error) {
12471
+ return {
12472
+ status: 401,
12473
+ json: {
12474
+ message: auth.error
12475
+ }
12476
+ };
12477
+ }
12478
+ const res = await getJsonEntryAtCommit(serverOps, req.query.commit_sha, req.query.path, req.query.key);
12479
+ if (fp.result.isErr(res)) {
12480
+ return historyErrorResponse(res.error);
12481
+ }
12482
+ return {
12483
+ status: 200,
12484
+ json: {
12485
+ path: req.query.path,
12486
+ key: req.query.key,
12487
+ content: res.value
12488
+ }
12489
+ };
12490
+ }
12491
+ },
12277
12492
  "/history/files": {
12278
12493
  GET: async req => {
12279
- // No auth, for the same reason /files has none: this is served to an
12280
- // <img> that the app's own backend may fetch during image
12281
- // optimisation, with no cookies. What it exposes is a file at a commit
12282
- // that is already in the repository.
12494
+ /*
12495
+ * Authenticated, unlike /files.
12496
+ *
12497
+ * This used to reason "same as /files" and that was wrong twice over.
12498
+ * /files is open because a `patch_id` is an unguessable UUID standing
12499
+ * in for a credential, and because it HAS to be: a draft image is
12500
+ * fetched by the app's own backend during Next image optimisation,
12501
+ * backend-to-backend, with no cookies to send.
12502
+ *
12503
+ * A commit sha is not a secret - it is in `git log`, in the GitHub UI,
12504
+ * on every PR - so the first argument does not transfer. And nothing
12505
+ * fetches this server-side; both callers are the Studio in a browser
12506
+ * that has the session cookie (the history pane's <img>, and
12507
+ * stageRestore's fetch, both same-origin so the cookie goes). So the
12508
+ * second does not either, and there is nothing to trade away by
12509
+ * asking.
12510
+ */
12511
+ const auth = getAuth(req.cookies);
12512
+ if (auth.error) {
12513
+ return {
12514
+ status: 401,
12515
+ json: {
12516
+ message: auth.error
12517
+ }
12518
+ };
12519
+ }
12283
12520
  const res = await serverOps.getFileAtCommit(req.query.commit_sha, req.query.path, req.query.remote === "true");
12284
12521
  if (fp.result.isErr(res)) {
12285
12522
  const response = historyErrorResponse(res.error);
@@ -12318,6 +12555,11 @@ const ValServer = (valModules, options, callbacks) => {
12318
12555
  // 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
12319
12556
  // Thus: attack surface + ease of attack + benefit = low probability of attack
12320
12557
  // If we couldn't argue that patch ids are secret enough, then this would be a problem.
12558
+ // Note that /history/files, which looks like the same endpoint, IS
12559
+ // authenticated: its token is a commit sha, which is published, and
12560
+ // nothing fetches it backend-to-backend. Neither half of the argument
12561
+ // above transfers. See architecture/media.md, "Why /files has no auth,
12562
+ // and /history/files does".
12321
12563
  let fileBuffer;
12322
12564
  let mimeType;
12323
12565
  const remote = query.remote === "true";
@@ -14300,37 +14542,50 @@ async function getFileMetadata(projectRoot, validationError) {
14300
14542
  }
14301
14543
 
14302
14544
  /**
14303
- * Moves ONE `.jsonValues()` entry that was written inline in the `.val.ts` into
14304
- * its own `*.val.json`, replacing the inline value with
14305
- * `c.json(() => import("./<key>.val.json"))`.
14545
+ * The two files an extraction touches, as text — nothing written yet.
14306
14546
  *
14307
- * This is the fix for the `jsonValues:extract-entry` validation error. It is not
14308
- * expressible as a patch (a patch edits one `.val.ts` and cannot create the
14309
- * backing JSON file), so it writes both files directly — JSON first, so a
14310
- * failure part-way through never leaves the module pointing at a file that does
14311
- * not exist.
14547
+ * Both paths are project-relative in the `ModuleFilePath` style (a leading
14548
+ * slash), because that is what the callers already hold and what
14549
+ * {@link getNewJsonEntryPaths} derives. Join them onto the project root to get
14550
+ * somewhere to write.
14551
+ */
14552
+
14553
+ /**
14554
+ * Works out what moving ONE inline `.jsonValues()` entry into its own
14555
+ * `*.val.json` would change, without changing anything.
14556
+ *
14557
+ * This is the whole fix for `jsonValues:extract-entry`, minus the writing. It
14558
+ * is split out because the two callers cannot share a way of applying it: the
14559
+ * CLI writes both files ({@link extractJsonValuesEntry}), while the editor has
14560
+ * to hand the same two changes back as a `WorkspaceEdit` so they go through the
14561
+ * undo stack and respect an unsaved buffer. Anything that lived in only one of
14562
+ * those would be a fix that behaves differently depending on where it was
14563
+ * invoked from, which is exactly the drift `codeActions.ts` exists to prevent.
14564
+ *
14565
+ * It does NOT check whether `jsonPath` already exists: that needs a filesystem,
14566
+ * and the editor's answer ("is there a buffer or a file here?") is not the
14567
+ * CLI's. Every caller must check before writing — overwriting an unrelated file
14568
+ * is not a fix.
14312
14569
  *
14313
14570
  * Root-only, like the rest of the `.jsonValues()` machinery: the entry is looked
14314
14571
  * up in the module's root record/router object literal.
14315
14572
  */
14316
- function extractJsonValuesEntry(moduleFilePath, rootDir, entryKey, content, sourceFileHandler) {
14317
- const valTsPath = sourceFileHandler.resolveSourceModulePath(getSyntheticContainingPath(rootDir), `.${moduleFilePath.replace(".val.ts", ".val").replace(".val.js", ".val").replace(".val.jsx", ".val").replace(".val.tsx", ".val")}`);
14318
- const sourceFile = sourceFileHandler.getSourceFile(valTsPath);
14319
- if (!sourceFile) {
14320
- throw Error(`Source file ${valTsPath} not found`);
14321
- }
14573
+ function planJsonValuesEntryExtraction({
14574
+ moduleFilePath,
14575
+ entryKey,
14576
+ content,
14577
+ valTsPath,
14578
+ valTsText
14579
+ }) {
14322
14580
  const pathsRes = getNewJsonEntryPaths(moduleFilePath, entryKey);
14323
14581
  if (fp.result.isErr(pathsRes)) {
14324
- throw Error(formatPatchSourceError(pathsRes.error));
14582
+ return fp.result.err(formatPatchSourceError(pathsRes.error));
14325
14583
  }
14326
14584
  const {
14327
14585
  jsonPath,
14328
14586
  importPath
14329
14587
  } = pathsRes.value;
14330
- const absoluteJsonPath = path__namespace["default"].join(rootDir, jsonPath);
14331
- if (sourceFileHandler.host.fileExists(absoluteJsonPath)) {
14332
- throw Error(`Cannot extract .jsonValues() entry '${entryKey}' of ${moduleFilePath}: '${jsonPath}' already exists`);
14333
- }
14588
+ const sourceFile = ts__default["default"].createSourceFile(valTsPath, valTsText, ts__default["default"].ScriptTarget.ES2020);
14334
14589
 
14335
14590
  // Remove the inline property, then add the `c.json(...)` reference back. The
14336
14591
  // entry moves to the end of the record: entry ORDER in a jsonValues record is
@@ -14338,14 +14593,70 @@ function extractJsonValuesEntry(moduleFilePath, rootDir, entryKey, content, sour
14338
14593
  // insert-in-place would mean reimplementing insertValJsonEntry.
14339
14594
  const removed = removeValJsonEntry(sourceFile, [], entryKey);
14340
14595
  if (fp.result.isErr(removed)) {
14341
- throw Error(`${valTsPath}\n${formatOpsError(removed.error, sourceFile)}`);
14596
+ return fp.result.err(`${valTsPath}\n${formatOpsError(removed.error, sourceFile)}`);
14342
14597
  }
14343
14598
  const inserted = insertValJsonEntry(removed.value, [], entryKey, importPath);
14344
14599
  if (fp.result.isErr(inserted)) {
14345
- throw Error(`${valTsPath}\n${formatOpsError(inserted.error, removed.value)}`);
14600
+ return fp.result.err(`${valTsPath}\n${formatOpsError(inserted.error, removed.value)}`);
14601
+ }
14602
+ return fp.result.ok({
14603
+ jsonPath,
14604
+ jsonContent: JSON.stringify(content, null, 2) + "\n",
14605
+ valTsPath,
14606
+ valTsContent: printSourceFile(inserted.value)
14607
+ });
14608
+ }
14609
+
14610
+ /**
14611
+ * Moves ONE `.jsonValues()` entry that was written inline in the `.val.ts` into
14612
+ * its own `*.val.json`, replacing the inline value with
14613
+ * `c.json(() => import("./<key>.val.json"))`.
14614
+ *
14615
+ * The `val validate --fix` half of {@link planJsonValuesEntryExtraction}. It is
14616
+ * not expressible as a patch (a patch edits one `.val.ts` and cannot create the
14617
+ * backing JSON file), so it writes both files directly — JSON first, so a
14618
+ * failure part-way through never leaves the module pointing at a file that does
14619
+ * not exist.
14620
+ */
14621
+ function extractJsonValuesEntry(moduleFilePath, rootDir, entryKey, content, sourceFileHandler) {
14622
+ const valTsPath = sourceFileHandler.resolveSourceModulePath(getSyntheticContainingPath(rootDir), `.${moduleFilePath.replace(".val.ts", ".val").replace(".val.js", ".val").replace(".val.jsx", ".val").replace(".val.tsx", ".val")}`);
14623
+ const valTsText = sourceFileHandler.host.readFile(valTsPath);
14624
+ if (valTsText === undefined) {
14625
+ throw Error(`Source file ${valTsPath} not found`);
14626
+ }
14627
+ const planned = planJsonValuesEntryExtraction({
14628
+ moduleFilePath,
14629
+ entryKey,
14630
+ content,
14631
+ valTsPath,
14632
+ valTsText
14633
+ });
14634
+ if (fp.result.isErr(planned)) {
14635
+ throw Error(planned.error);
14346
14636
  }
14347
- sourceFileHandler.writeFile(absoluteJsonPath, JSON.stringify(content, null, 2) + "\n", "utf8");
14348
- sourceFileHandler.writeSourceFile(inserted.value);
14637
+ const absoluteJsonPath = path__namespace["default"].join(rootDir, planned.value.jsonPath);
14638
+ if (sourceFileHandler.host.fileExists(absoluteJsonPath)) {
14639
+ throw Error(`Cannot extract .jsonValues() entry '${entryKey}' of ${moduleFilePath}: '${planned.value.jsonPath}' already exists`);
14640
+ }
14641
+ sourceFileHandler.writeFile(absoluteJsonPath, planned.value.jsonContent, "utf8");
14642
+ sourceFileHandler.writeFile(valTsPath, planned.value.valTsContent, "utf8");
14643
+ }
14644
+
14645
+ /**
14646
+ * The text of a source file the TS ops produced, exactly as
14647
+ * `ValSourceFileHandler.writeSourceFile` would have written it.
14648
+ *
14649
+ * The unescape undoes the printer's `\uXXXX` output for non-ASCII. Today the
14650
+ * ops splice printed text into `document.text` and this fix only ever prints an
14651
+ * ASCII `c.json(() => import("./..."))`, so nothing here is escaped and the
14652
+ * unescape is a no-op — but `neverAsciiEscape` is off in the printer, so that is
14653
+ * a property of what this fix happens to print, not a guarantee. Kept because
14654
+ * this replaced a `writeSourceFile` call and the text written must not change.
14655
+ *
14656
+ * https://github.com/microsoft/TypeScript/issues/36174
14657
+ */
14658
+ function printSourceFile(sourceFile) {
14659
+ return unescape(sourceFile.text.replace(/\\u/g, "%u"));
14349
14660
  }
14350
14661
  function formatOpsError(error, sourceFile) {
14351
14662
  if (error instanceof patch.PatchError) {
@@ -14613,7 +14924,7 @@ async function handleRemoteFileUpload(ctx) {
14613
14924
  return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
14614
14925
  }
14615
14926
 
14616
- // Gallery (s.images({ remote: true }) / s.files({ remote: true })) upload.
14927
+ // Gallery (s.images({ ... }).remote() / s.files({ ... }).remote()) upload.
14617
14928
  // Unlike a single image/file field, a gallery entry is keyed by its local file
14618
14929
  // path and the value is bare metadata (no FileSource), so we derive the file
14619
14930
  // ref from the key and synthesize the image/file schema from the record's
@@ -15655,6 +15966,7 @@ exports.ok = ok;
15655
15966
  exports.parsePersonalAccessTokenFile = parsePersonalAccessTokenFile;
15656
15967
  exports.patchSourceFile = patchSourceFile;
15657
15968
  exports.persistPersonalAccessToken = persistPersonalAccessToken;
15969
+ exports.planJsonValuesEntryExtraction = planJsonValuesEntryExtraction;
15658
15970
  exports.readCapturedReport = readCapturedReport;
15659
15971
  exports.readPatchStore = readPatchStore;
15660
15972
  exports.rebaseContentOp = rebaseContentOp;