@valbuild/server 0.72.3 → 0.73.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.
@@ -1,7 +1,7 @@
1
1
  import { newQuickJSWASMModule } from 'quickjs-emscripten';
2
2
  import ts from 'typescript';
3
3
  import { result, pipe } from '@valbuild/core/fp';
4
- import { FILE_REF_PROP, FILE_REF_SUBTYPE_TAG, derefPatch, Internal, Schema, ImageSchema, RichTextSchema, FileSchema, VAL_EXTENSION } from '@valbuild/core';
4
+ import { FILE_REF_PROP, FILE_REF_SUBTYPE_TAG, VAL_EXTENSION, derefPatch, Internal, Schema, ImageSchema, RichTextSchema, FileSchema, DEFAULT_VAL_REMOTE_HOST } from '@valbuild/core';
5
5
  import { deepEqual, isNotRoot, PatchError, parseAndValidateArrayIndex, applyPatch, JSONOps, deepClone, sourceToPatchPath } from '@valbuild/core/patch';
6
6
  import * as fsPath from 'path';
7
7
  import fsPath__default from 'path';
@@ -14,6 +14,8 @@ import crypto$1 from 'crypto';
14
14
  import { z } from 'zod';
15
15
  import sizeOf from 'image-size';
16
16
  import { fromError, fromZodError } from 'zod-validation-error';
17
+ import http from 'http';
18
+ import https from 'https';
17
19
 
18
20
  class ValSyntaxError {
19
21
  constructor(message, node) {
@@ -65,7 +67,7 @@ function validateObjectProperties(nodes) {
65
67
  * validating its children.
66
68
  */
67
69
  function shallowValidateExpression(value) {
68
- return ts.isStringLiteralLike(value) || ts.isNumericLiteral(value) || value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword || ts.isArrayLiteralExpression(value) || ts.isObjectLiteralExpression(value) || isValFileMethodCall(value) || isValImageMethodCall(value) ? undefined : new ValSyntaxError("Expression must be a literal or call c.file", value);
70
+ return ts.isStringLiteralLike(value) || ts.isNumericLiteral(value) || value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword || ts.isArrayLiteralExpression(value) || ts.isObjectLiteralExpression(value) || isValFileMethodCall(value) || isValImageMethodCall(value) || isValRemoteMethodCall(value) ? undefined : new ValSyntaxError("Expression must be a literal or call c.file / c.image / c.remote", value);
69
71
  }
70
72
 
71
73
  /**
@@ -123,8 +125,23 @@ function evaluateExpression(value) {
123
125
  });
124
126
  }
125
127
  }));
128
+ } else if (isValRemoteMethodCall(value)) {
129
+ return pipe(findValRemoteNodeArg(value), result.flatMap(ref => {
130
+ if (value.arguments.length === 2) {
131
+ return pipe(evaluateExpression(value.arguments[1]), result.map(metadata => ({
132
+ [FILE_REF_PROP]: ref.text,
133
+ _type: "remote",
134
+ metadata
135
+ })));
136
+ } else {
137
+ return result.ok({
138
+ [FILE_REF_PROP]: ref.text,
139
+ _type: "remote"
140
+ });
141
+ }
142
+ }));
126
143
  } else {
127
- return result.err(new ValSyntaxError("Expression must be a literal or call c.file", value));
144
+ return result.err(new ValSyntaxError("Expression must be a literal or call c.file / c.image / c.remote", value));
128
145
  }
129
146
  }
130
147
  function findObjectPropertyAssignment(value, key) {
@@ -144,6 +161,9 @@ function isValFileMethodCall(node) {
144
161
  function isValImageMethodCall(node) {
145
162
  return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "image";
146
163
  }
164
+ function isValRemoteMethodCall(node) {
165
+ return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "remote";
166
+ }
147
167
  function findValFileNodeArg(node) {
148
168
  if (node.arguments.length === 0) {
149
169
  return result.err(new ValSyntaxError(`Invalid c.file() call: missing ref argument`, node));
@@ -155,6 +175,17 @@ function findValFileNodeArg(node) {
155
175
  const refNode = node.arguments[0];
156
176
  return result.ok(refNode);
157
177
  }
178
+ function findValRemoteNodeArg(node) {
179
+ if (node.arguments.length === 0) {
180
+ return result.err(new ValSyntaxError(`Invalid c.remote() call: missing ref argument`, node));
181
+ } else if (node.arguments.length > 2) {
182
+ return result.err(new ValSyntaxError(`Invalid c.remote() call: too many arguments ${node.arguments.length}}`, node));
183
+ } else if (!ts.isStringLiteral(node.arguments[0])) {
184
+ return result.err(new ValSyntaxError(`Invalid c.remote() call: ref must be a string literal`, node));
185
+ }
186
+ const refNode = node.arguments[0];
187
+ return result.ok(refNode);
188
+ }
158
189
  function findValImageNodeArg(node) {
159
190
  if (node.arguments.length === 0) {
160
191
  return result.err(new ValSyntaxError(`Invalid c.image() call: missing ref argument`, node));
@@ -192,6 +223,19 @@ function findValImageMetadataArg(node) {
192
223
  }
193
224
  return result.ok(undefined);
194
225
  }
226
+ function findValRemoteMetadataArg(node) {
227
+ if (node.arguments.length === 0) {
228
+ return result.err(new ValSyntaxError(`Invalid c.remote() call: missing ref argument`, node));
229
+ } else if (node.arguments.length > 2) {
230
+ return result.err(new ValSyntaxError(`Invalid c.remote() call: too many arguments ${node.arguments.length}}`, node));
231
+ } else if (node.arguments.length === 2) {
232
+ if (!ts.isObjectLiteralExpression(node.arguments[1])) {
233
+ return result.err(new ValSyntaxError(`Invalid c.remote() call: metadata must be a object literal`, node));
234
+ }
235
+ return result.ok(node.arguments[1]);
236
+ }
237
+ return result.ok(undefined);
238
+ }
195
239
 
196
240
  /**
197
241
  * Given a list of expressions, validates that all the expressions are not
@@ -293,6 +337,13 @@ function createValImageReference(value) {
293
337
  }
294
338
  return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("c"), ts.factory.createIdentifier("image")), undefined, args);
295
339
  }
340
+ function createValRemoteReference(value) {
341
+ const args = [ts.factory.createStringLiteral(value[FILE_REF_PROP])];
342
+ if (value.metadata) {
343
+ args.push(toExpression(value.metadata));
344
+ }
345
+ return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("c"), ts.factory.createIdentifier("remote")), undefined, args);
346
+ }
296
347
  function toExpression(value) {
297
348
  if (typeof value === "string") {
298
349
  // TODO: Use configuration/heuristics to determine use of single quote or double quote
@@ -325,6 +376,9 @@ function toExpression(value) {
325
376
  if (isValImageValue(value)) {
326
377
  return createValImageReference(value);
327
378
  }
379
+ if (isValRemoteValue(value)) {
380
+ return createValRemoteReference(value);
381
+ }
328
382
  return ts.factory.createObjectLiteralExpression(Object.entries(value).map(([key, value]) => createPropertyAssignment(key, value)));
329
383
  } else {
330
384
  return ts.factory.createStringLiteral(value);
@@ -496,6 +550,25 @@ function replaceInNode(document, node, key, value) {
496
550
  ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
497
551
  }));
498
552
  }
553
+ } else if (isValRemoteMethodCall(node)) {
554
+ if (key === FILE_REF_PROP) {
555
+ if (typeof value !== "string") {
556
+ return result.err(new PatchError("Cannot replace c.image reference with non-string value"));
557
+ }
558
+ return pipe(findValRemoteNodeArg(node), result.map(refNode => replaceNodeValue(document, refNode, value)));
559
+ } else {
560
+ return pipe(findValRemoteMetadataArg(node), result.flatMap(metadataArgNode => {
561
+ if (!metadataArgNode) {
562
+ return result.err(new PatchError("Cannot replace in c.remote metadata when it does not exist"));
563
+ }
564
+ if (key !== "metadata") {
565
+ return result.err(new PatchError(`Cannot replace c.remote metadata key ${key} when it does not exist`));
566
+ }
567
+ return replaceInNode(document,
568
+ // TODO: creating a fake object here might not be right - seems to work though
569
+ ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
570
+ }));
571
+ }
499
572
  } else {
500
573
  return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot replace in non-object/array"));
501
574
  }
@@ -522,6 +595,11 @@ function getFromNode(node, key) {
522
595
  return findValImageNodeArg(node);
523
596
  }
524
597
  return findValImageMetadataArg(node);
598
+ } else if (isValRemoteMethodCall(node)) {
599
+ if (key === FILE_REF_PROP) {
600
+ return findValRemoteNodeArg(node);
601
+ }
602
+ return findValRemoteMetadataArg(node);
525
603
  } else {
526
604
  return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot access non-object/array"));
527
605
  }
@@ -567,7 +645,7 @@ function removeFromNode(document, node, key) {
567
645
  }
568
646
  } else if (isValImageMethodCall(node)) {
569
647
  if (key === FILE_REF_PROP) {
570
- return result.err(new PatchError("Cannot remove a ref from c.file"));
648
+ return result.err(new PatchError("Cannot remove a ref from c.image"));
571
649
  } else {
572
650
  return pipe(findValImageMetadataArg(node), result.flatMap(metadataArgNode => {
573
651
  if (!metadataArgNode) {
@@ -576,6 +654,17 @@ function removeFromNode(document, node, key) {
576
654
  return removeFromNode(document, metadataArgNode, key);
577
655
  }));
578
656
  }
657
+ } else if (isValRemoteMethodCall(node)) {
658
+ if (key === FILE_REF_PROP) {
659
+ return result.err(new PatchError("Cannot remove a ref from c.remote"));
660
+ } else {
661
+ return pipe(findValRemoteMetadataArg(node), result.flatMap(metadataArgNode => {
662
+ if (!metadataArgNode) {
663
+ return result.err(new PatchError("Cannot remove from c.remote metadata when it does not exist"));
664
+ }
665
+ return removeFromNode(document, metadataArgNode, key);
666
+ }));
667
+ }
579
668
  } else {
580
669
  return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot remove from non-object/array"));
581
670
  }
@@ -584,18 +673,13 @@ function removeAtPath(document, rootNode, path) {
584
673
  return pipe(getPointerFromPath(rootNode, path), result.flatMap(([node, key]) => removeFromNode(document, node, key)));
585
674
  }
586
675
  function isValFileValue(value) {
587
- return !!(typeof value === "object" && value &&
588
- // TODO: replace the below with this:
589
- // VAL_EXTENSION in value &&
590
- // value[VAL_EXTENSION] === "file" &&
591
- FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string" && value[FILE_REF_SUBTYPE_TAG] !== "image");
676
+ return !!(typeof value === "object" && value && VAL_EXTENSION in value && value[VAL_EXTENSION] === "file" && FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string" && value[FILE_REF_SUBTYPE_TAG] !== "image");
592
677
  }
593
678
  function isValImageValue(value) {
594
- return !!(typeof value === "object" && value &&
595
- // TODO: replace the below with this:
596
- // VAL_EXTENSION in value &&
597
- // value[VAL_EXTENSION] === "file" &&
598
- FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string" && value[FILE_REF_SUBTYPE_TAG] === "image");
679
+ return !!(typeof value === "object" && value && VAL_EXTENSION in value && value[VAL_EXTENSION] === "file" && FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string" && value[FILE_REF_SUBTYPE_TAG] === "image");
680
+ }
681
+ function isValRemoteValue(value) {
682
+ return !!(typeof value === "object" && value && VAL_EXTENSION in value && value[VAL_EXTENSION] === "remote" && FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string");
599
683
  }
600
684
  function addToNode(document, node, key, value) {
601
685
  if (ts.isArrayLiteralExpression(node)) {
@@ -645,6 +729,23 @@ function addToNode(document, node, key, value) {
645
729
  return result.ok([insertAt(document, node.arguments, node.arguments.length, toExpression(value))]);
646
730
  }));
647
731
  }
732
+ } else if (isValRemoteMethodCall(node)) {
733
+ if (key === FILE_REF_PROP) {
734
+ if (typeof value !== "string") {
735
+ return result.err(new PatchError(`Cannot add ${FILE_REF_PROP} key to c.remote with non-string value`));
736
+ }
737
+ return pipe(findValRemoteNodeArg(node), result.map(arg => replaceNodeValue(document, arg, value)));
738
+ } else {
739
+ return pipe(findValRemoteMetadataArg(node), result.flatMap(metadataArgNode => {
740
+ if (metadataArgNode) {
741
+ return result.err(new PatchError("Cannot add metadata to c.remote when it already exists"));
742
+ }
743
+ if (key !== "metadata") {
744
+ return result.err(new PatchError(`Cannot add ${key} key to c.remote: only metadata is allowed`));
745
+ }
746
+ return result.ok([insertAt(document, node.arguments, node.arguments.length, toExpression(value))]);
747
+ }));
748
+ }
648
749
  } else {
649
750
  return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot add to non-object/array"));
650
751
  }
@@ -893,7 +994,7 @@ class ValSourceFileHandler {
893
994
  fs.mkdirSync(fsPath__default.dirname(fileName), {
894
995
  recursive: true
895
996
  });
896
- fs.writeFileSync(fileName, data, encoding);
997
+ fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
897
998
  },
898
999
  rmFile: fs.rmSync
899
1000
  }) {
@@ -943,7 +1044,7 @@ class ValModuleLoader {
943
1044
  fs.mkdirSync(fsPath__default.dirname(fileName), {
944
1045
  recursive: true
945
1046
  });
946
- fs.writeFileSync(fileName, data, encoding);
1047
+ fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
947
1048
  },
948
1049
  rmFile: fs.rmSync
949
1050
  }, disableCache = false) {
@@ -1235,7 +1336,7 @@ async function createService(projectRoot, opts, host = {
1235
1336
  fs.mkdirSync(fsPath__default.dirname(fileName), {
1236
1337
  recursive: true
1237
1338
  });
1238
- fs.writeFileSync(fileName, data, encoding);
1339
+ fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1239
1340
  },
1240
1341
  rmFile: fs.rmSync
1241
1342
  }, loader) {
@@ -1342,7 +1443,7 @@ function encodeJwt(payload, sessionKey) {
1342
1443
  }
1343
1444
 
1344
1445
  /* eslint-disable @typescript-eslint/no-unused-vars */
1345
- const textEncoder$1 = new TextEncoder();
1446
+ const textEncoder$3 = new TextEncoder();
1346
1447
  const jsonOps = new JSONOps();
1347
1448
  const tsOps = new TSOps(document => {
1348
1449
  return pipe(analyzeValModule(document), result.map(({
@@ -1372,12 +1473,12 @@ class ValOps {
1372
1473
  if (typeof input === "object") {
1373
1474
  return this.hashObject(input);
1374
1475
  }
1375
- return Internal.getSHA256Hash(textEncoder$1.encode(input));
1476
+ return Internal.getSHA256Hash(textEncoder$3.encode(input));
1376
1477
  }
1377
1478
  hashObject(obj) {
1378
1479
  const collector = [];
1379
1480
  this.collectObjectRecursive(obj, collector);
1380
- return Internal.getSHA256Hash(textEncoder$1.encode(collector.join("")));
1481
+ return Internal.getSHA256Hash(textEncoder$3.encode(collector.join("")));
1381
1482
  }
1382
1483
  collectObjectRecursive(item, collector) {
1383
1484
  if (typeof item === "string") {
@@ -1546,7 +1647,10 @@ class ValOps {
1546
1647
  for (const op of patch.patch) {
1547
1648
  if (op.op === "file") {
1548
1649
  const filePath = op.filePath;
1549
- fileLastUpdatedByPatchId[filePath] = patch.patchId;
1650
+ fileLastUpdatedByPatchId[filePath] = {
1651
+ patchId: patch.patchId,
1652
+ remote: op.remote
1653
+ };
1550
1654
  }
1551
1655
  const path = patch.path;
1552
1656
  if (!patchesByModule[path]) {
@@ -1682,6 +1786,7 @@ class ValOps {
1682
1786
  };
1683
1787
  const errors = {};
1684
1788
  const files = {};
1789
+ const remoteFiles = {};
1685
1790
  const entries = Object.entries(schemas);
1686
1791
  const modulePathsToValidate = patchesByModule && Object.keys(patchesByModule);
1687
1792
  for (const [pathS, schema] of entries) {
@@ -1723,7 +1828,7 @@ class ValOps {
1723
1828
  };
1724
1829
  if (validationErrors) {
1725
1830
  for (const validationError of validationErrors) {
1726
- var _validationError$fixe;
1831
+ var _validationError$fixe, _validationError$fixe2, _validationError$fixe3;
1727
1832
  if (isOnlyFileCheckValidationError(validationError)) {
1728
1833
  if (files[sourcePath]) {
1729
1834
  throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
@@ -1732,7 +1837,9 @@ class ValOps {
1732
1837
  if (isFileSource(value)) {
1733
1838
  files[sourcePath] = value;
1734
1839
  }
1735
- } else if ((_validationError$fixe = validationError.fixes) !== null && _validationError$fixe !== void 0 && _validationError$fixe.includes("keyof:check-keys")) {
1840
+ } else if ((_validationError$fixe = validationError.fixes) !== null && _validationError$fixe !== void 0 && _validationError$fixe.includes("image:check-remote") || (_validationError$fixe2 = validationError.fixes) !== null && _validationError$fixe2 !== void 0 && _validationError$fixe2.includes("file:check-remote")) {
1841
+ remoteFiles[sourcePath] = validationError.value;
1842
+ } else if ((_validationError$fixe3 = validationError.fixes) !== null && _validationError$fixe3 !== void 0 && _validationError$fixe3.includes("keyof:check-keys")) {
1736
1843
  const TYPE_ERROR_MESSAGE = `This is most likely a Val version mismatch or Val bug.`;
1737
1844
  if (!validationError.value) {
1738
1845
  addError({
@@ -1781,9 +1888,14 @@ class ValOps {
1781
1888
  }
1782
1889
  return {
1783
1890
  errors,
1784
- files
1891
+ files,
1892
+ remoteFiles
1785
1893
  };
1786
1894
  }
1895
+ async validateRemoteFiles(schemas, sources, remoteFiles) {
1896
+ // TODO: Implement
1897
+ return {};
1898
+ }
1787
1899
 
1788
1900
  // #region validateFiles
1789
1901
  async validateFiles(schemas, sources, files, fileLastUpdatedByPatchId) {
@@ -1831,13 +1943,13 @@ class ValOps {
1831
1943
  }
1832
1944
  const type = schemaAtPath instanceof ImageSchema ? "image" : "file";
1833
1945
  const filePath = value[FILE_REF_PROP];
1834
- const patchId = (fileLastUpdatedByPatchId === null || fileLastUpdatedByPatchId === void 0 ? void 0 : fileLastUpdatedByPatchId[filePath]) || null;
1946
+ const fileData = (fileLastUpdatedByPatchId === null || fileLastUpdatedByPatchId === void 0 ? void 0 : fileLastUpdatedByPatchId[filePath]) || null;
1835
1947
  let metadata;
1836
1948
  let metadataErrors;
1837
1949
 
1838
1950
  // TODO: refactor so we call get metadata once instead of iterating like this. Reason: should be a lot faster
1839
- if (patchId) {
1840
- const patchFileMetadata = await this.getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId);
1951
+ if (fileData) {
1952
+ const patchFileMetadata = await this.getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, fileData.patchId, fileData.remote);
1841
1953
  if (patchFileMetadata.errors) {
1842
1954
  metadataErrors = patchFileMetadata.errors;
1843
1955
  } else {
@@ -1857,7 +1969,7 @@ class ValOps {
1857
1969
  message: e.message,
1858
1970
  value: {
1859
1971
  filePath,
1860
- patchId
1972
+ patchId: (fileData === null || fileData === void 0 ? void 0 : fileData.patchId) ?? null
1861
1973
  }
1862
1974
  }))
1863
1975
  };
@@ -2044,13 +2156,18 @@ class ValOps {
2044
2156
  }
2045
2157
  const patchedBinaryFilesDescriptors = {};
2046
2158
  const binaryFilePatchErrors = {};
2047
- await Promise.all(Object.entries(fileLastUpdatedByPatchId).map(async ([filePath, patchId]) => {
2159
+ await Promise.all(Object.entries(fileLastUpdatedByPatchId).map(async ([filePath, patchData]) => {
2160
+ const {
2161
+ patchId,
2162
+ remote
2163
+ } = patchData;
2048
2164
  if (globalAppliedPatches.includes(patchId)) {
2049
2165
  // TODO: do we want to make sure the file is there? Then again, it should be rare that it happens (unless there's a Val bug) so it might be enough to fail later (at commit)
2050
2166
  // TODO: include sha256? This way we can make sure we pick the right file since theoretically there could be multiple files with the same path in the same patch
2051
2167
  // or is that the case? We are picking the latest file by path so, that should be enough?
2052
2168
  patchedBinaryFilesDescriptors[filePath] = {
2053
- patchId
2169
+ patchId,
2170
+ remote
2054
2171
  };
2055
2172
  } else {
2056
2173
  hasErrors = true;
@@ -2145,18 +2262,20 @@ class ValOps {
2145
2262
  error: new PatchError("Value is not a string")
2146
2263
  };
2147
2264
  } else {
2148
- const sha256 = Internal.getSHA256Hash(textEncoder$1.encode(value));
2265
+ const sha256 = Internal.getSHA256Hash(textEncoder$3.encode(value));
2149
2266
  files[filePath] = {
2150
2267
  value,
2151
2268
  sha256,
2152
- path: op.path
2269
+ path: op.path,
2270
+ remote: op.remote
2153
2271
  };
2154
2272
  sourceFileOps.push({
2155
2273
  op: "file",
2156
2274
  path: op.path,
2157
2275
  filePath,
2158
2276
  nestedFilePath: op.nestedFilePath,
2159
- value: sha256
2277
+ value: sha256,
2278
+ remote: op.remote
2160
2279
  });
2161
2280
  }
2162
2281
  }
@@ -2239,7 +2358,7 @@ class ValOps {
2239
2358
  const MaxRetries = 3;
2240
2359
  let lastRes;
2241
2360
  for (let i = 0; i < MaxRetries; i++) {
2242
- lastRes = await this.saveBase64EncodedBinaryFileFromPatch(filePath, parentRef, patchId, data.value, type, metadataOps.metadata);
2361
+ lastRes = await this.saveBase64EncodedBinaryFileFromPatch(filePath, parentRef, patchId, data.value, type, metadataOps.metadata, data.remote);
2243
2362
  if (!lastRes.error) {
2244
2363
  return {
2245
2364
  filePath
@@ -2271,8 +2390,8 @@ class ValOps {
2271
2390
  // #region abstract ops
2272
2391
  }
2273
2392
  function isOnlyFileCheckValidationError(validationError) {
2274
- var _validationError$fixe2;
2275
- if ((_validationError$fixe2 = validationError.fixes) !== null && _validationError$fixe2 !== void 0 && _validationError$fixe2.every(f => f === "file:check-metadata" || f === "image:check-metadata")) {
2393
+ var _validationError$fixe4;
2394
+ if ((_validationError$fixe4 = validationError.fixes) !== null && _validationError$fixe4 !== void 0 && _validationError$fixe4.every(f => f === "file:check-metadata" || f === "image:check-metadata")) {
2276
2395
  return true;
2277
2396
  }
2278
2397
  return false;
@@ -2299,7 +2418,7 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
2299
2418
  width,
2300
2419
  height,
2301
2420
  type
2302
- } = sizeOf(buffer);
2421
+ } = sizeOf(new Uint8Array(buffer));
2303
2422
  const normalizedType = type === "jpg" ? "jpeg" : type === "svg" ? "svg+xml" : type;
2304
2423
  if (type !== undefined && `image/${normalizedType}` !== mimeType) {
2305
2424
  return {
@@ -2408,6 +2527,74 @@ function computeChangedPatchParentRefs(currentPatches, deletePatchIds) {
2408
2527
  };
2409
2528
  }
2410
2529
 
2530
+ function getFileExt(filePath) {
2531
+ // NOTE: We do not import the path module. This code is copied in different projects. We want the same implementation and which means that this might running in browser where path is not available).
2532
+ return filePath.split(".").pop() || "";
2533
+ }
2534
+
2535
+ const textEncoder$2 = new TextEncoder();
2536
+ async function uploadRemoteFile(remoteHost, fileBuffer, publicProjectId, bucket, filePath, schema, metadata, auth) {
2537
+ const fileHash = Internal.remote.getFileHash(fileBuffer);
2538
+ const coreVersion = Internal.VERSION.core || "unknown";
2539
+ const fileExt = getFileExt(filePath);
2540
+ const ref = Internal.remote.createRemoteRef(remoteHost, {
2541
+ publicProjectId,
2542
+ coreVersion,
2543
+ bucket,
2544
+ validationHash: Internal.remote.getValidationHash(coreVersion, schema, fileExt, metadata, fileHash, textEncoder$2),
2545
+ fileHash,
2546
+ filePath
2547
+ });
2548
+ return uploadRemoteRef(fileBuffer, ref, auth);
2549
+ }
2550
+ async function uploadRemoteRef(fileBuffer, ref, auth) {
2551
+ const authHeader = "apiKey" in auth ? {
2552
+ Authorization: `Bearer ${auth.apiKey}`
2553
+ } : {
2554
+ "x-val-pat": auth.pat
2555
+ };
2556
+ const res = await fetch(ref, {
2557
+ method: "PUT",
2558
+ headers: {
2559
+ ...authHeader,
2560
+ "Content-Type": "application/octet-stream"
2561
+ },
2562
+ body: fileBuffer
2563
+ });
2564
+ if (!res.ok) {
2565
+ var _res$headers$get;
2566
+ if (res.status === 409) {
2567
+ // File already exists
2568
+ return {
2569
+ success: true,
2570
+ ref
2571
+ };
2572
+ }
2573
+ if ((_res$headers$get = res.headers.get("content-type")) !== null && _res$headers$get !== void 0 && _res$headers$get.includes("application/json")) {
2574
+ const json = await res.json();
2575
+ if (json.message) {
2576
+ return {
2577
+ success: false,
2578
+ error: `${ref}. Failed to upload file: ${json.message}.`
2579
+ };
2580
+ } else {
2581
+ return {
2582
+ success: false,
2583
+ error: `${ref}. Failed to upload file: ${JSON.stringify(json)}.`
2584
+ };
2585
+ }
2586
+ }
2587
+ return {
2588
+ success: false,
2589
+ error: `${ref}. Failed to upload file: ${await res.text()}.`
2590
+ };
2591
+ }
2592
+ return {
2593
+ success: true,
2594
+ ref
2595
+ };
2596
+ }
2597
+
2411
2598
  class ValOpsFS extends ValOps {
2412
2599
  static VAL_DIR = ".val";
2413
2600
  constructor(rootDir, valModules, options) {
@@ -2849,10 +3036,10 @@ class ValOpsFS extends ValOps {
2849
3036
  };
2850
3037
  }
2851
3038
  }
2852
- async saveBase64EncodedBinaryFileFromPatch(filePath, parentRef, patchId, data, _type, metadata) {
3039
+ async saveBase64EncodedBinaryFileFromPatch(filePath, parentRef, patchId, data, _type, metadata, remote) {
2853
3040
  const patchDir = this.getParentPatchIdFromParentRef(parentRef);
2854
- const patchFilePath = this.getBinaryFilePath(filePath, patchDir);
2855
- const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchDir);
3041
+ const patchFilePath = this.getBinaryFilePath(filePath, patchDir, remote);
3042
+ const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchDir, remote);
2856
3043
  try {
2857
3044
  const buffer = bufferFromDataUrl(data);
2858
3045
  if (!buffer) {
@@ -2883,7 +3070,7 @@ class ValOpsFS extends ValOps {
2883
3070
  };
2884
3071
  }
2885
3072
  }
2886
- async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
3073
+ async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId, remote) {
2887
3074
  const patchDirRes = await this.getParentPatchIdFromPatchId(patchId);
2888
3075
  if (result.isErr(patchDirRes)) {
2889
3076
  return {
@@ -2892,7 +3079,7 @@ class ValOpsFS extends ValOps {
2892
3079
  }]
2893
3080
  };
2894
3081
  }
2895
- const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchDirRes.value);
3082
+ const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchDirRes.value, remote);
2896
3083
  if (!this.host.fileExists(metadataFilePath)) {
2897
3084
  return {
2898
3085
  errors: [{
@@ -2932,7 +3119,10 @@ class ValOpsFS extends ValOps {
2932
3119
  if (!result.isOk(patchDirRes)) {
2933
3120
  return null;
2934
3121
  }
2935
- const absPath = this.getBinaryFilePath(filePath, patchDirRes.value);
3122
+ const absPath = this.getBinaryFilePath(filePath, patchDirRes.value,
3123
+ // We save remote remote files using the filepath (so not the remote reference) and we also retrieve them using the filepath. Therefore remote is always false
3124
+ false // remote = false
3125
+ );
2936
3126
  if (!this.host.fileExists(absPath)) {
2937
3127
  return null;
2938
3128
  }
@@ -2996,43 +3186,90 @@ class ValOpsFS extends ValOps {
2996
3186
  }
2997
3187
  }
2998
3188
  }
2999
- async saveFiles(preparedCommit) {
3189
+ async saveOrUploadFiles(preparedCommit, auth, testMode) {
3000
3190
  const updatedFiles = [];
3191
+ const uploadedRemoteRefs = [];
3001
3192
  const errors = {};
3002
- for (const [filePath, data] of Object.entries(preparedCommit.patchedSourceFiles)) {
3003
- const absPath = fsPath__default.join(this.rootDir, ...filePath.split("/"));
3004
- try {
3005
- this.host.writeUf8File(absPath, data);
3006
- updatedFiles.push(absPath);
3007
- } catch (err) {
3008
- errors[absPath] = {
3009
- message: err instanceof Error ? err.message : "Unknown error",
3010
- filePath
3193
+ const remoteFileDescriptors = Object.entries(preparedCommit.patchedBinaryFilesDescriptors).filter(([, {
3194
+ remote
3195
+ }]) => remote).map(([ref, {
3196
+ patchId
3197
+ }]) => [ref, {
3198
+ patchId
3199
+ }]);
3200
+ const localFileDescriptors = Object.entries(preparedCommit.patchedBinaryFilesDescriptors).filter(([, {
3201
+ remote
3202
+ }]) => !remote).map(([ref, {
3203
+ patchId
3204
+ }]) => [ref, {
3205
+ patchId
3206
+ }]);
3207
+ for (const [ref, {
3208
+ patchId
3209
+ }] of remoteFileDescriptors) {
3210
+ const splitRemoteRefRes = Internal.remote.splitRemoteRef(ref);
3211
+ if (splitRemoteRefRes.status === "error") {
3212
+ errors[ref] = {
3213
+ message: "Failed to split remote ref: " + ref
3214
+ };
3215
+ continue;
3216
+ }
3217
+ const fileBuffer = await this.getBase64EncodedBinaryFileFromPatch(splitRemoteRefRes.filePath, patchId);
3218
+ if (!fileBuffer) {
3219
+ errors[ref] = {
3220
+ message: "Failed to get binary file from patch. Ref: " + ref + ". PatchId: " + patchId
3011
3221
  };
3222
+ continue;
3223
+ }
3224
+ if (testMode === "test-skip-remote") {
3225
+ console.log("Skip remote flag enabled. Skipping file upload", ref);
3226
+ continue;
3012
3227
  }
3228
+ console.log("Uploading remote file", ref);
3229
+ const res = await uploadRemoteRef(fileBuffer, ref, auth);
3230
+ if (!res.success) {
3231
+ console.error("Failed to upload remote file", ref, res.error);
3232
+ throw new Error(`Failed to upload remote file: ${ref}. ${res.error}`);
3233
+ }
3234
+ console.log("Completed remote file", ref);
3235
+ uploadedRemoteRefs.push(ref);
3013
3236
  }
3014
3237
  const patchIdToPatchDirMapRes = await this.getParentPatchIdFromPatchIdMap();
3015
3238
  if (result.isErr(patchIdToPatchDirMapRes)) {
3016
3239
  return {
3017
3240
  updatedFiles,
3241
+ uploadedRemoteRefs,
3018
3242
  errors
3019
3243
  };
3020
3244
  }
3021
3245
  const patchIdToPatchDirMap = patchIdToPatchDirMapRes.value;
3022
- for (const [filePath, {
3246
+ for (const [ref, {
3023
3247
  patchId
3024
- }] of Object.entries(preparedCommit.patchedBinaryFilesDescriptors)) {
3248
+ }] of localFileDescriptors) {
3249
+ const filePath = ref;
3025
3250
  const absPath = fsPath__default.join(this.rootDir, ...filePath.split("/"));
3026
3251
  try {
3027
3252
  const patchDir = patchIdToPatchDirMap[patchId];
3028
3253
  if (!patchDir) {
3029
3254
  errors[absPath] = {
3030
3255
  message: "Failed to find PatchDir for PatchId " + patchId,
3031
- filePath
3256
+ filePath: filePath
3032
3257
  };
3033
3258
  continue;
3034
3259
  }
3035
- this.host.copyFile(this.getBinaryFilePath(filePath, patchDir), absPath);
3260
+ this.host.copyFile(this.getBinaryFilePath(filePath, patchDir, false), absPath);
3261
+ updatedFiles.push(absPath);
3262
+ } catch (err) {
3263
+ errors[absPath] = {
3264
+ message: err instanceof Error ? err.message : "Unknown error",
3265
+ filePath: filePath
3266
+ };
3267
+ }
3268
+ }
3269
+ for (const [filePath, data] of Object.entries(preparedCommit.patchedSourceFiles)) {
3270
+ const absPath = fsPath__default.join(this.rootDir, ...filePath.split("/"));
3271
+ try {
3272
+ this.host.writeUf8File(absPath, data);
3036
3273
  updatedFiles.push(absPath);
3037
3274
  } catch (err) {
3038
3275
  errors[absPath] = {
@@ -3065,6 +3302,7 @@ class ValOpsFS extends ValOps {
3065
3302
  }
3066
3303
  return {
3067
3304
  updatedFiles,
3305
+ uploadedRemoteRefs,
3068
3306
  errors
3069
3307
  };
3070
3308
  }
@@ -3133,10 +3371,26 @@ class ValOpsFS extends ValOps {
3133
3371
  getFullPatchDir(patchDir) {
3134
3372
  return fsPath__default.join(this.getPatchesDir(), patchDir);
3135
3373
  }
3136
- getBinaryFilePath(filePath, patchDir) {
3374
+ getBinaryFilePath(filePath, patchDir, remote) {
3375
+ if (remote) {
3376
+ const res = Internal.remote.splitRemoteRef(filePath);
3377
+ if (res.status === "error") {
3378
+ throw new Error("Failed to split remote ref: " + filePath);
3379
+ }
3380
+ const actualFilePath = res.filePath;
3381
+ return fsPath__default.join(this.getFullPatchDir(patchDir), "files", actualFilePath, fsPath__default.basename(actualFilePath));
3382
+ }
3137
3383
  return fsPath__default.join(this.getFullPatchDir(patchDir), "files", filePath, fsPath__default.basename(filePath));
3138
3384
  }
3139
- getBinaryFileMetadataPath(filePath, patchDir) {
3385
+ getBinaryFileMetadataPath(filePath, patchDir, remote) {
3386
+ if (remote) {
3387
+ const res = Internal.remote.splitRemoteRef(filePath);
3388
+ if (res.status === "error") {
3389
+ throw new Error("Failed to split remote ref (in metadata path): " + filePath);
3390
+ }
3391
+ const actualFilePath = res.filePath;
3392
+ return fsPath__default.join(this.getFullPatchDir(patchDir), "files", actualFilePath, fsPath__default.basename(actualFilePath));
3393
+ }
3140
3394
  return fsPath__default.join(this.getFullPatchDir(patchDir), "files", filePath, "metadata.json");
3141
3395
  }
3142
3396
  getPatchFilePath(patchDir) {
@@ -3216,7 +3470,7 @@ class FSOpsHost {
3216
3470
  fs.mkdirSync(fsPath__default.dirname(path), {
3217
3471
  recursive: true
3218
3472
  });
3219
- fs.writeFileSync(path, data, "base64url");
3473
+ fs.writeFileSync(path, new Uint8Array(data), "base64url");
3220
3474
  }
3221
3475
  copyFile(from, to) {
3222
3476
  fs.mkdirSync(fsPath__default.dirname(to), {
@@ -3240,7 +3494,7 @@ const FSPatchBase = z.object({
3240
3494
  timestamp: z.string().datetime()
3241
3495
  });
3242
3496
 
3243
- const textEncoder = new TextEncoder();
3497
+ const textEncoder$1 = new TextEncoder();
3244
3498
  const PatchId = z.string().refine(s => !!s); // TODO: validate
3245
3499
  const CommitSha = z.string().refine(s => !!s); // TODO: validate
3246
3500
  z.string().refine(s => !!s); // TODO: validate
@@ -3287,7 +3541,8 @@ const FilesResponse = z.object({
3287
3541
  filePath: z.string(),
3288
3542
  location: z.literal("patch"),
3289
3543
  patchId: PatchId,
3290
- value: z.string()
3544
+ value: z.string(),
3545
+ remote: z.boolean()
3291
3546
  }), z.object({
3292
3547
  filePath: z.string(),
3293
3548
  location: z.literal("repo"),
@@ -3298,7 +3553,8 @@ const FilesResponse = z.object({
3298
3553
  filePath: z.string(),
3299
3554
  location: z.literal("patch"),
3300
3555
  patchId: PatchId,
3301
- message: z.string()
3556
+ message: z.string(),
3557
+ remote: z.boolean()
3302
3558
  }), z.object({
3303
3559
  filePath: z.string(),
3304
3560
  location: z.literal("repo"),
@@ -3734,7 +3990,23 @@ class ValOpsHttp extends ValOps {
3734
3990
  });
3735
3991
  });
3736
3992
  }
3737
- async saveBase64EncodedBinaryFileFromPatch(filePath, parentRef, patchId, data, type, metadata) {
3993
+ async saveBase64EncodedBinaryFileFromPatch(filePathOrRef, _parentRef,
3994
+ // this is required for the FS implementation, but not for the HTTP implementation (patchId is enough)
3995
+ patchId, data, type, metadata, remote) {
3996
+ let filePath;
3997
+ if (remote) {
3998
+ const splitRemoteRefDataRes = Internal.remote.splitRemoteRef(filePathOrRef);
3999
+ if (splitRemoteRefDataRes.status === "error") {
4000
+ return {
4001
+ error: {
4002
+ message: `Could not split remote ref: ${splitRemoteRefDataRes.error}`
4003
+ }
4004
+ };
4005
+ }
4006
+ filePath = "/" + splitRemoteRefDataRes.filePath;
4007
+ } else {
4008
+ filePath = filePathOrRef;
4009
+ }
3738
4010
  return fetch(`${this.contentUrl}/v1/${this.project}/patches/${patchId}/files`, {
3739
4011
  method: "POST",
3740
4012
  headers: {
@@ -3742,10 +4014,11 @@ class ValOpsHttp extends ValOps {
3742
4014
  "Content-Type": "application/json"
3743
4015
  },
3744
4016
  body: JSON.stringify({
3745
- filePath: filePath,
4017
+ filePath,
3746
4018
  data,
3747
4019
  type,
3748
- metadata
4020
+ metadata,
4021
+ remote
3749
4022
  })
3750
4023
  }).then(async res => {
3751
4024
  if (res.ok) {
@@ -3783,7 +4056,7 @@ class ValOpsHttp extends ValOps {
3783
4056
  });
3784
4057
  params.set("body_sha",
3785
4058
  // We use this for cache invalidation
3786
- Internal.getSHA256Hash(textEncoder.encode(stringifiedFiles)));
4059
+ Internal.getSHA256Hash(textEncoder$1.encode(stringifiedFiles)));
3787
4060
  return fetch(`${this.contentUrl}/v1/${this.project}/files?${params}`, {
3788
4061
  method: "PUT",
3789
4062
  // Yes, PUT is weird. Weirder to have a body in a GET request.
@@ -3843,39 +4116,53 @@ class ValOpsHttp extends ValOps {
3843
4116
  }
3844
4117
  async getBinaryFile(filePath) {
3845
4118
  // We could also just get this from public/ on the running server. Current approach feels more clean, but will be slower / puts more server load... We might want to change this
3846
- const filesRes = await this.getHttpFiles([{
4119
+ const requestFiles = [];
4120
+ requestFiles.push({
3847
4121
  filePath: filePath,
3848
4122
  location: "repo",
3849
4123
  root: this.root,
3850
4124
  commitSha: this.commitSha
3851
- }]);
4125
+ });
4126
+ const filesRes = await this.getHttpFiles(requestFiles);
3852
4127
  if (filesRes.error) {
3853
4128
  return null;
3854
4129
  }
3855
- const file = filesRes.files.find(f => f.filePath === filePath);
4130
+ const file = filesRes.files[0];
4131
+ if (filesRes.files.length > 1) {
4132
+ console.error("Expected 1 file, got more:", filesRes.files);
4133
+ }
3856
4134
  if (!file) {
3857
4135
  return null;
3858
4136
  }
3859
4137
  return Buffer.from(file.value, "base64");
3860
4138
  }
3861
- async getBase64EncodedBinaryFileFromPatch(filePath, patchId) {
4139
+ async getBase64EncodedBinaryFileFromPatch(filePath, patchId, remote) {
3862
4140
  const filesRes = await this.getHttpFiles([{
3863
4141
  filePath: filePath,
3864
4142
  location: "patch",
3865
- patchId
4143
+ patchId,
4144
+ remote
3866
4145
  }]);
3867
4146
  if (filesRes.error) {
4147
+ console.error("Error getting file:", filePath, filesRes.error);
3868
4148
  return null;
3869
4149
  }
3870
- const file = filesRes.files.find(f => f.filePath === filePath);
4150
+ if (filesRes.errors) {
4151
+ console.error("Failed while retrieving files", filePath, filesRes.errors);
4152
+ }
4153
+ const file = filesRes.files[0];
4154
+ if (filesRes.files.length > 1) {
4155
+ console.error("Expected 1 file, got more:", filesRes.files);
4156
+ }
3871
4157
  if (!file) {
3872
4158
  return null;
3873
4159
  }
3874
4160
  return bufferFromDataUrl(file.value) ?? null;
3875
4161
  }
3876
- async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
4162
+ async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId, remote) {
3877
4163
  const params = new URLSearchParams();
3878
4164
  params.set("file_path", filePath);
4165
+ params.set("remote", remote.toString());
3879
4166
  try {
3880
4167
  const metadataRes = await fetch(`${this.contentUrl}/v1/${this.project}/patches/${patchId}/files?${params}`, {
3881
4168
  headers: {
@@ -4104,8 +4391,112 @@ class ValOpsHttp extends ValOps {
4104
4391
  }
4105
4392
  }
4106
4393
 
4394
+ const host = process.env.VAL_CONTENT_URL || "https://content.val.build";
4395
+ const SettingsSchema = z.object({
4396
+ publicProjectId: z.string(),
4397
+ remoteFileBuckets: z.array(z.object({
4398
+ bucket: z.string()
4399
+ }))
4400
+ });
4401
+ async function getSettings(projectName, auth) {
4402
+ try {
4403
+ const response = await fetch(`${host}/v1/${projectName}/settings`, {
4404
+ headers: "pat" in auth ? {
4405
+ "x-val-pat": auth.pat,
4406
+ "Content-Type": "application/json"
4407
+ } : {
4408
+ Authorization: `Bearer ${auth.apiKey}`,
4409
+ "Content-Type": "application/json"
4410
+ }
4411
+ });
4412
+ if (response.status === 404) {
4413
+ return {
4414
+ success: false,
4415
+ message: `Project ${projectName} not found. Verify that user has access to project and that it exists.`
4416
+ };
4417
+ }
4418
+ if (response.status !== 200) {
4419
+ return {
4420
+ success: false,
4421
+ message: `Failed to get project id: ${response.statusText}`
4422
+ };
4423
+ }
4424
+ const json = await response.json();
4425
+ const parseRes = SettingsSchema.safeParse(json);
4426
+ if (!parseRes.success) {
4427
+ return {
4428
+ success: false,
4429
+ message: `Failed to parse settings data: ${parseRes.error.message}`
4430
+ };
4431
+ }
4432
+ return {
4433
+ success: true,
4434
+ data: parseRes.data
4435
+ };
4436
+ } catch (err) {
4437
+ return {
4438
+ success: false,
4439
+ message: `Failed to get project id. Check network connection and try again.`
4440
+ };
4441
+ }
4442
+ }
4443
+
4444
+ function getPersonalAccessTokenPath(root) {
4445
+ return fsPath__default.join(fsPath__default.resolve(root), ".val", "pat.json");
4446
+ }
4447
+ function parsePersonalAccessTokenFile(content) {
4448
+ if (!content) {
4449
+ return {
4450
+ success: false,
4451
+ error: "Invalid content: undefined"
4452
+ };
4453
+ }
4454
+ let patFileContent;
4455
+ try {
4456
+ patFileContent = JSON.parse(content);
4457
+ } catch {
4458
+ return {
4459
+ success: false,
4460
+ error: `Invalid content: file is not a valid JSON file`
4461
+ };
4462
+ }
4463
+ if (typeof patFileContent !== "object") {
4464
+ return {
4465
+ success: false,
4466
+ error: "Invalid content: not an object"
4467
+ };
4468
+ }
4469
+ if (!patFileContent) {
4470
+ return {
4471
+ success: false,
4472
+ error: "Invalid content: null"
4473
+ };
4474
+ }
4475
+ if (!("pat" in patFileContent)) {
4476
+ return {
4477
+ success: false,
4478
+ error: "Invalid content: key 'pat' is missing"
4479
+ };
4480
+ }
4481
+ const patField = patFileContent.pat;
4482
+ if (typeof patField === "string") {
4483
+ return {
4484
+ success: true,
4485
+ data: {
4486
+ pat: patField
4487
+ }
4488
+ };
4489
+ } else {
4490
+ return {
4491
+ success: false,
4492
+ error: "Invalid content: pat is not a string"
4493
+ };
4494
+ }
4495
+ }
4496
+
4107
4497
  /* eslint-disable @typescript-eslint/no-unused-vars */
4108
4498
  const ValServer = (valModules, options, callbacks) => {
4499
+ process.env.VAL_REMOTE_HOST || DEFAULT_VAL_REMOTE_HOST;
4109
4500
  let serverOps;
4110
4501
  if (options.mode === "fs") {
4111
4502
  serverOps = new ValOpsFS(options.cwd, valModules, {
@@ -4259,6 +4650,76 @@ const ValServer = (valModules, options, callbacks) => {
4259
4650
  redirectTo: appAuthorizeUrl
4260
4651
  };
4261
4652
  };
4653
+ let remoteFileAuth = null;
4654
+ const getRemoteFileAuth = async () => {
4655
+ if (remoteFileAuth) {
4656
+ return {
4657
+ status: 200,
4658
+ json: {
4659
+ remoteFileAuth
4660
+ }
4661
+ };
4662
+ }
4663
+ if (options.apiKey) {
4664
+ remoteFileAuth = {
4665
+ apiKey: options.apiKey
4666
+ };
4667
+ } else if (serverOps instanceof ValOpsFS) {
4668
+ const projectRootDir = options.config.root;
4669
+ if (!projectRootDir) {
4670
+ return {
4671
+ status: 400,
4672
+ json: {
4673
+ errorCode: "project-not-configured",
4674
+ message: "Root directory was empty"
4675
+ }
4676
+ };
4677
+ }
4678
+ const fs = await import('fs');
4679
+ const patPath = getPersonalAccessTokenPath(fsPath__default.join(process.cwd()));
4680
+ let patFile;
4681
+ try {
4682
+ patFile = await fs.promises.readFile(patPath, "utf-8");
4683
+ } catch (err) {
4684
+ return {
4685
+ status: 400,
4686
+ json: {
4687
+ errorCode: "pat-error",
4688
+ message: "Could not read personal access token file"
4689
+ }
4690
+ };
4691
+ }
4692
+ const patRes = parsePersonalAccessTokenFile(patFile);
4693
+ if (patRes.success) {
4694
+ remoteFileAuth = {
4695
+ pat: patRes.data.pat
4696
+ };
4697
+ } else {
4698
+ return {
4699
+ status: 400,
4700
+ json: {
4701
+ errorCode: "pat-error",
4702
+ message: "Could not parse personal access token file"
4703
+ }
4704
+ };
4705
+ }
4706
+ }
4707
+ if (!remoteFileAuth) {
4708
+ return {
4709
+ status: 400,
4710
+ json: {
4711
+ errorCode: "project-not-configured",
4712
+ message: "Remote file auth is not configured"
4713
+ }
4714
+ };
4715
+ }
4716
+ return {
4717
+ status: 200,
4718
+ json: {
4719
+ remoteFileAuth
4720
+ }
4721
+ };
4722
+ };
4262
4723
  return {
4263
4724
  "/draft/enable": {
4264
4725
  GET: async req => {
@@ -4572,6 +5033,63 @@ const ValServer = (valModules, options, callbacks) => {
4572
5033
  };
4573
5034
  }
4574
5035
  },
5036
+ "/remote/settings": {
5037
+ GET: async req => {
5038
+ const cookies = req.cookies;
5039
+ const auth = getAuth(cookies);
5040
+ if (auth.error) {
5041
+ return {
5042
+ status: 401,
5043
+ json: {
5044
+ errorCode: "unauthorized",
5045
+ message: auth.error
5046
+ }
5047
+ };
5048
+ }
5049
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
5050
+ return {
5051
+ status: 401,
5052
+ json: {
5053
+ errorCode: "unauthorized",
5054
+ message: "Unauthorized"
5055
+ }
5056
+ };
5057
+ }
5058
+ if (!options.project) {
5059
+ return {
5060
+ status: 400,
5061
+ json: {
5062
+ errorCode: "project-not-configured",
5063
+ message: "Project is not configured. Update your val.config file with a valid remote project"
5064
+ }
5065
+ };
5066
+ }
5067
+ const remoteFileAuthRes = await getRemoteFileAuth();
5068
+ if (remoteFileAuthRes.status !== 200) {
5069
+ return remoteFileAuthRes;
5070
+ }
5071
+ const remoteFileAuth = remoteFileAuthRes.json.remoteFileAuth;
5072
+ const settingsRes = await getSettings(options.project, remoteFileAuth);
5073
+ if (!settingsRes.success) {
5074
+ console.warn("Could not get public project id: " + settingsRes.message);
5075
+ return {
5076
+ status: 400,
5077
+ json: {
5078
+ errorCode: "error-could-not-get-settings",
5079
+ message: `Could not get settings: ${settingsRes.message}`
5080
+ }
5081
+ };
5082
+ }
5083
+ return {
5084
+ status: 200,
5085
+ json: {
5086
+ publicProjectId: settingsRes.data.publicProjectId,
5087
+ remoteFileBuckets: settingsRes.data.remoteFileBuckets,
5088
+ coreVersion: Internal.VERSION.core || "unknown"
5089
+ }
5090
+ };
5091
+ }
5092
+ },
4575
5093
  //#region stat
4576
5094
  "/stat": {
4577
5095
  POST: async req => {
@@ -4931,7 +5449,8 @@ const ValServer = (valModules, options, callbacks) => {
4931
5449
  };
4932
5450
  let sourcesValidation = {
4933
5451
  errors: {},
4934
- files: {}
5452
+ files: {},
5453
+ remoteFiles: {}
4935
5454
  };
4936
5455
  if (query.validate_sources || query.validate_binary_files) {
4937
5456
  const schemas = await serverOps.getSchemas();
@@ -4940,6 +5459,8 @@ const ValServer = (valModules, options, callbacks) => {
4940
5459
  // TODO: send binary files validation errors
4941
5460
  if (query.validate_binary_files) {
4942
5461
  await serverOps.validateFiles(schemas, sourcesRes.sources, sourcesValidation.files);
5462
+ // TODO : actually do something with this
5463
+ await serverOps.validateRemoteFiles(schemas, sourcesRes.sources, sourcesValidation.remoteFiles);
4943
5464
  }
4944
5465
  }
4945
5466
  const schemaSha = await serverOps.getSchemaSha();
@@ -5111,7 +5632,12 @@ const ValServer = (valModules, options, callbacks) => {
5111
5632
  };
5112
5633
  }
5113
5634
  if (serverOps instanceof ValOpsFS) {
5114
- await serverOps.saveFiles(preparedCommit);
5635
+ const remoteFileAuthRes = await getRemoteFileAuth();
5636
+ if (remoteFileAuthRes.status !== 200) {
5637
+ return remoteFileAuthRes;
5638
+ }
5639
+ const remoteFileAuth = remoteFileAuthRes.json.remoteFileAuth;
5640
+ await serverOps.saveOrUploadFiles(preparedCommit, remoteFileAuth);
5115
5641
  await serverOps.deletePatches(patchIds);
5116
5642
  return {
5117
5643
  status: 200,
@@ -5187,11 +5713,16 @@ const ValServer = (valModules, options, callbacks) => {
5187
5713
  let cacheControl;
5188
5714
  let fileBuffer;
5189
5715
  let mimeType;
5716
+ const remote = query.remote === "true";
5190
5717
  if (query.patch_id) {
5191
- fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
5718
+ fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id, remote);
5192
5719
  mimeType = Internal.filenameToMimeType(filePath);
5193
- cacheControl = "public, max-age=20000, immutable";
5720
+ // TODO: reenable this:
5721
+ // cacheControl = "public, max-age=20000, immutable";
5194
5722
  } else {
5723
+ if (serverOps instanceof ValOpsHttp && remote) {
5724
+ console.error(`Remote file: ${filePath} requested without patch id. This is most likely a bug in Val.`);
5725
+ }
5195
5726
  fileBuffer = await serverOps.getBinaryFile(filePath);
5196
5727
  }
5197
5728
  if (fileBuffer) {
@@ -5938,7 +6469,7 @@ class ValFSHost {
5938
6469
  }
5939
6470
 
5940
6471
  async function extractImageMetadata(filename, input) {
5941
- const imageSize = sizeOf(input);
6472
+ const imageSize = sizeOf(new Uint8Array(input));
5942
6473
  let mimeType = null;
5943
6474
  if (imageSize.type) {
5944
6475
  const possibleMimeType = `image/${imageSize.type}`;
@@ -5988,33 +6519,189 @@ function getValidationErrorFileRef(validationError) {
5988
6519
  return maybeRef;
5989
6520
  }
5990
6521
 
5991
- // TODO: find a better name? transformFixesToPatch?
5992
- async function createFixPatch(config, apply, sourcePath, validationError) {
5993
- async function getImageMetadata() {
5994
- const fileRef = getValidationErrorFileRef(validationError);
5995
- if (!fileRef) {
5996
- // TODO:
5997
- throw Error("Cannot fix image without a file reference");
5998
- }
5999
- const filename = fsPath__default.join(config.projectRoot, fileRef);
6000
- const buffer = fs.readFileSync(filename);
6001
- return extractImageMetadata(filename, buffer);
6002
- }
6003
- async function getFileMetadata() {
6004
- const fileRef = getValidationErrorFileRef(validationError);
6005
- if (!fileRef) {
6006
- // TODO:
6007
- throw Error("Cannot fix file without a file reference");
6008
- }
6009
- const filename = fsPath__default.join(config.projectRoot, fileRef);
6010
- fs.readFileSync(filename);
6011
- return extractFileMetadata(fileRef);
6522
+ const textEncoder = new TextEncoder();
6523
+ async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
6524
+ const remoteRefDataRes = Internal.remote.splitRemoteRef(ref);
6525
+ if (remoteRefDataRes.status === "error") {
6526
+ return remoteRefDataRes;
6527
+ }
6528
+ const fileHash = remoteRefDataRes.fileHash;
6529
+ if (!fileHash) {
6530
+ return {
6531
+ status: "error",
6532
+ error: `File hash is missing in remote ref: ${ref}`
6533
+ };
6534
+ }
6535
+ const relativeFilePath = remoteRefDataRes.filePath;
6536
+ if (!relativeFilePath) {
6537
+ return {
6538
+ status: "error",
6539
+ error: `File path is missing in remote ref: ${ref}`
6540
+ };
6541
+ }
6542
+ if (!relativeFilePath.startsWith("public/val/")) {
6543
+ return {
6544
+ status: "error",
6545
+ error: `File path must be within the public/val/ directory (e.g. public/val/path/to/file.txt). Got: ${relativeFilePath}`
6546
+ };
6547
+ }
6548
+ const coreVersion = Internal.VERSION.core || "unknown";
6549
+ const fileExt = getFileExt(relativeFilePath);
6550
+ const currentValidationHash = Internal.remote.getValidationHash(coreVersion, schema, fileExt, metadata,
6551
+ // TODO: validate
6552
+ fileHash, textEncoder);
6553
+ const validationHashFromRemoteRefValues = Internal.remote.getValidationHash(remoteRefDataRes.version, schema, getFileExt(remoteRefDataRes.filePath), metadata,
6554
+ // TODO: validate
6555
+ fileHash, textEncoder);
6556
+ if (
6557
+ // Current validation hash is the same as the remote validation hash
6558
+ // We assume the uploaded file exists and is correct
6559
+ currentValidationHash === remoteRefDataRes.validationHash &&
6560
+ // This can happen if the version or fileExt has been tampered with
6561
+ // (or if the way it is computed has changed)
6562
+ validationHashFromRemoteRefValues === remoteRefDataRes.validationHash) {
6563
+ return {
6564
+ status: "success"
6565
+ };
6566
+ }
6567
+ // Validation hash does not match, so we need to re-validate the remote content
6568
+ const fileBufferRes = await getFileBufferFromRemote(ref, fileExt, remoteRefDataRes.fileHash, projectRoot);
6569
+
6570
+ // We could not download the remote file for some reason (for example, the file hash is wrong)
6571
+ if (fileBufferRes.status === "error") {
6572
+ return fileBufferRes;
6573
+ }
6574
+
6575
+ // At this point we have the file buffer and we know the correct file hash
6576
+ // We must create a new ref since something has changed
6577
+
6578
+ let currentMetadata;
6579
+ try {
6580
+ if (schema.type === "image") {
6581
+ currentMetadata = await extractImageMetadata(remoteRefDataRes.filePath, fileBufferRes.fileBuffer);
6582
+ } else if (schema.type === "file") {
6583
+ currentMetadata = await extractFileMetadata(remoteRefDataRes.filePath, fileBufferRes.fileBuffer);
6584
+ } else {
6585
+ const exhaustiveCheck = schema;
6586
+ return {
6587
+ status: "error",
6588
+ error: `Unknown schema type: ${JSON.stringify(exhaustiveCheck)}`
6589
+ };
6590
+ }
6591
+ } catch (err) {
6592
+ return {
6593
+ status: "error",
6594
+ error: `Failed to extract metadata from remote file: ${err instanceof Error ? err.message : "Unknown error"}. Cached file: ${fileBufferRes.cachedFilePath}`
6595
+ };
6596
+ }
6597
+ const updatedMetadata = {
6598
+ ...metadata,
6599
+ ...currentMetadata
6600
+ };
6601
+ const nextValidationHash = Internal.remote.getValidationHash(coreVersion, schema, fileExt, updatedMetadata, fileBufferRes.fileHash, textEncoder);
6602
+ if (!updatedMetadata.mimeType) {
6603
+ return {
6604
+ status: "error",
6605
+ error: `MIME type is missing in metadata: ${JSON.stringify(updatedMetadata)}`
6606
+ };
6607
+ }
6608
+ const newFileExt = Internal.mimeTypeToFileExt(updatedMetadata.mimeType);
6609
+ const newFilePath = relativeFilePath.slice(0, -fileExt.length) + newFileExt;
6610
+ return {
6611
+ status: "fix-required",
6612
+ metadata: updatedMetadata,
6613
+ ref: Internal.remote.createRemoteRef(remoteHost, {
6614
+ publicProjectId: remoteRefDataRes.projectId,
6615
+ coreVersion,
6616
+ bucket: remoteRefDataRes.bucket,
6617
+ validationHash: nextValidationHash,
6618
+ fileHash: fileBufferRes.fileHash,
6619
+ filePath: newFilePath
6620
+ })
6621
+ };
6622
+ }
6623
+ function getCachedRemoteFileDir(projectRoot) {
6624
+ // store in projectRoot/.val/remote-file-cache
6625
+ const remoteFileCacheDir = fsPath__default.join(projectRoot, ".val", "remote-file-cache");
6626
+ return remoteFileCacheDir;
6627
+ }
6628
+ function getCachedRemoteFilePath(fileExt, currentFileHash, remoteFileCacheDir) {
6629
+ const remoteFilePath = fsPath__default.join(remoteFileCacheDir, currentFileHash + "." + fileExt);
6630
+ return remoteFilePath;
6631
+ }
6632
+ async function getFileBufferFromRemote(ref, fileExt, currentFileHash, projectRoot) {
6633
+ const remoteFileCacheDir = getCachedRemoteFileDir(projectRoot);
6634
+ const remoteFilePath = getCachedRemoteFilePath(fileExt, currentFileHash, remoteFileCacheDir);
6635
+ try {
6636
+ const fileBuffer = await fs.promises.readFile(remoteFilePath);
6637
+ const computedFileHash = Internal.remote.getFileHash(fileBuffer);
6638
+ if (computedFileHash !== currentFileHash) {
6639
+ await fs.promises.unlink(remoteFilePath);
6640
+ throw Error(`Cached file hash does not match the expected hash: ${computedFileHash} !== ${currentFileHash}`);
6641
+ }
6642
+ } catch (err) {
6643
+ try {
6644
+ await fs.promises.mkdir(remoteFileCacheDir, {
6645
+ recursive: true
6646
+ });
6647
+ await downloadFileFromRemote(ref, remoteFilePath);
6648
+ } catch (err) {
6649
+ try {
6650
+ // try to delete in case of partial download
6651
+ await fs.promises.unlink(remoteFilePath);
6652
+ } catch {
6653
+ //
6654
+ }
6655
+ return {
6656
+ status: "error",
6657
+ error: `Failed to download remote file: ${err instanceof Error ? err.message : "Unknown error"}`
6658
+ };
6659
+ }
6012
6660
  }
6661
+ const fileBuffer = await fs.promises.readFile(remoteFilePath);
6662
+ const computedFileHash = Internal.remote.getFileHash(fileBuffer);
6663
+ return {
6664
+ status: "success",
6665
+ fileBuffer,
6666
+ fileHash: computedFileHash,
6667
+ cachedFilePath: remoteFilePath
6668
+ };
6669
+ }
6670
+ async function downloadFileFromRemote(ref, filePath) {
6671
+ return new Promise((resolve, reject) => {
6672
+ const url = new URL(ref);
6673
+ const client = url.protocol === "https:" ? https : http;
6674
+ const request = client.get(url, response => {
6675
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
6676
+ // Handle redirects
6677
+ return downloadFileFromRemote(response.headers.location, filePath).then(resolve).catch(reject);
6678
+ }
6679
+ if (response.statusCode == 404) {
6680
+ return reject(new Error(`${ref}. File not found. The URL is most likely incorrect.`));
6681
+ }
6682
+ if (response.statusCode !== 200) {
6683
+ return reject(new Error(`${ref}. Failed to download file. HTTP Status: ${response.statusCode}`));
6684
+ }
6685
+ const writeStream = fs.createWriteStream(filePath);
6686
+ response.pipe(writeStream);
6687
+ writeStream.on("finish", () => resolve({
6688
+ status: "success",
6689
+ filePath
6690
+ }));
6691
+ writeStream.on("error", err => reject(new Error(`${ref}. Error writing file: ${err.message}`)));
6692
+ });
6693
+ request.on("error", err => reject(new Error(`${ref}. Download error: ${err.message}`)));
6694
+ request.end();
6695
+ });
6696
+ }
6697
+
6698
+ // TODO: find a better name? transformFixesToPatch?
6699
+ async function createFixPatch(config, apply, sourcePath, validationError, remoteFiles, moduleSource, moduleSchema) {
6013
6700
  const remainingErrors = [];
6014
6701
  const patch = [];
6015
6702
  for (const fix of validationError.fixes || []) {
6016
6703
  if (fix === "image:check-metadata" || fix === "image:add-metadata") {
6017
- const imageMetadata = await getImageMetadata();
6704
+ const imageMetadata = await getImageMetadata(config.projectRoot, validationError);
6018
6705
  if (imageMetadata.width === undefined || imageMetadata.height === undefined) {
6019
6706
  remainingErrors.push({
6020
6707
  ...validationError,
@@ -6087,7 +6774,7 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
6087
6774
  });
6088
6775
  }
6089
6776
  } else if (fix === "file:add-metadata" || fix === "file:check-metadata") {
6090
- const fileMetadata = await getFileMetadata();
6777
+ const fileMetadata = await getFileMetadata(config.projectRoot, validationError);
6091
6778
  if (fileMetadata === undefined) {
6092
6779
  remainingErrors.push({
6093
6780
  ...validationError,
@@ -6144,6 +6831,161 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
6144
6831
  }
6145
6832
  });
6146
6833
  }
6834
+ } else if (fix === "image:upload-remote" || fix === "file:upload-remote") {
6835
+ const remoteFile = remoteFiles[sourcePath];
6836
+ if (!remoteFile) {
6837
+ remainingErrors.push({
6838
+ ...validationError,
6839
+ message: "Cannot fix local to remote image: remote image was not uploaded",
6840
+ fixes: undefined
6841
+ });
6842
+ } else {
6843
+ patch.push({
6844
+ op: "replace",
6845
+ value: {
6846
+ _type: "remote",
6847
+ _ref: remoteFile.ref,
6848
+ metadata: remoteFile.metadata
6849
+ },
6850
+ path: sourceToPatchPath(sourcePath)
6851
+ });
6852
+ }
6853
+ } else if (fix === "image:download-remote" || fix === "file:download-remote") {
6854
+ const v = getRemoteValueFromValidationError(validationError);
6855
+ if (!v.success) {
6856
+ remainingErrors.push({
6857
+ ...validationError,
6858
+ message: v.message,
6859
+ fixes: undefined
6860
+ });
6861
+ continue;
6862
+ }
6863
+ const splitRemoteRefDataRes = Internal.remote.splitRemoteRef(v._ref);
6864
+ if (splitRemoteRefDataRes.status === "error") {
6865
+ remainingErrors.push({
6866
+ ...validationError,
6867
+ message: splitRemoteRefDataRes.error,
6868
+ fixes: undefined
6869
+ });
6870
+ continue;
6871
+ }
6872
+ const url = v._ref;
6873
+ const filePath = splitRemoteRefDataRes.filePath;
6874
+ if (!filePath) {
6875
+ remainingErrors.push({
6876
+ ...validationError,
6877
+ message: "Unexpected error while downloading remote (no filePath)",
6878
+ fixes: undefined
6879
+ });
6880
+ continue;
6881
+ }
6882
+ if (!filePath.startsWith("public/val/")) {
6883
+ remainingErrors.push({
6884
+ ...validationError,
6885
+ message: "Unexpected error while downloading remote (invalid file path - must start with public/val/)",
6886
+ fixes: undefined
6887
+ });
6888
+ continue;
6889
+ }
6890
+ const absoluteFilePath = fsPath__default.join(config.projectRoot, splitRemoteRefDataRes.filePath);
6891
+ await fs.promises.mkdir(fsPath__default.dirname(absoluteFilePath), {
6892
+ recursive: true
6893
+ });
6894
+ const res = await downloadFileFromRemote(url, absoluteFilePath);
6895
+ if (res.status === "error") {
6896
+ remainingErrors.push({
6897
+ ...validationError,
6898
+ message: res.error,
6899
+ fixes: undefined
6900
+ });
6901
+ continue;
6902
+ }
6903
+ const value = {
6904
+ [VAL_EXTENSION]: "file",
6905
+ [FILE_REF_PROP]: `/${filePath}`,
6906
+ ...(fix === "image:download-remote" ? {
6907
+ [FILE_REF_SUBTYPE_TAG]: "image"
6908
+ } : {})
6909
+ };
6910
+ patch.push({
6911
+ op: "replace",
6912
+ path: sourceToPatchPath(sourcePath),
6913
+ value: v.metadata ? {
6914
+ ...value,
6915
+ metadata: v.metadata
6916
+ } : value
6917
+ });
6918
+ } else if (fix === "file:check-remote" || fix === "image:check-remote") {
6919
+ const v = getRemoteValueFromValidationError(validationError);
6920
+ if (!v.success) {
6921
+ remainingErrors.push({
6922
+ ...validationError,
6923
+ message: v.message,
6924
+ fixes: undefined
6925
+ });
6926
+ continue;
6927
+ }
6928
+ const [, modulePath] = Internal.splitModuleFilePathAndModulePath(sourcePath);
6929
+ if (moduleSource === undefined) {
6930
+ remainingErrors.push({
6931
+ ...validationError,
6932
+ message: "Unexpected error while checking remote (no moduleSource)",
6933
+ fixes: undefined
6934
+ });
6935
+ continue;
6936
+ }
6937
+ if (moduleSchema === undefined) {
6938
+ remainingErrors.push({
6939
+ ...validationError,
6940
+ message: "Unexpected error while checking remote (no moduleSchema)",
6941
+ fixes: undefined
6942
+ });
6943
+ continue;
6944
+ }
6945
+ const {
6946
+ schema: schemaAtPath
6947
+ } = Internal.resolvePath(modulePath, moduleSource, moduleSchema);
6948
+ if (schemaAtPath.type === "image" || schemaAtPath.type === "file") {
6949
+ const res = await checkRemoteRef(config.remoteHost, v._ref, config.projectRoot, schemaAtPath, v.metadata);
6950
+ if (res.status === "success") ; else if (res.status === "error") {
6951
+ remainingErrors.push({
6952
+ ...validationError,
6953
+ message: res.error,
6954
+ fixes: undefined
6955
+ });
6956
+ } else if (res.status === "fix-required") {
6957
+ if (apply) {
6958
+ patch.push({
6959
+ op: "replace",
6960
+ path: sourceToPatchPath(sourcePath),
6961
+ value: {
6962
+ _type: "remote",
6963
+ _ref: res.ref,
6964
+ metadata: res.metadata
6965
+ }
6966
+ });
6967
+ } else {
6968
+ remainingErrors.push({
6969
+ ...validationError,
6970
+ message: `Remote ref: ${res.ref} is not valid. Use the --fix flag to fix this issue.`,
6971
+ fixes: undefined
6972
+ });
6973
+ }
6974
+ } else {
6975
+ const exhaustiveCheck = res;
6976
+ remainingErrors.push({
6977
+ ...validationError,
6978
+ message: `Internal error found found unexpected status: ${JSON.stringify(exhaustiveCheck)}`,
6979
+ fixes: undefined
6980
+ });
6981
+ }
6982
+ } else {
6983
+ remainingErrors.push({
6984
+ ...validationError,
6985
+ message: "Could not check remote ref: schema type is not image or file: " + (schemaAtPath === null || schemaAtPath === void 0 ? void 0 : schemaAtPath.type),
6986
+ fixes: undefined
6987
+ });
6988
+ }
6147
6989
  }
6148
6990
  }
6149
6991
  if (!validationError.fixes || validationError.fixes.length === 0) {
@@ -6154,5 +6996,72 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
6154
6996
  remainingErrors
6155
6997
  };
6156
6998
  }
6999
+ function getRemoteValueFromValidationError(v) {
7000
+ if (v.value && typeof v.value !== "object") {
7001
+ return {
7002
+ success: false,
7003
+ message: "Unexpected error while checking remote (not an object)"
7004
+ };
7005
+ }
7006
+ if (!v.value) {
7007
+ return {
7008
+ success: false,
7009
+ message: "Unexpected error while checking remote (no value)"
7010
+ };
7011
+ }
7012
+ if (typeof v.value !== "object" || v.value === null || !(FILE_REF_PROP in v.value)) {
7013
+ return {
7014
+ success: false,
7015
+ message: "Unexpected error while checking remote (no _ref in value)"
7016
+ };
7017
+ }
7018
+ if (typeof v.value._ref !== "string") {
7019
+ return {
7020
+ success: false,
7021
+ message: "Unexpected error while checking remote (_ref is not a string)"
7022
+ };
7023
+ }
7024
+ let metadata;
7025
+ if ("metadata" in v.value && typeof v.value.metadata === "object") {
7026
+ if (v.value.metadata === null) {
7027
+ return {
7028
+ success: false,
7029
+ message: "Unexpected error while checking remote (metadata is null)"
7030
+ };
7031
+ }
7032
+ if (Array.isArray(v.value.metadata)) {
7033
+ return {
7034
+ success: false,
7035
+ message: "Unexpected error while checking remote (metadata is an array)"
7036
+ };
7037
+ }
7038
+ metadata = v.value.metadata;
7039
+ }
7040
+ return {
7041
+ success: true,
7042
+ _ref: v.value._ref,
7043
+ metadata
7044
+ };
7045
+ }
7046
+ async function getImageMetadata(projectRoot, validationError) {
7047
+ const fileRef = getValidationErrorFileRef(validationError);
7048
+ if (!fileRef) {
7049
+ // TODO:
7050
+ throw Error("Cannot fix image without a file reference");
7051
+ }
7052
+ const filename = fsPath__default.join(projectRoot, fileRef);
7053
+ const buffer = fs.readFileSync(filename);
7054
+ return extractImageMetadata(filename, buffer);
7055
+ }
7056
+ async function getFileMetadata(projectRoot, validationError) {
7057
+ const fileRef = getValidationErrorFileRef(validationError);
7058
+ if (!fileRef) {
7059
+ // TODO:
7060
+ throw Error("Cannot fix file without a file reference");
7061
+ }
7062
+ const filename = fsPath__default.join(projectRoot, fileRef);
7063
+ fs.readFileSync(filename);
7064
+ return extractFileMetadata(fileRef);
7065
+ }
6157
7066
 
6158
- export { Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createFixPatch, createService, createValApiRouter, createValServer, decodeJwt, encodeJwt, formatSyntaxErrorTree, getCompilerOptions, getExpire, patchSourceFile, safeReadGit };
7067
+ export { Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createFixPatch, createService, createValApiRouter, createValServer, decodeJwt, encodeJwt, formatSyntaxErrorTree, getCompilerOptions, getExpire, getPersonalAccessTokenPath, getSettings, parsePersonalAccessTokenFile, patchSourceFile, safeReadGit, uploadRemoteFile };