@valbuild/server 0.61.0 → 0.62.1

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.
@@ -7,15 +7,16 @@ var ts = require('typescript');
7
7
  var fp = require('@valbuild/core/fp');
8
8
  var core = require('@valbuild/core');
9
9
  var patch = require('@valbuild/core/patch');
10
- var path = require('path');
10
+ var fsPath = require('path');
11
11
  var fs = require('fs');
12
12
  var sucrase = require('sucrase');
13
13
  var ui = require('@valbuild/ui');
14
- var z = require('zod');
14
+ var server = require('@valbuild/ui/server');
15
15
  var internal = require('@valbuild/shared/internal');
16
+ var crypto$1 = require('crypto');
17
+ var z = require('zod');
16
18
  var sizeOf = require('image-size');
17
- var crypto = require('crypto');
18
- var server = require('@valbuild/ui/server');
19
+ var zodValidationError = require('zod-validation-error');
19
20
 
20
21
  function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
21
22
 
@@ -38,11 +39,11 @@ function _interopNamespace(e) {
38
39
  }
39
40
 
40
41
  var ts__default = /*#__PURE__*/_interopDefault(ts);
41
- var path__namespace = /*#__PURE__*/_interopNamespace(path);
42
+ var fsPath__namespace = /*#__PURE__*/_interopNamespace(fsPath);
42
43
  var fs__default = /*#__PURE__*/_interopDefault(fs);
44
+ var crypto__default = /*#__PURE__*/_interopDefault(crypto$1);
43
45
  var z__default = /*#__PURE__*/_interopDefault(z);
44
46
  var sizeOf__default = /*#__PURE__*/_interopDefault(sizeOf);
45
- var crypto__default = /*#__PURE__*/_interopDefault(crypto);
46
47
 
47
48
  class ValSyntaxError {
48
49
  constructor(message, node) {
@@ -634,10 +635,10 @@ class TSOps {
634
635
  }
635
636
 
636
637
  function getSyntheticContainingPath(rootDir) {
637
- return path__namespace["default"].join(rootDir, "<val>"); // TODO: this is the synthetic path used when evaluating / patching modules. I am not sure <val> is the best choice: val.ts / js better? But that is weird too. At least now it is clear(er) that it is indeed a synthetic file (i.e. not an actual file)
638
+ return fsPath__namespace["default"].join(rootDir, "<val>"); // TODO: this is the synthetic path used when evaluating / patching modules. I am not sure <val> is the best choice: val.ts / js better? But that is weird too. At least now it is clear(er) that it is indeed a synthetic file (i.e. not an actual file)
638
639
  }
639
640
 
640
- const ops$1 = new TSOps(document => {
641
+ const ops = new TSOps(document => {
641
642
  return fp.pipe(analyzeValModule(document), fp.result.map(({
642
643
  source
643
644
  }) => source));
@@ -647,12 +648,12 @@ const ops$1 = new TSOps(document => {
647
648
  const patchValFile = async (id, rootDir, patch$1, sourceFileHandler, runtime) => {
648
649
  // const timeId = randomUUID();
649
650
  // console.time("patchValFile" + timeId);
650
- const filePath = sourceFileHandler.resolveSourceModulePath(getSyntheticContainingPath(rootDir), `.${id}.val`);
651
+ const filePath = sourceFileHandler.resolveSourceModulePath(getSyntheticContainingPath(rootDir), `.${id.replace(".val.ts", ".val").replace(".val.js", ".val").replace(".val.jsx", ".val").replace(".val.tsx", ".val")}`);
651
652
  const sourceFile = sourceFileHandler.getSourceFile(filePath);
652
653
  if (!sourceFile) {
653
654
  throw Error(`Source file ${filePath} not found`);
654
655
  }
655
- const derefRes = core.derefPatch(patch$1, sourceFile, ops$1);
656
+ const derefRes = core.derefPatch(patch$1, sourceFile, ops);
656
657
  if (fp.result.isErr(derefRes)) {
657
658
  throw derefRes.error;
658
659
  }
@@ -690,12 +691,12 @@ function convertDataUrlToBase64(dataUrl) {
690
691
  }
691
692
  const patchSourceFile = (sourceFile, patch$1) => {
692
693
  if (typeof sourceFile === "string") {
693
- return patch.applyPatch(ts__default["default"].createSourceFile("<val>", sourceFile, ts__default["default"].ScriptTarget.ES2015), ops$1, patch$1);
694
+ return patch.applyPatch(ts__default["default"].createSourceFile("<val>", sourceFile, ts__default["default"].ScriptTarget.ES2015), ops, patch$1);
694
695
  }
695
- return patch.applyPatch(sourceFile, ops$1, patch$1);
696
+ return patch.applyPatch(sourceFile, ops, patch$1);
696
697
  };
697
698
 
698
- const readValFile = async (id, rootDirPath, runtime, options) => {
699
+ const readValFile = async (moduleFilePath, rootDirPath, runtime, options) => {
699
700
  const context = runtime.newContext();
700
701
 
701
702
  // avoid failures when console.log is called
@@ -730,12 +731,12 @@ const readValFile = async (id, rootDirPath, runtime, options) => {
730
731
  processHandle.dispose();
731
732
  optionsHandle.dispose();
732
733
  try {
733
- const modulePath = `.${id}.val`;
734
+ const modulePath = `.${moduleFilePath.replace(".val.js", ".val").replace(".val.ts", ".val").replace(".val.tsx", ".val").replace(".val.jsx", ".val")}`;
734
735
  const code = `import * as valModule from ${JSON.stringify(modulePath)};
735
736
  import { Internal } from "@valbuild/core";
736
737
 
737
738
  globalThis.valModule = {
738
- id: valModule?.default && Internal.getValPath(valModule?.default),
739
+ path: valModule?.default && Internal.getValPath(valModule?.default),
739
740
  schema: !!globalThis['__VAL_OPTIONS__'].schema ? valModule?.default && Internal.getSchema(valModule?.default)?.serialize() : undefined,
740
741
  source: !!globalThis['__VAL_OPTIONS__'].source ? valModule?.default && Internal.getSource(valModule?.default) : undefined,
741
742
  validation: !!globalThis['__VAL_OPTIONS__'].validate ? valModule?.default && Internal.getSchema(valModule?.default)?.validate(
@@ -749,11 +750,11 @@ globalThis.valModule = {
749
750
  const fatalErrors = [];
750
751
  if (result.error) {
751
752
  const error = result.error.consume(context.dump);
752
- console.error(`Fatal error reading val file: ${id}. Error: ${error.message}\n`, error.stack);
753
+ console.error(`Fatal error reading val file: ${moduleFilePath}. Error: ${error.message}\n`, error.stack);
753
754
  return {
754
- path: id,
755
+ path: moduleFilePath,
755
756
  errors: {
756
- invalidModuleId: id,
757
+ invalidModulePath: moduleFilePath,
757
758
  fatal: [{
758
759
  message: `${error.name || "Unknown error"}: ${error.message || "<no message>"}`,
759
760
  stack: error.stack
@@ -765,21 +766,21 @@ globalThis.valModule = {
765
766
  const valModule = context.getProp(context.global, "valModule").consume(context.dump);
766
767
  if (
767
768
  // if one of these are set it is a Val module, so must validate
768
- (valModule === null || valModule === void 0 ? void 0 : valModule.id) !== undefined || (valModule === null || valModule === void 0 ? void 0 : valModule.schema) !== undefined || (valModule === null || valModule === void 0 ? void 0 : valModule.source) !== undefined) {
769
- if (valModule.id !== id) {
770
- fatalErrors.push(`Wrong c.define id! Expected: '${id}', found: '${valModule.id}'`);
769
+ (valModule === null || valModule === void 0 ? void 0 : valModule.path) !== undefined || (valModule === null || valModule === void 0 ? void 0 : valModule.schema) !== undefined || (valModule === null || valModule === void 0 ? void 0 : valModule.source) !== undefined) {
770
+ if (valModule.path !== moduleFilePath) {
771
+ fatalErrors.push(`Wrong c.define id! Expected: '${moduleFilePath}', found: '${valModule.path}'`);
771
772
  } else if (encodeURIComponent(valModule.id).replace(/%2F/g, "/") !== valModule.id) {
772
773
  fatalErrors.push(`Invalid c.define id! Must be a web-safe path without escape characters, found: '${valModule.id}', which was encoded as: '${encodeURIComponent(valModule.id).replace("%2F", "/")}'`);
773
774
  } else if ((valModule === null || valModule === void 0 ? void 0 : valModule.schema) === undefined && options.schema) {
774
- fatalErrors.push(`Expected val id: '${id}' to have a schema`);
775
+ fatalErrors.push(`Expected val id: '${moduleFilePath}' to have a schema`);
775
776
  } else if ((valModule === null || valModule === void 0 ? void 0 : valModule.source) === undefined && options.source) {
776
- fatalErrors.push(`Expected val id: '${id}' to have a source`);
777
+ fatalErrors.push(`Expected val id: '${moduleFilePath}' to have a source`);
777
778
  }
778
779
  }
779
780
  let errors = false;
780
781
  if (fatalErrors.length > 0) {
781
782
  errors = {
782
- invalidModuleId: valModule.id !== id ? id : undefined,
783
+ invalidModulePath: valModule.path !== moduleFilePath ? moduleFilePath : undefined,
783
784
  fatal: fatalErrors.map(message => ({
784
785
  message
785
786
  }))
@@ -792,7 +793,7 @@ globalThis.valModule = {
792
793
  };
793
794
  }
794
795
  return {
795
- path: valModule.id || id,
796
+ path: valModule.id || moduleFilePath,
796
797
  // NOTE: we use path here, since SerializedModuleContent (maybe bad name?) can be used for whole modules as well as subparts of modules
797
798
  source: valModule.source,
798
799
  schema: valModule.schema,
@@ -805,8 +806,8 @@ globalThis.valModule = {
805
806
  };
806
807
 
807
808
  const getCompilerOptions = (rootDir, parseConfigHost) => {
808
- const tsConfigPath = path__namespace["default"].resolve(rootDir, "tsconfig.json");
809
- const jsConfigPath = path__namespace["default"].resolve(rootDir, "jsconfig.json");
809
+ const tsConfigPath = fsPath__namespace["default"].resolve(rootDir, "tsconfig.json");
810
+ const jsConfigPath = fsPath__namespace["default"].resolve(rootDir, "jsconfig.json");
810
811
  let configFilePath;
811
812
  if (parseConfigHost.fileExists(jsConfigPath)) {
812
813
  configFilePath = jsConfigPath;
@@ -837,7 +838,7 @@ class ValSourceFileHandler {
837
838
  constructor(projectRoot, compilerOptions, host = {
838
839
  ...ts__default["default"].sys,
839
840
  writeFile: (fileName, data, encoding) => {
840
- fs__default["default"].mkdirSync(path__namespace["default"].dirname(fileName), {
841
+ fs__default["default"].mkdirSync(fsPath__namespace["default"].dirname(fileName), {
841
842
  recursive: true
842
843
  });
843
844
  fs__default["default"].writeFileSync(fileName, data, encoding);
@@ -864,7 +865,7 @@ class ValSourceFileHandler {
864
865
  this.host.writeFile(filePath, content, encoding);
865
866
  }
866
867
  resolveSourceModulePath(containingFilePath, requestedModuleName) {
867
- const resolutionRes = ts__default["default"].resolveModuleName(requestedModuleName, path__namespace["default"].isAbsolute(containingFilePath) ? containingFilePath : path__namespace["default"].resolve(this.projectRoot, containingFilePath), this.compilerOptions, this.host, undefined, undefined, ts__default["default"].ModuleKind.ESNext);
868
+ const resolutionRes = ts__default["default"].resolveModuleName(requestedModuleName, fsPath__namespace["default"].isAbsolute(containingFilePath) ? containingFilePath : fsPath__namespace["default"].resolve(this.projectRoot, containingFilePath), this.compilerOptions, this.host, undefined, undefined, ts__default["default"].ModuleKind.ESNext);
868
869
  const resolvedModule = resolutionRes.resolvedModule;
869
870
  if (!resolvedModule) {
870
871
  throw Error(`Could not resolve module "${requestedModuleName}", base: "${containingFilePath}": No resolved modules returned: ${JSON.stringify(resolutionRes)}`);
@@ -887,7 +888,7 @@ class ValModuleLoader {
887
888
  sourceFileHandler, host = {
888
889
  ...ts__default["default"].sys,
889
890
  writeFile: (fileName, data, encoding) => {
890
- fs__default["default"].mkdirSync(path__namespace["default"].dirname(fileName), {
891
+ fs__default["default"].mkdirSync(fsPath__namespace["default"].dirname(fileName), {
891
892
  recursive: true
892
893
  });
893
894
  fs__default["default"].writeFileSync(fileName, data, encoding);
@@ -1179,7 +1180,7 @@ export default new Proxy({}, {
1179
1180
  async function createService(projectRoot, opts, host = {
1180
1181
  ...ts__default["default"].sys,
1181
1182
  writeFile: (fileName, data, encoding) => {
1182
- fs__default["default"].mkdirSync(path__namespace["default"].dirname(fileName), {
1183
+ fs__default["default"].mkdirSync(fsPath__namespace["default"].dirname(fileName), {
1183
1184
  recursive: true
1184
1185
  });
1185
1186
  fs__default["default"].writeFileSync(fileName, data, encoding);
@@ -1198,15 +1199,15 @@ class Service {
1198
1199
  this.runtime = runtime;
1199
1200
  this.projectRoot = projectRoot;
1200
1201
  }
1201
- async get(moduleId, modulePath, options) {
1202
- const valModule = await readValFile(moduleId, this.projectRoot, this.runtime, options ?? {
1202
+ async get(moduleFilePath, modulePath, options) {
1203
+ const valModule = await readValFile(moduleFilePath, this.projectRoot, this.runtime, options ?? {
1203
1204
  validate: true,
1204
1205
  source: true,
1205
1206
  schema: true
1206
1207
  });
1207
1208
  if (valModule.source && valModule.schema) {
1208
1209
  const resolved = core.Internal.resolvePath(modulePath, valModule.source, valModule.schema);
1209
- const sourcePath = resolved.path ? [moduleId, resolved.path].join(".") : moduleId;
1210
+ const sourcePath = resolved.path ? [moduleFilePath, resolved.path].join(".") : moduleFilePath;
1210
1211
  return {
1211
1212
  path: sourcePath,
1212
1213
  schema: resolved.schema instanceof core.Schema ? resolved.schema.serialize() : resolved.schema,
@@ -1222,101 +1223,73 @@ class Service {
1222
1223
  return valModule;
1223
1224
  }
1224
1225
  }
1225
- async patch(moduleId, patch) {
1226
- await patchValFile(moduleId, this.projectRoot, patch, this.sourceFileHandler, this.runtime);
1226
+ async patch(moduleFilePath, patch) {
1227
+ await patchValFile(moduleFilePath, this.projectRoot, patch, this.sourceFileHandler, this.runtime);
1227
1228
  }
1228
1229
  dispose() {
1229
1230
  this.runtime.dispose();
1230
1231
  }
1231
1232
  }
1232
1233
 
1233
- const JSONValueT = z__default["default"].lazy(() => z__default["default"].union([z__default["default"].string(), z__default["default"].number(), z__default["default"].boolean(), z__default["default"].null(), z__default["default"].array(JSONValueT), z__default["default"].record(JSONValueT)]));
1234
-
1235
- /**
1236
- * Raw JSON patch operation.
1237
- */
1238
- const OperationJSONT = z__default["default"].discriminatedUnion("op", [z__default["default"].object({
1239
- op: z__default["default"].literal("add"),
1240
- path: z__default["default"].string(),
1241
- value: JSONValueT
1242
- }).strict(), z__default["default"].object({
1243
- op: z__default["default"].literal("remove"),
1244
- /**
1245
- * Must be non-root
1246
- */
1247
- path: z__default["default"].string()
1248
- }).strict(), z__default["default"].object({
1249
- op: z__default["default"].literal("replace"),
1250
- path: z__default["default"].string(),
1251
- value: JSONValueT
1252
- }).strict(), z__default["default"].object({
1253
- op: z__default["default"].literal("move"),
1254
- /**
1255
- * Must be non-root and not a proper prefix of "path".
1256
- */
1257
- from: z__default["default"].string(),
1258
- path: z__default["default"].string()
1259
- }).strict(), z__default["default"].object({
1260
- op: z__default["default"].literal("copy"),
1261
- from: z__default["default"].string(),
1262
- path: z__default["default"].string()
1263
- }).strict(), z__default["default"].object({
1264
- op: z__default["default"].literal("test"),
1265
- path: z__default["default"].string(),
1266
- value: JSONValueT
1267
- }).strict(), z__default["default"].object({
1268
- op: z__default["default"].literal("file"),
1269
- path: z__default["default"].string(),
1270
- filePath: z__default["default"].string(),
1271
- value: z__default["default"].string()
1272
- }).strict()]);
1273
- const PatchJSON = z__default["default"].array(OperationJSONT);
1274
- /**
1275
- * Raw JSON patch operation.
1276
- */
1277
- const OperationT = z__default["default"].discriminatedUnion("op", [z__default["default"].object({
1278
- op: z__default["default"].literal("add"),
1279
- path: z__default["default"].array(z__default["default"].string()),
1280
- value: JSONValueT
1281
- }).strict(), z__default["default"].object({
1282
- op: z__default["default"].literal("remove"),
1283
- path: z__default["default"].array(z__default["default"].string()).nonempty()
1284
- }).strict(), z__default["default"].object({
1285
- op: z__default["default"].literal("replace"),
1286
- path: z__default["default"].array(z__default["default"].string()),
1287
- value: JSONValueT
1288
- }).strict(), z__default["default"].object({
1289
- op: z__default["default"].literal("move"),
1290
- from: z__default["default"].array(z__default["default"].string()).nonempty(),
1291
- path: z__default["default"].array(z__default["default"].string())
1292
- }).strict(), z__default["default"].object({
1293
- op: z__default["default"].literal("copy"),
1294
- from: z__default["default"].array(z__default["default"].string()),
1295
- path: z__default["default"].array(z__default["default"].string())
1296
- }).strict(), z__default["default"].object({
1297
- op: z__default["default"].literal("test"),
1298
- path: z__default["default"].array(z__default["default"].string()),
1299
- value: JSONValueT
1300
- }).strict(), z__default["default"].object({
1301
- op: z__default["default"].literal("file"),
1302
- path: z__default["default"].array(z__default["default"].string()),
1303
- filePath: z__default["default"].string(),
1304
- value: z__default["default"].union([z__default["default"].string(), z__default["default"].object({
1305
- sha256: z__default["default"].string(),
1306
- mimeType: z__default["default"].string()
1307
- })])
1308
- }).strict()]);
1309
- const Patch = z__default["default"].array(OperationT);
1310
-
1311
- function getValidationErrorFileRef(validationError) {
1312
- const maybeRef = validationError.value && typeof validationError.value === "object" && core.FILE_REF_PROP in validationError.value && typeof validationError.value[core.FILE_REF_PROP] === "string" ? validationError.value[core.FILE_REF_PROP] : undefined;
1313
- if (!maybeRef) {
1234
+ function decodeJwt(token, secretKey) {
1235
+ const [headerBase64, payloadBase64, signatureBase64, ...rest] = token.split(".");
1236
+ if (!headerBase64 || !payloadBase64 || !signatureBase64 || rest.length > 0) {
1237
+ console.debug("Invalid JWT: format is not exactly {header}.{payload}.{signature}", token);
1314
1238
  return null;
1315
1239
  }
1316
- return maybeRef;
1240
+ try {
1241
+ const parsedHeader = JSON.parse(Buffer.from(headerBase64, "base64").toString("utf8"));
1242
+ const headerVerification = JwtHeaderSchema.safeParse(parsedHeader);
1243
+ if (!headerVerification.success) {
1244
+ console.debug("Invalid JWT: invalid header", parsedHeader);
1245
+ return null;
1246
+ }
1247
+ if (headerVerification.data.typ !== jwtHeader.typ) {
1248
+ console.debug("Invalid JWT: invalid header typ", parsedHeader);
1249
+ return null;
1250
+ }
1251
+ if (headerVerification.data.alg !== jwtHeader.alg) {
1252
+ console.debug("Invalid JWT: invalid header alg", parsedHeader);
1253
+ return null;
1254
+ }
1255
+ } catch (err) {
1256
+ console.debug("Invalid JWT: could not parse header", err);
1257
+ return null;
1258
+ }
1259
+ if (secretKey) {
1260
+ const signature = crypto__default["default"].createHmac("sha256", secretKey).update(`${headerBase64}.${payloadBase64}`).digest("base64");
1261
+ if (signature !== signatureBase64) {
1262
+ console.debug("Invalid JWT: invalid signature");
1263
+ return null;
1264
+ }
1265
+ }
1266
+ try {
1267
+ const parsedPayload = JSON.parse(Buffer.from(payloadBase64, "base64").toString("utf8"));
1268
+ return parsedPayload;
1269
+ } catch (err) {
1270
+ console.debug("Invalid JWT: could not parse payload", err);
1271
+ return null;
1272
+ }
1273
+ }
1274
+ function getExpire() {
1275
+ return Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 4; // 4 days
1276
+ }
1277
+ const JwtHeaderSchema = z.z.object({
1278
+ alg: z.z.literal("HS256"),
1279
+ typ: z.z.literal("JWT")
1280
+ });
1281
+ const jwtHeader = {
1282
+ alg: "HS256",
1283
+ typ: "JWT"
1284
+ };
1285
+ const jwtHeaderBase64 = Buffer.from(JSON.stringify(jwtHeader)).toString("base64");
1286
+ function encodeJwt(payload, sessionKey) {
1287
+ // NOTE: this is only used for authentication, not for authorization (i.e. what a user can do) - this is handled when actually doing operations
1288
+ const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString("base64");
1289
+ return `${jwtHeaderBase64}.${payloadBase64}.${crypto__default["default"].createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
1317
1290
  }
1318
1291
 
1319
- const textEncoder$1 = new TextEncoder();
1292
+ const textEncoder$2 = new TextEncoder();
1320
1293
  async function extractImageMetadata(filename, input) {
1321
1294
  const imageSize = sizeOf__default["default"](input);
1322
1295
  let mimeType = null;
@@ -1350,7 +1323,7 @@ async function extractImageMetadata(filename, input) {
1350
1323
  };
1351
1324
  }
1352
1325
  function getSha256(mimeType, input) {
1353
- return core.Internal.getSHA256Hash(textEncoder$1.encode(
1326
+ return core.Internal.getSHA256Hash(textEncoder$2.encode(
1354
1327
  // TODO: we should probably store the mimetype in the metadata and reuse it here
1355
1328
  `data:${mimeType};base64,${input.toString("base64")}`));
1356
1329
  }
@@ -1366,1249 +1339,2004 @@ async function extractFileMetadata(filename, input) {
1366
1339
  };
1367
1340
  }
1368
1341
 
1369
- function validateMetadata(actualMetadata, expectedMetadata) {
1370
- const missingMetadata = [];
1371
- const erroneousMetadata = {};
1372
- if (typeof actualMetadata !== "object" || actualMetadata === null) {
1373
- return {
1374
- globalErrors: ["Metadata is wrong type: must be an object."]
1375
- };
1376
- }
1377
- if (Array.isArray(actualMetadata)) {
1378
- return {
1379
- globalErrors: ["Metadata is wrong type: cannot be an array."]
1380
- };
1381
- }
1382
- const recordMetadata = actualMetadata;
1383
- const globalErrors = [];
1384
- for (const anyKey in expectedMetadata) {
1385
- if (typeof anyKey !== "string") {
1386
- globalErrors.push(`Expected metadata has key '${anyKey}' that is not typeof 'string', but: '${typeof anyKey}'. This is most likely a Val bug.`);
1387
- } else {
1388
- if (anyKey in actualMetadata) {
1389
- const key = anyKey;
1390
- if (expectedMetadata[key] !== recordMetadata[key]) {
1391
- erroneousMetadata[key] = `Expected metadata '${key}' to be ${JSON.stringify(expectedMetadata[key])}, but got ${JSON.stringify(recordMetadata[key])}.`;
1392
- }
1393
- } else {
1394
- missingMetadata.push(anyKey);
1395
- }
1396
- }
1397
- }
1398
- if (globalErrors.length === 0 && missingMetadata.length === 0 && Object.keys(erroneousMetadata).length === 0) {
1399
- return false;
1400
- }
1401
- return {
1402
- missingMetadata,
1403
- erroneousMetadata
1404
- };
1405
- }
1342
+ /* eslint-disable @typescript-eslint/no-unused-vars */
1343
+ const textEncoder$1 = new TextEncoder();
1344
+ const jsonOps = new patch.JSONOps();
1345
+ const tsOps = new TSOps(document => {
1346
+ return fp.pipe(analyzeValModule(document), fp.result.map(({
1347
+ source
1348
+ }) => source));
1349
+ });
1350
+ // #region ValOps
1351
+ class ValOps {
1352
+ /** Sources from val modules, immutable (without patches or anything) */
1406
1353
 
1407
- function getValidationErrorMetadata(validationError) {
1408
- const maybeMetadata = validationError.value && typeof validationError.value === "object" && "metadata" in validationError.value && validationError.value.metadata && validationError.value.metadata;
1409
- if (!maybeMetadata) {
1410
- return null;
1411
- }
1412
- return maybeMetadata;
1413
- }
1354
+ /** The sha265 / hash of sources + schema + config */
1414
1355
 
1415
- const ops = new patch.JSONOps();
1416
- class ValServer {
1417
- constructor(cwd, valModules, options, callbacks) {
1418
- this.cwd = cwd;
1356
+ /** Schema from val modules, immutable */
1357
+
1358
+ /** The sha265 / hash of schema + config - if this changes users needs to reload */
1359
+
1360
+ constructor(valModules, options) {
1419
1361
  this.valModules = valModules;
1420
1362
  this.options = options;
1421
- this.callbacks = callbacks;
1363
+ this.sources = null;
1364
+ this.baseSha = null;
1365
+ this.schemas = null;
1366
+ this.schemaSha = null;
1367
+ this.modulesErrors = null;
1368
+ }
1369
+ hash(input) {
1370
+ let str;
1371
+ if (typeof input === "string") {
1372
+ str = input;
1373
+ } else {
1374
+ str = JSON.stringify(input);
1375
+ }
1376
+ return core.Internal.getSHA256Hash(textEncoder$1.encode(str));
1422
1377
  }
1423
1378
 
1424
- /* Auth endpoints: */
1425
- async enable(query) {
1426
- const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
1427
- if (typeof redirectToRes !== "string") {
1428
- return redirectToRes;
1379
+ // #region initTree
1380
+ async initTree() {
1381
+ if (this.baseSha === null || this.schemaSha === null || this.sources === null || this.schemas === null || this.modulesErrors === null) {
1382
+ const currentModulesErrors = [];
1383
+ const addModuleError = (message, index, path) => {
1384
+ currentModulesErrors[index] = {
1385
+ message,
1386
+ path: path
1387
+ };
1388
+ };
1389
+ const currentSources = {};
1390
+ const currentSchemas = {};
1391
+ let baseSha = this.hash(JSON.stringify(this.valModules.config));
1392
+ let schemaSha = baseSha;
1393
+ for (let moduleIdx = 0; moduleIdx < this.valModules.modules.length; moduleIdx++) {
1394
+ const module = this.valModules.modules[moduleIdx];
1395
+ if (!module.def) {
1396
+ addModuleError("val.modules is missing 'def' property", moduleIdx);
1397
+ continue;
1398
+ }
1399
+ if (typeof module.def !== "function") {
1400
+ addModuleError("val.modules 'def' property is not a function", moduleIdx);
1401
+ continue;
1402
+ }
1403
+ await module.def().then(value => {
1404
+ if (!value) {
1405
+ addModuleError(`val.modules 'def' did not return a value`, moduleIdx);
1406
+ return;
1407
+ }
1408
+ if (!value.default) {
1409
+ addModuleError(`val.modules 'def' did not return a default export`, moduleIdx);
1410
+ return;
1411
+ }
1412
+ const path = core.Internal.getValPath(value.default);
1413
+ if (path === undefined) {
1414
+ addModuleError(`path is undefined`, moduleIdx);
1415
+ return;
1416
+ }
1417
+ const schema = core.Internal.getSchema(value.default);
1418
+ if (schema === undefined) {
1419
+ addModuleError(`schema in path '${path}' is undefined`, moduleIdx, path);
1420
+ return;
1421
+ }
1422
+ if (!(schema instanceof core.Schema)) {
1423
+ addModuleError(`schema in path '${path}' is not an instance of Schema`, moduleIdx, path);
1424
+ return;
1425
+ }
1426
+ if (typeof schema.serialize !== "function") {
1427
+ addModuleError(`schema.serialize in path '${path}' is not a function`, moduleIdx, path);
1428
+ return;
1429
+ }
1430
+ const source = core.Internal.getSource(value.default);
1431
+ if (source === undefined) {
1432
+ addModuleError(`source in ${path} is undefined`, moduleIdx, path);
1433
+ return;
1434
+ }
1435
+ const pathM = path;
1436
+ currentSources[pathM] = source;
1437
+ currentSchemas[pathM] = schema;
1438
+ // make sure the checks above is enough that this does not fail - even if val modules are not set up correctly
1439
+ baseSha += this.hash({
1440
+ path,
1441
+ schema: schema.serialize(),
1442
+ source,
1443
+ modulesErrors: currentModulesErrors
1444
+ });
1445
+ schemaSha += this.hash(schema.serialize());
1446
+ });
1447
+ }
1448
+ this.sources = currentSources;
1449
+ this.schemas = currentSchemas;
1450
+ this.baseSha = baseSha;
1451
+ this.schemaSha = schemaSha;
1452
+ this.modulesErrors = currentModulesErrors;
1429
1453
  }
1430
- await this.callbacks.onEnable(true);
1431
1454
  return {
1432
- cookies: {
1433
- [internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
1434
- },
1435
- status: 302,
1436
- redirectTo: redirectToRes
1455
+ baseSha: this.baseSha,
1456
+ schemaSha: this.schemaSha,
1457
+ sources: this.sources,
1458
+ schemas: this.schemas,
1459
+ moduleErrors: this.modulesErrors
1437
1460
  };
1438
1461
  }
1439
- async disable(query) {
1440
- const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
1441
- if (typeof redirectToRes !== "string") {
1442
- return redirectToRes;
1443
- }
1444
- await this.callbacks.onDisable(true);
1445
- return {
1446
- cookies: {
1447
- [internal.VAL_ENABLE_COOKIE_NAME]: {
1448
- value: "false"
1449
- }
1450
- },
1451
- status: 302,
1452
- redirectTo: redirectToRes
1453
- };
1462
+ async init() {
1463
+ const {
1464
+ baseSha,
1465
+ schemaSha
1466
+ } = await this.initTree();
1467
+ await this.onInit(baseSha, schemaSha);
1454
1468
  }
1455
- async getTree(treePath,
1456
- // TODO: use the params: patch, schema, source now we return everything, every time
1457
- query, cookies, requestHeaders) {
1458
- const ensureRes = await debugTiming("ensureInitialized", () => this.ensureInitialized("getTree", cookies));
1459
- if (fp.result.isErr(ensureRes)) {
1460
- return ensureRes.error;
1461
- }
1462
- const applyPatches = query.patch === "true";
1463
- const includeSource = query.source === "true";
1464
- const includeSchema = query.schema === "true";
1465
- const moduleIds = await debugTiming("getAllModules", () => this.getAllModules(treePath));
1466
- let {
1467
- patchIdsByModuleId,
1468
- patchesById,
1469
- fileUpdates
1470
- } = {
1471
- patchIdsByModuleId: {},
1472
- patchesById: {},
1473
- fileUpdates: {}
1474
- };
1475
- if (applyPatches) {
1476
- const res = await debugTiming("readPatches", () => this.readPatches(cookies));
1477
- if (fp.result.isErr(res)) {
1478
- return res.error;
1479
- }
1480
- patchIdsByModuleId = res.value.patchIdsByModuleId;
1481
- patchesById = res.value.patchesById;
1482
- fileUpdates = res.value.fileUpdates;
1483
- }
1484
- const validate = false;
1485
- const possiblyPatchedContent = await debugTiming("applyAllPatchesThenValidate", () => Promise.all(moduleIds.map(async moduleId => {
1486
- return this.applyAllPatchesThenValidate(moduleId, patchIdsByModuleId, patchesById, fileUpdates, cookies, requestHeaders, applyPatches, validate, includeSource, includeSchema);
1487
- })));
1488
- const modules = Object.fromEntries(possiblyPatchedContent.map(serializedModuleContent => {
1489
- const module = {
1490
- schema: serializedModuleContent.schema,
1491
- source: serializedModuleContent.source,
1492
- errors: serializedModuleContent.errors
1493
- };
1494
- return [serializedModuleContent.path, module];
1495
- }));
1496
- const apiTreeResponse = {
1497
- modules,
1498
- git: this.options.git
1499
- };
1500
- return {
1501
- status: 200,
1502
- json: apiTreeResponse
1503
- };
1469
+ async getBaseSources() {
1470
+ return this.initTree().then(result => result.sources);
1504
1471
  }
1505
- async postValidate(rawBody, cookies, requestHeaders) {
1506
- const ensureRes = await this.ensureInitialized("postValidate", cookies);
1507
- if (fp.result.isErr(ensureRes)) {
1508
- return ensureRes.error;
1509
- }
1510
- return this.validateThenMaybeCommit(rawBody, false, cookies, requestHeaders);
1472
+ async getSchemas() {
1473
+ return this.initTree().then(result => result.schemas);
1511
1474
  }
1512
- async postCommit(rawBody, cookies, requestHeaders) {
1513
- const ensureRes = await this.ensureInitialized("postCommit", cookies);
1514
- if (fp.result.isErr(ensureRes)) {
1515
- return ensureRes.error;
1516
- }
1517
- const res = await this.validateThenMaybeCommit(rawBody, true, cookies, requestHeaders);
1518
- if (res.status === 200) {
1519
- if (res.json.validationErrors) {
1520
- return {
1521
- status: 400,
1522
- json: {
1523
- ...res.json
1524
- }
1525
- };
1526
- }
1527
- return {
1528
- status: 200,
1529
- json: {
1530
- ...res.json,
1531
- git: this.options.git
1532
- }
1533
- };
1534
- }
1535
- return res;
1475
+ async getModuleErrors() {
1476
+ return this.initTree().then(result => result.moduleErrors);
1477
+ }
1478
+ async getBaseSha() {
1479
+ return this.initTree().then(result => result.baseSha);
1480
+ }
1481
+ async getSchemaSha() {
1482
+ return this.initTree().then(result => result.schemaSha);
1536
1483
  }
1537
1484
 
1538
- /* */
1539
- async applyAllPatchesThenValidate(moduleId, patchIdsByModuleId, patchesById, fileUpdates, cookies, requestHeaders, applyPatches, validate, includeSource, includeSchema) {
1540
- const serializedModuleContent = await this.getModule(moduleId, {
1541
- source: includeSource,
1542
- schema: includeSchema
1543
- });
1544
- if (!applyPatches) {
1545
- return serializedModuleContent;
1546
- }
1547
- const schema = serializedModuleContent.schema;
1548
- const maybeSource = serializedModuleContent.source;
1549
- if (serializedModuleContent.errors && (serializedModuleContent.errors.fatal || serializedModuleContent.errors.invalidModuleId)) {
1550
- return serializedModuleContent;
1551
- }
1552
- if (!maybeSource || !schema) {
1553
- return serializedModuleContent;
1554
- }
1555
- let source = maybeSource;
1556
- await debugTiming("applyPatches:" + moduleId, async () => {
1557
- for (const patchId of patchIdsByModuleId[moduleId] ?? []) {
1558
- const patch$1 = patchesById[patchId];
1559
- if (!patch$1) {
1560
- continue;
1561
- }
1562
- const patchRes = patch.applyPatch(source, ops, patch$1.filter(core.Internal.notFileOp));
1563
- if (fp.result.isOk(patchRes)) {
1564
- source = patchRes.value;
1565
- } else {
1566
- console.error("Val: got an unexpected error while applying patch. Is there a mismatch in Val versions? Perhaps Val is misconfigured?", {
1567
- patchId,
1568
- moduleId,
1569
- patch: JSON.stringify(patch$1, null, 2),
1570
- error: patchRes.error
1571
- });
1572
- return {
1573
- path: moduleId,
1574
- schema,
1575
- source,
1576
- errors: {
1577
- fatal: [{
1578
- message: "Unexpected error applying patch",
1579
- type: "invalid-patch"
1580
- }]
1581
- }
1582
- };
1485
+ // #region analyzePatches
1486
+ analyzePatches(patchesById) {
1487
+ const patchesByModule = {};
1488
+ const fileLastUpdatedByPatchId = {};
1489
+ for (const [patchIdS, {
1490
+ path,
1491
+ patch,
1492
+ createdAt: created_at
1493
+ }] of Object.entries(patchesById)) {
1494
+ const patchId = patchIdS;
1495
+ for (const op of patch) {
1496
+ if (op.op === "file") {
1497
+ fileLastUpdatedByPatchId[op.filePath] = patchId;
1583
1498
  }
1584
1499
  }
1585
- });
1586
- if (validate) {
1587
- const validationErrors = await debugTiming("validate:" + moduleId, async () => core.deserializeSchema(schema).validate(moduleId, source));
1588
- if (validationErrors) {
1589
- const revalidated = await debugTiming("revalidate image/file:" + moduleId, async () => this.revalidateImageAndFileValidation(validationErrors, fileUpdates, cookies, requestHeaders));
1590
- return {
1591
- path: moduleId,
1592
- schema,
1593
- source,
1594
- errors: revalidated && {
1595
- validation: revalidated
1596
- }
1597
- };
1500
+ if (!patchesByModule[path]) {
1501
+ patchesByModule[path] = [];
1598
1502
  }
1503
+ patchesByModule[path].push({
1504
+ patchId,
1505
+ createdAt: created_at
1506
+ });
1507
+ }
1508
+ for (const path in patchesByModule) {
1509
+ patchesByModule[path].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1599
1510
  }
1600
1511
  return {
1601
- path: moduleId,
1602
- schema,
1603
- source,
1604
- errors: false
1512
+ patchesByModule,
1513
+ fileLastUpdatedByPatchId
1605
1514
  };
1606
1515
  }
1607
- // TODO: name this better: we need to check for image and file validation errors
1608
- // since they cannot be handled directly inside the validation function.
1609
- // The reason is that validate will be called inside QuickJS (in the future, hopefully),
1610
- // which does not have access to the filesystem, at least not at the time of writing this comment.
1611
- // If you are reading this, and we still are not using QuickJS to validate, this assumption might be wrong.
1612
- async revalidateImageAndFileValidation(validationErrors, fileUpdates, cookies, reqHeaders) {
1613
- const revalidatedValidationErrors = {};
1614
- for (const pathStr in validationErrors) {
1615
- const errorSourcePath = pathStr;
1616
- const errors = validationErrors[errorSourcePath];
1617
- revalidatedValidationErrors[errorSourcePath] = [];
1618
- for (const error of errors) {
1619
- var _error$fixes;
1620
- if ((_error$fixes = error.fixes) !== null && _error$fixes !== void 0 && _error$fixes.every(fix => fix === "file:check-metadata" || fix === "image:replace-metadata" // TODO: rename fix to: image:check-metadata
1621
- )) {
1622
- const fileRef = getValidationErrorFileRef(error);
1623
- if (fileRef) {
1624
- var _fileUpdates$fileRef;
1625
- const filePath = path__namespace["default"].join(this.cwd, fileRef);
1626
- let expectedMetadata = await this.getMetadata(fileRef, (_fileUpdates$fileRef = fileUpdates[fileRef]) === null || _fileUpdates$fileRef === void 0 ? void 0 : _fileUpdates$fileRef.sha256);
1627
-
1628
- // if this is a new file or we have an actual FS, we read the file and get the metadata
1629
- if (!expectedMetadata) {
1630
- let fileBuffer = undefined;
1631
- const updatedFileMetadata = fileUpdates[fileRef];
1632
- if (updatedFileMetadata) {
1633
- const fileRes = await this.getFiles(fileRef, {
1634
- sha256: updatedFileMetadata.sha256
1635
- }, cookies, reqHeaders);
1636
- if (fileRes.status === 200 && fileRes.body) {
1637
- const res = new Response(fileRes.body);
1638
- fileBuffer = Buffer.from(await res.arrayBuffer());
1639
- } else {
1640
- console.error("Val: unexpected error while fetching image / file:", fileRef, {
1641
- error: fileRes
1642
- });
1643
- }
1644
- }
1645
- // try fetch file directly via http
1646
- if (fileRef.startsWith("/public")) {
1647
- const host = `${reqHeaders["x-forwarded-proto"]}://${reqHeaders["host"]}`;
1648
- const fileUrl = fileRef.slice("/public".length);
1649
- const fetchRes = await fetch(new URL(fileUrl, host));
1650
- if (fetchRes.status === 200) {
1651
- fileBuffer = Buffer.from(await fetchRes.arrayBuffer());
1652
- } else {
1653
- console.error("Val: unexpected error while fetching image / file:", fileRef, {
1654
- error: {
1655
- status: fetchRes.status,
1656
- url: fetchRes.url
1657
- }
1658
- });
1659
- }
1660
- } else {
1661
- console.error("Val: unexpected while getting public image / file (file reference did not start with /public)", fileRef);
1662
- }
1663
- if (!fileBuffer) {
1664
- revalidatedValidationErrors[errorSourcePath].push({
1665
- message: `Could not read file: ${filePath}`
1666
- });
1667
- continue;
1668
- }
1669
- if (error.fixes.some(fix => fix === "image:replace-metadata")) {
1670
- expectedMetadata = await extractImageMetadata(filePath, fileBuffer);
1671
- } else {
1672
- expectedMetadata = await extractFileMetadata(filePath, fileBuffer);
1673
- }
1674
- }
1675
- if (!expectedMetadata) {
1676
- revalidatedValidationErrors[errorSourcePath].push({
1677
- message: `Could not read file metadata. Is the reference to the file: ${fileRef} correct?`
1678
- });
1516
+
1517
+ // #region getTree
1518
+ async getTree(analysis) {
1519
+ if (!analysis) {
1520
+ const {
1521
+ sources
1522
+ } = await this.initTree();
1523
+ return {
1524
+ sources,
1525
+ errors: {}
1526
+ };
1527
+ }
1528
+ const {
1529
+ sources
1530
+ } = await this.initTree();
1531
+ const patchedSources = {};
1532
+ const errors = {};
1533
+ for (const [pathS, patches] of Object.entries(analysis.patchesByModule)) {
1534
+ const path = pathS;
1535
+ if (!sources[path]) {
1536
+ if (!errors[path]) {
1537
+ errors[path] = [];
1538
+ }
1539
+ errors[path].push({
1540
+ invalidPath: true,
1541
+ error: new patch.PatchError(`Module at path: '${path}' not found`)
1542
+ });
1543
+ }
1544
+ const source = sources[path];
1545
+ for (const {
1546
+ patchId
1547
+ } of patches) {
1548
+ if (errors[path]) {
1549
+ errors[path].push({
1550
+ patchId: patchId,
1551
+ error: new patch.PatchError(`Cannot apply patch: previous errors exists`)
1552
+ });
1553
+ } else {
1554
+ const patchData = analysis.patches[patchId];
1555
+ if (!patchData) {
1556
+ errors[path].push({
1557
+ patchId: patchId,
1558
+ error: new patch.PatchError(`Patch not found`)
1559
+ });
1560
+ continue;
1561
+ }
1562
+ const applicableOps = [];
1563
+ const fileFixOps = {};
1564
+ for (const op of patchData.patch) {
1565
+ if (op.op === "file") {
1566
+ // NOTE: We insert the last patch_id that modify a file
1567
+ // when constructing the url we use the patch id (and the file path)
1568
+ // to fetch the right file
1569
+ // NOTE: overwrite and use last patch_id if multiple patches modify the same file
1570
+ fileFixOps[op.path.join("/")] = [{
1571
+ op: "add",
1572
+ path: op.path.concat("patch_id"),
1573
+ value: patchId
1574
+ }];
1679
1575
  } else {
1680
- const actualMetadata = getValidationErrorMetadata(error);
1681
- const revalidatedError = validateMetadata(actualMetadata, expectedMetadata);
1682
- if (!revalidatedError) {
1683
- // no errors anymore:
1684
- continue;
1685
- }
1686
- const errorMsgs = (revalidatedError.globalErrors || []).concat(Object.values(revalidatedError.erroneousMetadata || {})).concat(Object.values(revalidatedError.missingMetadata || []).map(missingKey => {
1687
- var _expectedMetadata;
1688
- return `Required key: '${missingKey}' is not defined. Should be: '${JSON.stringify((_expectedMetadata = expectedMetadata) === null || _expectedMetadata === void 0 ? void 0 : _expectedMetadata[missingKey])}'`;
1689
- }));
1690
- revalidatedValidationErrors[errorSourcePath].push(...errorMsgs.map(message => ({
1691
- message
1692
- })));
1576
+ applicableOps.push(op);
1693
1577
  }
1694
- } else {
1695
- revalidatedValidationErrors[errorSourcePath].push(error);
1696
1578
  }
1697
- } else {
1698
- revalidatedValidationErrors[errorSourcePath].push(error);
1699
- }
1700
- }
1701
- }
1702
- const hasErrors = Object.values(revalidatedValidationErrors).some(errors => errors.length > 0);
1703
- if (hasErrors) {
1704
- return revalidatedValidationErrors;
1705
- }
1706
- return hasErrors;
1707
- }
1708
- sortPatchIds(patchesByModule) {
1709
- return Object.values(patchesByModule).flatMap(modulePatches => modulePatches).sort((a, b) => {
1710
- return a.created_at.localeCompare(b.created_at);
1711
- }).map(patchData => patchData.patch_id);
1712
- }
1713
-
1714
- // can be overridden if FS cannot read from static assets / public folder (because of bundlers or what not)
1715
- async readStaticBinaryFile(filePath) {
1716
- return fs__default["default"].promises.readFile(filePath);
1717
- }
1718
- async readPatches(cookies) {
1719
- const res = await this.getPatches({},
1720
- // {} means no ids, so get all patches
1721
- cookies);
1722
- if (res.status === 400 || res.status === 401 || res.status === 403 || res.status === 404 || res.status === 500 || res.status === 501) {
1723
- return fp.result.err(res);
1724
- } else if (res.status === 200 || res.status === 201) {
1725
- const patchesByModule = res.json;
1726
- const patches = [];
1727
- const patchIdsByModuleId = {};
1728
- const patchesById = {};
1729
- for (const [moduleIdS, modulePatchData] of Object.entries(patchesByModule)) {
1730
- const moduleId = moduleIdS;
1731
- patchIdsByModuleId[moduleId] = modulePatchData.map(patch => patch.patch_id);
1732
- for (const patchData of modulePatchData) {
1733
- patches.push([patchData.patch_id, moduleId, patchData.patch]);
1734
- patchesById[patchData.patch_id] = patchData.patch;
1735
- }
1736
- }
1737
- const fileUpdates = {};
1738
- const sortedPatchIds = this.sortPatchIds(patchesByModule);
1739
- for (const sortedPatchId of sortedPatchIds) {
1740
- const patchId = sortedPatchId;
1741
- for (const op of patchesById[patchId] || []) {
1742
- if (op.op === "file") {
1743
- const parsedFileOp = z.z.object({
1744
- sha256: z.z.string(),
1745
- mimeType: z.z.string()
1746
- }).safeParse(op.value);
1747
- if (!parsedFileOp.success) {
1748
- return fp.result.err({
1749
- status: 500,
1750
- json: {
1751
- message: "Unexpected error: file op value must be transformed into object",
1752
- details: {
1753
- value: "First 200 chars: " + JSON.stringify(op.value).slice(0, 200),
1754
- patchId
1755
- }
1756
- }
1757
- });
1579
+ const patchRes = patch.applyPatch(source, jsonOps, applicableOps.concat(...Object.values(fileFixOps)));
1580
+ if (fp.result.isErr(patchRes)) {
1581
+ if (!errors[path]) {
1582
+ errors[path] = [];
1758
1583
  }
1759
- fileUpdates[op.filePath] = {
1760
- ...parsedFileOp.data
1761
- };
1584
+ errors[path].push({
1585
+ patchId: patchId,
1586
+ error: patchRes.error
1587
+ });
1588
+ } else {
1589
+ patchedSources[path] = patchRes.value;
1762
1590
  }
1763
1591
  }
1764
1592
  }
1765
- return fp.result.ok({
1766
- patches,
1767
- patchIdsByModuleId,
1768
- patchesById,
1769
- fileUpdates
1770
- });
1771
- } else {
1772
- return fp.result.err({
1773
- status: 500,
1774
- json: {
1775
- message: "Unknown error"
1776
- }
1777
- });
1778
1593
  }
1594
+ return {
1595
+ sources: patchedSources,
1596
+ errors
1597
+ };
1779
1598
  }
1780
- async validateThenMaybeCommit(rawBody, commit, cookies, requestHeaders) {
1781
- const filterPatchesByModuleIdRes = z.z.object({
1782
- patches: z.z.record(z.z.array(z.z.string())).optional()
1783
- }).safeParse(rawBody);
1784
- if (!filterPatchesByModuleIdRes.success) {
1785
- return {
1786
- status: 404,
1787
- json: {
1788
- message: "Could not parse body",
1789
- details: filterPatchesByModuleIdRes.error
1790
- }
1791
- };
1792
- }
1793
- const res = await this.readPatches(cookies);
1794
- if (fp.result.isErr(res)) {
1795
- return res.error;
1796
- }
1797
- const {
1798
- patchIdsByModuleId,
1799
- patchesById,
1800
- patches,
1801
- fileUpdates
1802
- } = res.value;
1803
- const validationErrorsByModuleId = {};
1804
- for (const moduleIdStr of await this.getAllModules("/")) {
1805
- const moduleId = moduleIdStr;
1806
- const serializedModuleContent = await this.applyAllPatchesThenValidate(moduleId, filterPatchesByModuleIdRes.data.patches ||
1807
- // TODO: refine to ModuleId and PatchId when parsing
1808
- patchIdsByModuleId, patchesById, fileUpdates, cookies, requestHeaders, true, true, true, commit);
1809
- if (serializedModuleContent.errors) {
1810
- validationErrorsByModuleId[moduleId] = serializedModuleContent;
1811
- }
1812
- }
1813
- if (Object.keys(validationErrorsByModuleId).length > 0) {
1814
- const modules = {};
1815
- for (const [patchId, moduleId] of patches) {
1816
- if (!modules[moduleId]) {
1817
- modules[moduleId] = {
1818
- patches: {
1819
- applied: []
1820
- }
1599
+
1600
+ // #region validateSources
1601
+ async validateSources(schemas, sources, patchesByModule) {
1602
+ const errors = {};
1603
+ const files = {};
1604
+ const entries = Object.entries(schemas);
1605
+ const modulePathsToValidate = patchesByModule && Object.keys(patchesByModule);
1606
+ for (const [pathS, schema] of entries) {
1607
+ if (modulePathsToValidate && !modulePathsToValidate.includes(pathS)) {
1608
+ continue;
1609
+ }
1610
+ const path = pathS;
1611
+ const source = sources[path];
1612
+ if (source === undefined) {
1613
+ if (!errors[path]) {
1614
+ errors[path] = {
1615
+ validations: {}
1821
1616
  };
1822
1617
  }
1823
- if (validationErrorsByModuleId[moduleId]) {
1824
- var _modules$moduleId$pat;
1825
- if (!modules[moduleId].patches.failed) {
1826
- modules[moduleId].patches.failed = [];
1618
+ errors[path] = {
1619
+ ...errors[path],
1620
+ invalidSource: {
1621
+ message: `Module at path: '${path}' does not exist`
1827
1622
  }
1828
- (_modules$moduleId$pat = modules[moduleId].patches.failed) === null || _modules$moduleId$pat === void 0 || _modules$moduleId$pat.push(patchId);
1829
- } else {
1830
- modules[moduleId].patches.applied.push(patchId);
1831
- }
1623
+ };
1624
+ continue;
1832
1625
  }
1833
- return {
1834
- status: 200,
1835
- json: {
1836
- modules,
1837
- validationErrors: validationErrorsByModuleId
1626
+ const res = schema.validate(path, source);
1627
+ if (res === false) {
1628
+ continue;
1629
+ }
1630
+ for (const [sourcePathS, validationErrors] of Object.entries(res)) {
1631
+ const sourcePath = sourcePathS;
1632
+ for (const validationError of validationErrors) {
1633
+ if (isOnlyFileCheckValidationError(validationError)) {
1634
+ if (files[sourcePath]) {
1635
+ throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
1636
+ }
1637
+ const value = validationError.value;
1638
+ if (isFileSource(value)) {
1639
+ files[sourcePath] = value;
1640
+ }
1641
+ } else {
1642
+ if (!errors[path]) {
1643
+ errors[path] = {
1644
+ validations: {}
1645
+ };
1646
+ }
1647
+ if (!errors[path].validations[sourcePath]) {
1648
+ errors[path].validations[sourcePath] = [];
1649
+ }
1650
+ errors[path].validations[sourcePath].push(validationError);
1651
+ }
1838
1652
  }
1839
- };
1840
- }
1841
- let modules;
1842
- if (commit) {
1843
- const commitRes = await this.execCommit(patches, cookies);
1844
- if (commitRes.status !== 200) {
1845
- return commitRes;
1846
1653
  }
1847
- modules = commitRes.json;
1848
- } else {
1849
- modules = await this.getPatchedModules(patches);
1850
1654
  }
1851
1655
  return {
1852
- status: 200,
1853
- json: {
1854
- modules,
1855
- validationErrors: false
1656
+ errors,
1657
+ files
1658
+ };
1659
+ }
1660
+
1661
+ // #region validateFiles
1662
+ async validateFiles(schemas, sources, files, fileLastUpdatedByPatchId) {
1663
+ const validateFileAtSourcePath = async (sourcePath, value) => {
1664
+ const [fullModulePath, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
1665
+ const schema = schemas[fullModulePath];
1666
+ if (!schema) {
1667
+ return {
1668
+ [sourcePath]: [{
1669
+ message: `Schema not found for path: '${fullModulePath}'`,
1670
+ value
1671
+ }]
1672
+ };
1673
+ }
1674
+ const source = sources[fullModulePath];
1675
+ if (!source) {
1676
+ return {
1677
+ [sourcePath]: [{
1678
+ message: `Source not found for path: '${fullModulePath}'`,
1679
+ value
1680
+ }]
1681
+ };
1682
+ }
1683
+ let schemaAtPath;
1684
+ try {
1685
+ const {
1686
+ schema: resolvedSchema
1687
+ } = core.Internal.resolvePath(modulePath, sources[fullModulePath], schemas[fullModulePath]);
1688
+ schemaAtPath = resolvedSchema;
1689
+ } catch (e) {
1690
+ if (e instanceof Error) {
1691
+ return {
1692
+ [sourcePath]: [{
1693
+ message: `Could not resolve schema at path: ${modulePath}. Error: ${e.message}`,
1694
+ value
1695
+ }]
1696
+ };
1697
+ }
1698
+ return {
1699
+ [sourcePath]: [{
1700
+ message: `Could not resolve schema at path: ${modulePath}. Unknown error.`,
1701
+ value
1702
+ }]
1703
+ };
1704
+ }
1705
+ const type = schemaAtPath instanceof core.ImageSchema ? "image" : "file";
1706
+ const filePath = value[core.FILE_REF_PROP];
1707
+ const patchId = (fileLastUpdatedByPatchId === null || fileLastUpdatedByPatchId === void 0 ? void 0 : fileLastUpdatedByPatchId[filePath]) || null;
1708
+ let metadata;
1709
+ let metadataErrors;
1710
+
1711
+ // TODO: refactor so we call get metadata once instead of iterating like this. Reason: should be a lot faster
1712
+ if (patchId) {
1713
+ const patchFileMetadata = await this.getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId);
1714
+ if (patchFileMetadata.errors) {
1715
+ metadataErrors = patchFileMetadata.errors;
1716
+ } else {
1717
+ metadata = patchFileMetadata.metadata;
1718
+ }
1719
+ } else {
1720
+ const patchFileMetadata = await this.getBinaryFileMetadata(filePath, type);
1721
+ if (patchFileMetadata.errors) {
1722
+ metadataErrors = patchFileMetadata.errors;
1723
+ } else {
1724
+ metadata = patchFileMetadata.metadata;
1725
+ }
1726
+ }
1727
+ if (metadataErrors && metadataErrors.length > 0) {
1728
+ return {
1729
+ [sourcePath]: metadataErrors.map(e => ({
1730
+ message: e.message,
1731
+ value: {
1732
+ filePath,
1733
+ patchId
1734
+ }
1735
+ }))
1736
+ };
1737
+ }
1738
+ if (!metadata) {
1739
+ return {
1740
+ [sourcePath]: [{
1741
+ message: "Unexpectedly got no metadata",
1742
+ value: {
1743
+ filePath
1744
+ }
1745
+ }]
1746
+ };
1747
+ }
1748
+ const metadataSourcePath = core.Internal.createValPathOfItem(sourcePath, "metadata");
1749
+ if (!metadataSourcePath) {
1750
+ throw new Error("Could not create metadata path");
1751
+ }
1752
+ const currentValueMetadata = value.metadata;
1753
+ if (!currentValueMetadata) {
1754
+ return {
1755
+ [metadataSourcePath]: [{
1756
+ message: "Missing metadata field: 'metadata'",
1757
+ value
1758
+ }]
1759
+ };
1856
1760
  }
1761
+ const fieldErrors = {};
1762
+ for (const field of getFieldsForType(type)) {
1763
+ const fieldMetadata = metadata[field];
1764
+ const fieldSourcePath = core.Internal.createValPathOfItem(metadataSourcePath, field);
1765
+ if (!fieldSourcePath) {
1766
+ throw new Error("Could not create field path");
1767
+ }
1768
+ if (!(field in currentValueMetadata)) {
1769
+ return {
1770
+ [fieldSourcePath]: [{
1771
+ message: `Missing metadata field: '${field}'`,
1772
+ value
1773
+ }]
1774
+ };
1775
+ }
1776
+ if (fieldMetadata !== currentValueMetadata[field]) {
1777
+ fieldErrors[fieldSourcePath] = [{
1778
+ message: `Metadata field '${field}' of value: ${JSON.stringify(currentValueMetadata[field])} does not match expected value: ${JSON.stringify(fieldMetadata)}`,
1779
+ value: {
1780
+ actual: currentValueMetadata[field],
1781
+ expected: fieldMetadata
1782
+ },
1783
+ fixes: ["image:replace-metadata"]
1784
+ }];
1785
+ }
1786
+ }
1787
+ return fieldErrors;
1857
1788
  };
1789
+ const allErrors = (await Promise.all(Object.entries(files).map(([sourcePathS, value]) => validateFileAtSourcePath(sourcePathS, value).then(res => {
1790
+ if (res) {
1791
+ return Object.entries(res);
1792
+ } else {
1793
+ return [];
1794
+ }
1795
+ })))).flat();
1796
+ return Object.fromEntries(allErrors);
1858
1797
  }
1859
- async getPatchedModules(patches) {
1860
- const modules = {};
1861
- for (const [patchId, moduleId] of patches) {
1862
- if (!modules[moduleId]) {
1863
- modules[moduleId] = {
1864
- patches: {
1865
- applied: []
1798
+
1799
+ // #region prepareCommit
1800
+ async prepare(patchAnalysis) {
1801
+ const {
1802
+ patchesByModule,
1803
+ fileLastUpdatedByPatchId
1804
+ } = patchAnalysis;
1805
+ const applySourceFilePatches = async (path, patches) => {
1806
+ const sourceFileRes = await this.getSourceFile(path);
1807
+ const errors = [];
1808
+ if (sourceFileRes.error) {
1809
+ errors.push({
1810
+ message: sourceFileRes.error.message,
1811
+ filePath: path
1812
+ });
1813
+ return {
1814
+ path,
1815
+ errors,
1816
+ skippedPatches: patches.map(p => p.patchId)
1817
+ };
1818
+ }
1819
+ const sourceFile = sourceFileRes.data;
1820
+ let tsSourceFile = ts__default["default"].createSourceFile("<val>", sourceFile, ts__default["default"].ScriptTarget.ES2015);
1821
+ const appliedPatches = [];
1822
+ const triedPatches = [];
1823
+ for (const {
1824
+ patchId
1825
+ } of patches) {
1826
+ var _patchAnalysis$patche;
1827
+ const patch$1 = (_patchAnalysis$patche = patchAnalysis.patches) === null || _patchAnalysis$patche === void 0 || (_patchAnalysis$patche = _patchAnalysis$patche[patchId]) === null || _patchAnalysis$patche === void 0 ? void 0 : _patchAnalysis$patche.patch;
1828
+ if (!patch$1) {
1829
+ errors.push({
1830
+ message: `Analysis required non-existing patch: ${patchId}`
1831
+ });
1832
+ break;
1833
+ }
1834
+ const sourceFileOps = patch$1.filter(op => op.op !== "file"); // file is not a valid source file op
1835
+ const patchRes = patch.applyPatch(tsSourceFile, tsOps, sourceFileOps);
1836
+ if (fp.result.isErr(patchRes)) {
1837
+ if (Array.isArray(patchRes.error)) {
1838
+ errors.push(...patchRes.error);
1839
+ } else {
1840
+ errors.push(patchRes.error);
1841
+ }
1842
+ triedPatches.push(patchId);
1843
+ break;
1844
+ }
1845
+ appliedPatches.push(patchId);
1846
+ tsSourceFile = patchRes.value;
1847
+ }
1848
+ if (errors.length === 0) {
1849
+ var _this$options;
1850
+ let sourceFileText = tsSourceFile.getText(tsSourceFile);
1851
+ if ((_this$options = this.options) !== null && _this$options !== void 0 && _this$options.formatter) {
1852
+ try {
1853
+ sourceFileText = this.options.formatter(sourceFileText, path);
1854
+ } catch (err) {
1855
+ errors.push({
1856
+ message: "Could not format source file: " + (err instanceof Error ? err.message : "Unknown error")
1857
+ });
1866
1858
  }
1859
+ }
1860
+ return {
1861
+ path,
1862
+ appliedPatches,
1863
+ result: sourceFileText
1864
+ };
1865
+ } else {
1866
+ const skippedPatches = patches.slice(appliedPatches.length + triedPatches.length).map(p => p.patchId);
1867
+ return {
1868
+ path,
1869
+ appliedPatches,
1870
+ triedPatches,
1871
+ skippedPatches,
1872
+ errors
1867
1873
  };
1868
1874
  }
1869
- modules[moduleId].patches.applied.push(patchId);
1875
+ };
1876
+ const allResults = await Promise.all(Object.entries(patchesByModule).map(([path, patches]) => applySourceFilePatches(path, patches)));
1877
+ let hasErrors = false;
1878
+ const sourceFilePatchErrors = {};
1879
+ const appliedPatches = {};
1880
+ const triedPatches = {};
1881
+ const skippedPatches = {};
1882
+ const patchedSourceFiles = {};
1883
+
1884
+ //
1885
+ const globalAppliedPatches = [];
1886
+ for (const res of allResults) {
1887
+ if (res.errors) {
1888
+ hasErrors = true;
1889
+ sourceFilePatchErrors[res.path] = res.errors;
1890
+ appliedPatches[res.path] = res.appliedPatches ?? [];
1891
+ triedPatches[res.path] = res.triedPatches ?? [];
1892
+ skippedPatches[res.path] = res.skippedPatches ?? [];
1893
+ } else {
1894
+ patchedSourceFiles[res.path] = res.result;
1895
+ appliedPatches[res.path] = res.appliedPatches ?? [];
1896
+ }
1897
+ for (const patchId of res.appliedPatches ?? []) {
1898
+ globalAppliedPatches.push(patchId);
1899
+ }
1870
1900
  }
1871
- return modules;
1901
+ const patchedBinaryFilesDescriptors = {};
1902
+ const binaryFilePatchErrors = {};
1903
+ await Promise.all(Object.entries(fileLastUpdatedByPatchId).map(async ([filePath, patchId]) => {
1904
+ if (globalAppliedPatches.includes(patchId)) {
1905
+ // 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)
1906
+ // 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
1907
+ // or is that the case? We are picking the latest file by path so, that should be enough?
1908
+ patchedBinaryFilesDescriptors[filePath] = {
1909
+ patchId
1910
+ };
1911
+ } else {
1912
+ hasErrors = true;
1913
+ binaryFilePatchErrors[filePath] = {
1914
+ message: "Patch not applied"
1915
+ };
1916
+ }
1917
+ }));
1918
+ const res = {
1919
+ hasErrors,
1920
+ sourceFilePatchErrors,
1921
+ binaryFilePatchErrors,
1922
+ patchedSourceFiles,
1923
+ patchedBinaryFilesDescriptors,
1924
+ appliedPatches,
1925
+ skippedPatches,
1926
+ triedPatches
1927
+ };
1928
+ return res;
1872
1929
  }
1873
1930
 
1874
- /* Abstract methods */
1875
-
1876
- /**
1877
- * Runs before remoteFS dependent methods (e.g.getModule, ...) are called to make sure that:
1878
- * 1) The remote FS, if applicable, is initialized
1879
- * 2) The error is returned via API if the remote FS could not be initialized
1880
- * */
1881
-
1882
- /* Abstract endpoints */
1883
-
1884
- /* Abstract auth endpoints: */
1885
-
1886
- /* Abstract patch endpoints: */
1887
- }
1888
- const chunkSize = 1024 * 1024;
1889
- function bufferToReadableStream(buffer) {
1890
- const stream = new ReadableStream({
1891
- start(controller) {
1892
- let offset = 0;
1893
- while (offset < buffer.length) {
1894
- const chunk = buffer.subarray(offset, offset + chunkSize);
1895
- controller.enqueue(chunk);
1896
- offset += chunkSize;
1931
+ // #region createPatch
1932
+ async createPatch(path, patch$1, authorId) {
1933
+ const {
1934
+ sources,
1935
+ schemas,
1936
+ moduleErrors
1937
+ } = await this.initTree();
1938
+ const source = sources[path];
1939
+ const schema = schemas[path];
1940
+ const moduleError = moduleErrors.find(e => e.path === path);
1941
+ if (moduleError) {
1942
+ return {
1943
+ error: {
1944
+ message: `Cannot patch. Module at path: '${path}' has fatal errors: ` + moduleErrors.map(m => `"${m.message}"`).join(" and ")
1945
+ }
1946
+ };
1947
+ }
1948
+ if (!source) {
1949
+ return {
1950
+ error: {
1951
+ message: `Cannot patch. Module source at path: '${path}' does not exist`
1952
+ }
1953
+ };
1954
+ }
1955
+ if (!schema) {
1956
+ return {
1957
+ error: {
1958
+ message: `Cannot patch. Module schema at path: '${path}' does not exist`
1959
+ }
1960
+ };
1961
+ }
1962
+ const sourceFileOps = [];
1963
+ const files = {};
1964
+ for (const op of patch$1) {
1965
+ if (op.op !== "file") {
1966
+ sourceFileOps.push(op);
1967
+ } else {
1968
+ const {
1969
+ value,
1970
+ filePath
1971
+ } = op;
1972
+ if (files[filePath]) {
1973
+ files[filePath] = {
1974
+ error: new patch.PatchError("Cannot have multiple files with same path in same patch")
1975
+ };
1976
+ } else if (typeof value !== "string") {
1977
+ files[filePath] = {
1978
+ error: new patch.PatchError("Value is not a string")
1979
+ };
1980
+ } else {
1981
+ const sha256 = core.Internal.getSHA256Hash(textEncoder$1.encode(value));
1982
+ files[filePath] = {
1983
+ value,
1984
+ sha256,
1985
+ path: op.path
1986
+ };
1987
+ sourceFileOps.push({
1988
+ op: "file",
1989
+ path: op.path,
1990
+ filePath,
1991
+ value: {
1992
+ sha256
1993
+ }
1994
+ });
1995
+ }
1897
1996
  }
1898
- controller.close();
1899
1997
  }
1900
- });
1901
- return stream;
1902
- }
1903
- const ENABLE_COOKIE_VALUE = {
1904
- value: "true",
1905
- options: {
1906
- httpOnly: false,
1907
- sameSite: "lax"
1908
- }
1909
- };
1910
- function getRedirectUrl(query, overrideHost) {
1911
- if (typeof query.redirect_to !== "string") {
1912
- return {
1913
- status: 400,
1914
- json: {
1915
- message: "Missing redirect_to query param"
1998
+ const saveRes = await this.saveSourceFilePatch(path, sourceFileOps, authorId);
1999
+ if (saveRes.error) {
2000
+ return {
2001
+ error: saveRes.error
2002
+ };
2003
+ }
2004
+ const patchId = saveRes.patchId;
2005
+ const saveFileRes = await Promise.all(Object.entries(files).map(async ([filePath, data]) => {
2006
+ if (data.error) {
2007
+ return {
2008
+ filePath,
2009
+ error: data.error
2010
+ };
2011
+ } else {
2012
+ var _lastRes;
2013
+ let type;
2014
+ const modulePath = core.Internal.patchPathToModulePath(data.path);
2015
+ try {
2016
+ const {
2017
+ schema: schemaAtPath
2018
+ } = core.Internal.resolvePath(modulePath, source, schema);
2019
+ type = schemaAtPath instanceof core.ImageSchema ? "image" : schemaAtPath instanceof core.FileSchema ? "file" : schema.serialize().type;
2020
+ } catch (e) {
2021
+ if (e instanceof Error) {
2022
+ return {
2023
+ filePath,
2024
+ error: new patch.PatchError(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`)
2025
+ };
2026
+ }
2027
+ return {
2028
+ filePath,
2029
+ error: new patch.PatchError(`Could not resolve file type at: ${modulePath}. Unknown error.`)
2030
+ };
2031
+ }
2032
+ if (type !== "image" && type !== "file") {
2033
+ return {
2034
+ filePath,
2035
+ error: new patch.PatchError("Unknown file type (resolved from schema): " + type)
2036
+ };
2037
+ }
2038
+ const mimeType = getMimeTypeFromBase64(data.value);
2039
+ if (!mimeType) {
2040
+ return {
2041
+ filePath,
2042
+ error: new patch.PatchError("Could not get mimeType from base 64 encoded value. First chars were: " + data.value.slice(0, 20))
2043
+ };
2044
+ }
2045
+ const buffer = bufferFromDataUrl(data.value);
2046
+ if (!buffer) {
2047
+ return {
2048
+ filePath,
2049
+ error: new patch.PatchError("Could not create buffer from base 64 encoded value")
2050
+ };
2051
+ }
2052
+ const metadataOps = createMetadataFromBuffer(type, mimeType, buffer);
2053
+ if (metadataOps.errors) {
2054
+ return {
2055
+ filePath,
2056
+ error: new patch.PatchError(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`)
2057
+ };
2058
+ }
2059
+ const MaxRetries = 3;
2060
+ let lastRes;
2061
+ for (let i = 0; i < MaxRetries; i++) {
2062
+ lastRes = await this.saveBase64EncodedBinaryFileFromPatch(filePath, patchId, data.value, type, metadataOps.metadata);
2063
+ if (!lastRes.error) {
2064
+ return {
2065
+ filePath
2066
+ };
2067
+ }
2068
+ }
2069
+ return {
2070
+ filePath,
2071
+ error: new patch.PatchError(((_lastRes = lastRes) === null || _lastRes === void 0 || (_lastRes = _lastRes.error) === null || _lastRes === void 0 ? void 0 : _lastRes.message) || "Unexpectedly could not save patch file")
2072
+ };
1916
2073
  }
2074
+ }));
2075
+ return {
2076
+ patchId,
2077
+ files: saveFileRes,
2078
+ createdAt: new Date().toISOString()
1917
2079
  };
1918
2080
  }
1919
- if (overrideHost) {
1920
- return overrideHost + "?redirect_to=" + encodeURIComponent(query.redirect_to);
2081
+
2082
+ // #region abstract ops
2083
+ }
2084
+ function isOnlyFileCheckValidationError(validationError) {
2085
+ var _validationError$fixe;
2086
+ if ((_validationError$fixe = validationError.fixes) !== null && _validationError$fixe !== void 0 && _validationError$fixe.every(f => f === "file:check-metadata" || f === "image:replace-metadata")) {
2087
+ return true;
1921
2088
  }
1922
- return query.redirect_to;
2089
+ return false;
1923
2090
  }
1924
- const base64DataAttr = "data:";
1925
- function getMimeTypeFromBase64(content) {
1926
- const dataIndex = content.indexOf(base64DataAttr);
1927
- const base64Index = content.indexOf(";base64,");
1928
- if (dataIndex > -1 || base64Index > -1) {
1929
- const mimeType = content.slice(dataIndex + base64DataAttr.length, base64Index);
1930
- return mimeType;
2091
+ function isFileSource(value) {
2092
+ if (typeof value === "object" && value !== null && core.FILE_REF_PROP in value && core.VAL_EXTENSION in value && value[core.VAL_EXTENSION] === "file" && core.FILE_REF_PROP) {
2093
+ return true;
1931
2094
  }
1932
- return null;
2095
+ return false;
1933
2096
  }
1934
- function bufferFromDataUrl(dataUrl, contentType) {
1935
- let base64Data;
1936
- if (!contentType) {
1937
- const base64Index = dataUrl.indexOf(";base64,");
1938
- if (base64Index > -1) {
1939
- base64Data = dataUrl.slice(base64Index + ";base64,".length);
2097
+ function getFieldsForType(type) {
2098
+ if (type === "file") {
2099
+ return ["sha256", "mimeType"];
2100
+ } else if (type === "image") {
2101
+ return ["sha256", "mimeType", "height", "width"];
2102
+ }
2103
+ throw new Error("Unknown type: " + type);
2104
+ }
2105
+ function createMetadataFromBuffer(type, mimeType, buffer) {
2106
+ const sha256 = getSha256(mimeType, buffer);
2107
+ const errors = [];
2108
+ let availableMetadata;
2109
+ if (type === "image") {
2110
+ const {
2111
+ width,
2112
+ height,
2113
+ type
2114
+ } = sizeOf__default["default"](buffer);
2115
+ const normalizedType = type === "jpg" ? "jpeg" : type;
2116
+ if (type !== undefined && `image/${normalizedType}` !== mimeType) {
2117
+ return {
2118
+ errors: [{
2119
+ message: `Mime type does not match image type: ${mimeType} vs ${type}`
2120
+ }]
2121
+ };
1940
2122
  }
2123
+ availableMetadata = {
2124
+ sha256: sha256,
2125
+ mimeType,
2126
+ height,
2127
+ width
2128
+ };
1941
2129
  } else {
1942
- const dataUrlEncodingHeader = `${base64DataAttr}${contentType};base64,`;
1943
- if (dataUrl.slice(0, dataUrlEncodingHeader.length) === dataUrlEncodingHeader) {
1944
- base64Data = dataUrl.slice(dataUrlEncodingHeader.length);
2130
+ availableMetadata = {
2131
+ sha256: sha256,
2132
+ mimeType
2133
+ };
2134
+ }
2135
+ const metadata = {};
2136
+ for (const field of getFieldsForType(type)) {
2137
+ const foundFieldData = field in availableMetadata ? availableMetadata[field] : null;
2138
+ if (foundFieldData !== undefined && foundFieldData !== null) {
2139
+ metadata[field] = foundFieldData;
2140
+ } else {
2141
+ errors.push({
2142
+ message: `Field not found: '${field}'`,
2143
+ field
2144
+ });
1945
2145
  }
1946
2146
  }
1947
- if (base64Data) {
1948
- return Buffer.from(base64Data, "base64" // TODO: why does it not work with base64url?
1949
- );
2147
+ if (errors.length > 0) {
2148
+ return {
2149
+ errors
2150
+ };
1950
2151
  }
2152
+ return {
2153
+ metadata
2154
+ };
1951
2155
  }
1952
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
1953
- const COMMON_MIME_TYPES = {
1954
- aac: "audio/aac",
1955
- abw: "application/x-abiword",
1956
- arc: "application/x-freearc",
1957
- avif: "image/avif",
1958
- avi: "video/x-msvideo",
1959
- azw: "application/vnd.amazon.ebook",
1960
- bin: "application/octet-stream",
1961
- bmp: "image/bmp",
1962
- bz: "application/x-bzip",
1963
- bz2: "application/x-bzip2",
1964
- cda: "application/x-cdf",
1965
- csh: "application/x-csh",
1966
- css: "text/css",
1967
- csv: "text/csv",
1968
- doc: "application/msword",
1969
- docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1970
- eot: "application/vnd.ms-fontobject",
1971
- epub: "application/epub+zip",
1972
- gz: "application/gzip",
1973
- gif: "image/gif",
1974
- htm: "text/html",
1975
- html: "text/html",
1976
- ico: "image/vnd.microsoft.icon",
1977
- ics: "text/calendar",
1978
- jar: "application/java-archive",
1979
- jpeg: "image/jpeg",
1980
- jpg: "image/jpeg",
1981
- js: "text/javascript",
1982
- json: "application/json",
1983
- jsonld: "application/ld+json",
1984
- mid: "audio/midi",
1985
- midi: "audio/midi",
1986
- mjs: "text/javascript",
1987
- mp3: "audio/mpeg",
1988
- mp4: "video/mp4",
1989
- mpeg: "video/mpeg",
1990
- mpkg: "application/vnd.apple.installer+xml",
1991
- odp: "application/vnd.oasis.opendocument.presentation",
1992
- ods: "application/vnd.oasis.opendocument.spreadsheet",
1993
- odt: "application/vnd.oasis.opendocument.text",
1994
- oga: "audio/ogg",
1995
- ogv: "video/ogg",
1996
- ogx: "application/ogg",
1997
- opus: "audio/opus",
1998
- otf: "font/otf",
1999
- png: "image/png",
2000
- pdf: "application/pdf",
2001
- php: "application/x-httpd-php",
2002
- ppt: "application/vnd.ms-powerpoint",
2003
- pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
2004
- rar: "application/vnd.rar",
2005
- rtf: "application/rtf",
2006
- sh: "application/x-sh",
2007
- svg: "image/svg+xml",
2008
- tar: "application/x-tar",
2009
- tif: "image/tiff",
2010
- tiff: "image/tiff",
2011
- ts: "video/mp2t",
2012
- ttf: "font/ttf",
2013
- txt: "text/plain",
2014
- vsd: "application/vnd.visio",
2015
- wav: "audio/wav",
2016
- weba: "audio/webm",
2017
- webm: "video/webm",
2018
- webp: "image/webp",
2019
- woff: "font/woff",
2020
- woff2: "font/woff2",
2021
- xhtml: "application/xhtml+xml",
2022
- xls: "application/vnd.ms-excel",
2023
- xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
2024
- xml: "application/xml",
2025
- xul: "application/vnd.mozilla.xul+xml",
2026
- zip: "application/zip",
2027
- "3gp": "video/3gpp; audio/3gpp if it doesn't contain video",
2028
- "3g2": "video/3gpp2; audio/3gpp2 if it doesn't contain video",
2029
- "7z": "application/x-7z-compressed"
2030
- };
2031
- function guessMimeTypeFromPath(filePath) {
2032
- const fileExt = filePath.split(".").pop();
2033
- if (fileExt) {
2034
- return COMMON_MIME_TYPES[fileExt.toLowerCase()] || null;
2156
+ const base64DataAttr = "data:";
2157
+ function getMimeTypeFromBase64(content) {
2158
+ const dataIndex = content.indexOf(base64DataAttr);
2159
+ const base64Index = content.indexOf(";base64,");
2160
+ if (dataIndex > -1 || base64Index > -1) {
2161
+ const mimeType = content.slice(dataIndex + base64DataAttr.length, base64Index);
2162
+ const normalizedMimeType = mimeType === "image/jpg" ? "image/jpeg" : mimeType;
2163
+ return normalizedMimeType;
2035
2164
  }
2036
2165
  return null;
2037
2166
  }
2038
- function isCachedPatchFileOp(op) {
2039
- return !!(op.op === "file" && typeof op.filePath === "string" && op.value && typeof op.value === "object" && !Array.isArray(op.value) && "sha256" in op.value && typeof op.value.sha256 === "string");
2040
- }
2041
- async function debugTiming(id, fn) {
2042
- if (process.env["VAL_DEBUG_TIMING"] === "true") {
2043
- const start = Date.now();
2044
- const r = await fn();
2045
- console.log(`Timing: ${id} took: ${Date.now() - start}ms (${new Date().toISOString()})`);
2046
- return r;
2047
- } else {
2048
- return fn();
2167
+ function bufferFromDataUrl(dataUrl) {
2168
+ let base64Data;
2169
+ const base64Index = dataUrl.indexOf(";base64,");
2170
+ if (base64Index > -1) {
2171
+ base64Data = dataUrl.slice(base64Index + ";base64,".length);
2172
+ }
2173
+ if (base64Data) {
2174
+ return Buffer.from(base64Data, "base64" // TODO: why does it not work with base64url?
2175
+ );
2049
2176
  }
2050
2177
  }
2051
2178
 
2052
- const textEncoder = new TextEncoder();
2053
- class LocalValServer extends ValServer {
2054
- static PATCHES_DIR = "patches";
2055
- static FILES_DIR = "files";
2056
- constructor(valModules, options, callbacks) {
2057
- super(options.service.sourceFileHandler.projectRoot, valModules, options, callbacks);
2058
- this.valModules = valModules;
2059
- this.options = options;
2060
- this.callbacks = callbacks;
2061
- this.patchesRootPath = options.cacheDir || path__namespace["default"].join(options.service.sourceFileHandler.projectRoot, ".val");
2062
- this.host = this.options.service.sourceFileHandler.host;
2179
+ const JSONValueT = z__default["default"].lazy(() => z__default["default"].union([z__default["default"].string(), z__default["default"].number(), z__default["default"].boolean(), z__default["default"].null(), z__default["default"].array(JSONValueT), z__default["default"].record(JSONValueT)]));
2180
+
2181
+ /**
2182
+ * Raw JSON patch operation.
2183
+ */
2184
+ const OperationJSONT = z__default["default"].discriminatedUnion("op", [z__default["default"].object({
2185
+ op: z__default["default"].literal("add"),
2186
+ path: z__default["default"].string(),
2187
+ value: JSONValueT
2188
+ }).strict(), z__default["default"].object({
2189
+ op: z__default["default"].literal("remove"),
2190
+ /**
2191
+ * Must be non-root
2192
+ */
2193
+ path: z__default["default"].string()
2194
+ }).strict(), z__default["default"].object({
2195
+ op: z__default["default"].literal("replace"),
2196
+ path: z__default["default"].string(),
2197
+ value: JSONValueT
2198
+ }).strict(), z__default["default"].object({
2199
+ op: z__default["default"].literal("move"),
2200
+ /**
2201
+ * Must be non-root and not a proper prefix of "path".
2202
+ */
2203
+ from: z__default["default"].string(),
2204
+ path: z__default["default"].string()
2205
+ }).strict(), z__default["default"].object({
2206
+ op: z__default["default"].literal("copy"),
2207
+ from: z__default["default"].string(),
2208
+ path: z__default["default"].string()
2209
+ }).strict(), z__default["default"].object({
2210
+ op: z__default["default"].literal("test"),
2211
+ path: z__default["default"].string(),
2212
+ value: JSONValueT
2213
+ }).strict(), z__default["default"].object({
2214
+ op: z__default["default"].literal("file"),
2215
+ path: z__default["default"].string(),
2216
+ filePath: z__default["default"].string(),
2217
+ value: z__default["default"].string()
2218
+ }).strict()]);
2219
+ const PatchJSON = z__default["default"].array(OperationJSONT);
2220
+ /**
2221
+ * Raw JSON patch operation.
2222
+ */
2223
+ const OperationT = z__default["default"].discriminatedUnion("op", [z__default["default"].object({
2224
+ op: z__default["default"].literal("add"),
2225
+ path: z__default["default"].array(z__default["default"].string()),
2226
+ value: JSONValueT
2227
+ }).strict(), z__default["default"].object({
2228
+ op: z__default["default"].literal("remove"),
2229
+ path: z__default["default"].array(z__default["default"].string()).nonempty()
2230
+ }).strict(), z__default["default"].object({
2231
+ op: z__default["default"].literal("replace"),
2232
+ path: z__default["default"].array(z__default["default"].string()),
2233
+ value: JSONValueT
2234
+ }).strict(), z__default["default"].object({
2235
+ op: z__default["default"].literal("move"),
2236
+ from: z__default["default"].array(z__default["default"].string()).nonempty(),
2237
+ path: z__default["default"].array(z__default["default"].string())
2238
+ }).strict(), z__default["default"].object({
2239
+ op: z__default["default"].literal("copy"),
2240
+ from: z__default["default"].array(z__default["default"].string()),
2241
+ path: z__default["default"].array(z__default["default"].string())
2242
+ }).strict(), z__default["default"].object({
2243
+ op: z__default["default"].literal("test"),
2244
+ path: z__default["default"].array(z__default["default"].string()),
2245
+ value: JSONValueT
2246
+ }).strict(), z__default["default"].object({
2247
+ op: z__default["default"].literal("file"),
2248
+ path: z__default["default"].array(z__default["default"].string()),
2249
+ filePath: z__default["default"].string(),
2250
+ value: z__default["default"].union([z__default["default"].string(), z__default["default"].object({
2251
+ sha256: z__default["default"].string()
2252
+ })])
2253
+ }).strict()]);
2254
+ const Patch = z__default["default"].array(OperationT);
2255
+
2256
+ class ValOpsFS extends ValOps {
2257
+ static VAL_DIR = ".val";
2258
+ constructor(rootDir, valModules, options) {
2259
+ super(valModules, options);
2260
+ this.rootDir = rootDir;
2261
+ this.host = new FSOpsHost();
2262
+ }
2263
+ async onInit() {
2264
+ // do nothing
2063
2265
  }
2064
- async session() {
2065
- return {
2066
- status: 200,
2067
- json: {
2068
- mode: "local",
2069
- enabled: await this.callbacks.isEnabled()
2266
+ async readPatches(includes) {
2267
+ const patchesCacheDir = this.getPatchesDir();
2268
+ let patchJsonFiles = [];
2269
+ if (!this.host.directoryExists || this.host.directoryExists && this.host.directoryExists(patchesCacheDir)) {
2270
+ patchJsonFiles = this.host.readDirectory(patchesCacheDir, ["patch.json"], [], []);
2271
+ }
2272
+ const patches = {};
2273
+ const errors = {};
2274
+ const sortedPatchIds = patchJsonFiles.map(file => parseInt(fsPath__namespace["default"].basename(fsPath__namespace["default"].dirname(file)), 10)).sort();
2275
+ for (const patchIdNum of sortedPatchIds) {
2276
+ if (Number.isNaN(patchIdNum)) {
2277
+ throw new Error("Could not parse patch id from file name. Files found: " + patchJsonFiles.join(", "));
2070
2278
  }
2071
- };
2072
- }
2073
- async deletePatches(query) {
2074
- const deletedPatches = [];
2075
- for (const patchId of query.id ?? []) {
2076
- const rawPatchFileContent = this.host.readFile(this.getPatchFilePath(patchId));
2077
- if (!rawPatchFileContent) {
2078
- console.warn("Val: Patch not found", patchId);
2279
+ const patchId = patchIdNum.toString();
2280
+ if (includes && !includes.includes(patchId)) {
2079
2281
  continue;
2080
2282
  }
2081
- const parsedPatchesRes = z.z.record(Patch).safeParse(JSON.parse(rawPatchFileContent));
2082
- if (!parsedPatchesRes.success) {
2083
- console.warn("Val: Could not parse patch file", patchId, parsedPatchesRes.error);
2283
+ const parsedFSPatchRes = this.parseJsonFile(this.getPatchFilePath(patchId), FSPatch);
2284
+ let parsedFSPatchBaseRes = undefined;
2285
+ if (this.host.fileExists(this.getPatchBaseFile(patchId))) {
2286
+ parsedFSPatchBaseRes = this.parseJsonFile(this.getPatchBaseFile(patchId), FSPatchBase);
2287
+ }
2288
+ if (parsedFSPatchRes.error) {
2289
+ errors[patchId] = parsedFSPatchRes.error;
2290
+ } else if (parsedFSPatchBaseRes && parsedFSPatchBaseRes.error) {
2291
+ errors[patchId] = parsedFSPatchBaseRes.error;
2292
+ } else {
2293
+ patches[patchId] = {
2294
+ ...parsedFSPatchRes.data,
2295
+ appliedAt: parsedFSPatchBaseRes ? parsedFSPatchBaseRes.data : null
2296
+ };
2297
+ }
2298
+ }
2299
+ if (Object.keys(errors).length > 0) {
2300
+ return {
2301
+ patches,
2302
+ errors
2303
+ };
2304
+ }
2305
+ return {
2306
+ patches
2307
+ };
2308
+ }
2309
+ async getPatchOpsById(patchIds) {
2310
+ return this.readPatches(patchIds);
2311
+ }
2312
+ async findPatches(filters) {
2313
+ const patches = {};
2314
+ const errors = {};
2315
+ const {
2316
+ errors: allErrors,
2317
+ patches: allPatches
2318
+ } = await this.readPatches();
2319
+ for (const [patchIdS, patch] of Object.entries(allPatches)) {
2320
+ const patchId = patchIdS;
2321
+ if (filters.authors && !(patch.authorId === null || filters.authors.includes(patch.authorId))) {
2084
2322
  continue;
2085
2323
  }
2086
- const files = Object.values(parsedPatchesRes.data).flatMap(ops => ops.filter(isCachedPatchFileOp).map(op => ({
2087
- filePath: op.filePath,
2088
- sha256: op.value.sha256
2089
- })));
2090
- for (const file of files) {
2091
- this.host.rmFile(this.getFilePath(file.filePath, file.sha256));
2092
- this.host.rmFile(this.getFileMetadataPath(file.filePath, file.sha256));
2324
+ patches[patchId] = {
2325
+ path: patch.path,
2326
+ createdAt: patch.createdAt,
2327
+ authorId: patch.authorId,
2328
+ appliedAt: patch.appliedAt
2329
+ };
2330
+ const error = allErrors && allErrors[patchId];
2331
+ if (error) {
2332
+ errors[patchId] = error;
2093
2333
  }
2094
- this.host.rmFile(path__namespace["default"].join(this.patchesRootPath, LocalValServer.PATCHES_DIR, patchId));
2095
- deletedPatches.push(patchId);
2334
+ }
2335
+ if (errors && Object.keys(errors).length > 0) {
2336
+ return {
2337
+ patches,
2338
+ errors
2339
+ };
2096
2340
  }
2097
2341
  return {
2098
- status: 200,
2099
- json: deletedPatches
2342
+ patches
2100
2343
  };
2101
2344
  }
2102
- async postPatches(body) {
2103
- const patches = z.z.record(Patch).safeParse(body);
2104
- if (!patches.success) {
2345
+
2346
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2347
+ parseJsonFile(filePath, parser) {
2348
+ if (!this.host.fileExists(filePath)) {
2105
2349
  return {
2106
- status: 404,
2107
- json: {
2108
- message: `Invalid patch: ${patches.error.message}`,
2109
- details: patches.error.issues
2350
+ error: {
2351
+ message: `File not found: ${filePath}`,
2352
+ filePath
2353
+ }
2354
+ };
2355
+ }
2356
+ const data = this.host.readUtf8File(filePath);
2357
+ if (!data) {
2358
+ return {
2359
+ error: {
2360
+ message: `File is empty: ${filePath}`,
2361
+ filePath
2362
+ }
2363
+ };
2364
+ }
2365
+ let jsonData;
2366
+ try {
2367
+ jsonData = JSON.parse(data);
2368
+ } catch (err) {
2369
+ if (typeof err === "object" && err && "message" in err && typeof err.message === "string") {
2370
+ return {
2371
+ error: {
2372
+ message: `Could not parse JSON of file: ${filePath}. Message: ${err.message}`,
2373
+ filePath
2374
+ }
2375
+ };
2376
+ }
2377
+ return {
2378
+ error: {
2379
+ message: "Unknown error",
2380
+ filePath
2381
+ }
2382
+ };
2383
+ }
2384
+ if (!parser) {
2385
+ return {
2386
+ data: jsonData
2387
+ };
2388
+ }
2389
+ try {
2390
+ const parsed = parser.safeParse(jsonData);
2391
+ if (!parsed.success) {
2392
+ return {
2393
+ error: {
2394
+ message: `Could not parse file: ${filePath}. Details: ${JSON.stringify(zodValidationError.fromError(parsed.error).toString())}`,
2395
+ details: parsed.error,
2396
+ filePath
2397
+ }
2398
+ };
2399
+ }
2400
+ return {
2401
+ data: parsed.data
2402
+ };
2403
+ } catch (err) {
2404
+ if (typeof err === "object" && err && "message" in err && typeof err.message === "string") {
2405
+ return {
2406
+ error: {
2407
+ message: `Could not parse JSON of file: ${filePath}. Message: ${err.message}`,
2408
+ filePath
2409
+ }
2410
+ };
2411
+ }
2412
+ return {
2413
+ error: {
2414
+ message: "Unknown error",
2415
+ filePath
2110
2416
  }
2111
2417
  };
2112
2418
  }
2419
+ }
2420
+ async saveSourceFilePatch(path, patch, authorId) {
2113
2421
  let fileId = Date.now();
2114
- while (this.host.fileExists(this.getPatchFilePath(fileId.toString()))) {
2115
- // ensure unique file / patch id
2116
- fileId++;
2422
+ try {
2423
+ while (this.host.fileExists(this.getPatchFilePath(fileId.toString()))) {
2424
+ // ensure unique file / patch id
2425
+ fileId++;
2426
+ }
2427
+ const patchId = fileId.toString();
2428
+ const data = {
2429
+ patch,
2430
+ path,
2431
+ authorId,
2432
+ coreVersion: core.Internal.VERSION.core,
2433
+ createdAt: new Date().toISOString()
2434
+ };
2435
+ this.host.writeUf8File(this.getPatchFilePath(patchId), JSON.stringify(data));
2436
+ return {
2437
+ patchId
2438
+ };
2439
+ } catch (err) {
2440
+ if (err instanceof Error) {
2441
+ return {
2442
+ error: {
2443
+ message: err.message
2444
+ }
2445
+ };
2446
+ }
2447
+ return {
2448
+ error: {
2449
+ message: "Unknown error"
2450
+ }
2451
+ };
2117
2452
  }
2118
- const patchId = fileId.toString();
2119
- const res = {};
2120
- const parsedPatches = {};
2121
- for (const moduleIdStr in patches.data) {
2122
- const moduleId = moduleIdStr; // TODO: validate that this is a valid module id
2123
- res[moduleId] = {
2124
- patch_id: patchId
2125
- };
2126
- parsedPatches[moduleId] = [];
2127
- for (const op of patches.data[moduleId]) {
2128
- // We do not want to include value of a file op in the patch as they potentially contain a lot of data,
2129
- // therefore we store the file in a separate file and only store the sha256 hash in the patch.
2130
- // I.e. the patch that frontend sends is not the same as the one stored.
2131
- // Large amount of text is one thing, but one could easily imagine a lot of patches being accumulated over time with a lot of images which would then consume a non-negligible amount of memory.
2132
- //
2133
- // This is potentially confusing for us working on Val internals, however, the alternative was expected to cause a lot of issues down the line: low performance, a lot of data moved, etc
2134
- // In the worst scenario we imagine this being potentially crashing the server runtime, especially on smaller edge runtimes.
2135
- // Potential crashes are bad enough to warrant this workaround.
2136
- if (core.Internal.isFileOp(op)) {
2137
- const sha256 = core.Internal.getSHA256Hash(textEncoder.encode(op.value));
2138
- const mimeType = getMimeTypeFromBase64(op.value);
2139
- if (!mimeType) {
2140
- console.error("Val: Cannot determine mimeType from base64 data", op);
2141
- throw Error("Cannot determine mimeType from base64 data: " + op.filePath);
2453
+ }
2454
+ async getSourceFile(path) {
2455
+ const filePath = fsPath__namespace["default"].join(this.rootDir, path);
2456
+ if (!this.host.fileExists(filePath)) {
2457
+ return {
2458
+ error: {
2459
+ message: `File not found: ${filePath}`
2460
+ }
2461
+ };
2462
+ }
2463
+ return {
2464
+ data: this.host.readUtf8File(filePath)
2465
+ };
2466
+ }
2467
+ async saveSourceFile(path, data) {
2468
+ const filePath = fsPath__namespace["default"].join(this.rootDir, ...path.split("/"));
2469
+ try {
2470
+ this.host.writeUf8File(filePath, data);
2471
+ return {
2472
+ path
2473
+ };
2474
+ } catch (err) {
2475
+ if (err instanceof Error) {
2476
+ return {
2477
+ error: {
2478
+ message: err.message
2142
2479
  }
2143
- const buffer = bufferFromDataUrl(op.value, mimeType);
2144
- if (!buffer) {
2145
- console.error("Val: Cannot parse base64 data", op);
2146
- throw Error("Cannot parse base64 data: " + op.filePath);
2480
+ };
2481
+ }
2482
+ return {
2483
+ error: {
2484
+ message: "Unknown error"
2485
+ }
2486
+ };
2487
+ }
2488
+ }
2489
+ async saveBase64EncodedBinaryFileFromPatch(filePath, patchId, data, _type, metadata) {
2490
+ const patchFilePath = this.getBinaryFilePath(filePath, patchId);
2491
+ const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchId);
2492
+ try {
2493
+ const buffer = bufferFromDataUrl(data);
2494
+ if (!buffer) {
2495
+ return {
2496
+ error: {
2497
+ message: "Could not create buffer from data url. Not a data url? First chars were: " + data.slice(0, 20)
2147
2498
  }
2148
- this.host.writeFile(this.getFilePath(op.filePath, sha256), buffer, "binary");
2149
- this.host.writeFile(this.getFileMetadataPath(op.filePath, sha256), JSON.stringify({
2150
- mimeType,
2151
- sha256,
2152
- // useful for debugging / manual inspection
2153
- patchId,
2154
- createdAt: new Date().toISOString()
2155
- }, null, 2), "utf8");
2156
- parsedPatches[moduleId].push({
2157
- ...op,
2158
- value: {
2159
- sha256,
2160
- mimeType
2161
- }
2162
- });
2163
- } else {
2164
- parsedPatches[moduleId].push(op);
2499
+ };
2500
+ }
2501
+ this.host.writeUf8File(metadataFilePath, JSON.stringify(metadata));
2502
+ this.host.writeBinaryFile(patchFilePath, buffer);
2503
+ return {
2504
+ patchId,
2505
+ filePath
2506
+ };
2507
+ } catch (err) {
2508
+ if (err instanceof Error) {
2509
+ return {
2510
+ error: {
2511
+ message: err.message
2512
+ }
2513
+ };
2514
+ }
2515
+ return {
2516
+ error: {
2517
+ message: "Unknown error"
2165
2518
  }
2519
+ };
2520
+ }
2521
+ }
2522
+ async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
2523
+ const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchId);
2524
+ if (!this.host.fileExists(metadataFilePath)) {
2525
+ return {
2526
+ errors: [{
2527
+ message: "Metadata file not found",
2528
+ filePath
2529
+ }]
2530
+ };
2531
+ }
2532
+ const metadataParseRes = this.parseJsonFile(metadataFilePath, z.z.record(z.z.union([z.z.string(), z.z.number()])));
2533
+ if (metadataParseRes.error) {
2534
+ return {
2535
+ errors: [metadataParseRes.error]
2536
+ };
2537
+ }
2538
+ const parsed = metadataParseRes.data;
2539
+ const expectedFields = getFieldsForType(type);
2540
+ const fieldErrors = [];
2541
+ for (const field of expectedFields) {
2542
+ if (!(field in parsed)) {
2543
+ fieldErrors.push({
2544
+ message: `Expected fields for type: ${type}. Field not found: '${field}'`,
2545
+ field
2546
+ });
2166
2547
  }
2167
2548
  }
2168
- this.host.writeFile(this.getPatchFilePath(patchId), JSON.stringify(parsedPatches), "utf8");
2549
+ if (fieldErrors.length > 0) {
2550
+ return {
2551
+ errors: fieldErrors
2552
+ };
2553
+ }
2169
2554
  return {
2170
- status: 200,
2171
- json: res
2555
+ metadata: parsed
2556
+ };
2557
+ }
2558
+ async getBase64EncodedBinaryFileFromPatch(filePath, patchId) {
2559
+ const absPath = this.getBinaryFilePath(filePath, patchId);
2560
+ if (!this.host.fileExists(absPath)) {
2561
+ return null;
2562
+ }
2563
+ return this.host.readBinaryFile(absPath);
2564
+ }
2565
+ async deletePatches(patchIds) {
2566
+ const deleted = [];
2567
+ let errors = null;
2568
+ for (const patchId of patchIds) {
2569
+ try {
2570
+ this.host.deleteDir(this.getPatchDir(patchId));
2571
+ deleted.push(patchId);
2572
+ } catch (err) {
2573
+ if (!errors) {
2574
+ errors = {};
2575
+ }
2576
+ errors[patchId] = {
2577
+ message: err instanceof Error ? err.message : "Unknown error"
2578
+ };
2579
+ }
2580
+ }
2581
+ if (errors) {
2582
+ return {
2583
+ deleted,
2584
+ errors
2585
+ };
2586
+ }
2587
+ return {
2588
+ deleted
2589
+ };
2590
+ }
2591
+ async saveFiles(preparedCommit) {
2592
+ const updatedFiles = [];
2593
+ const errors = {};
2594
+ for (const [filePath, data] of Object.entries(preparedCommit.patchedSourceFiles)) {
2595
+ const absPath = fsPath__namespace["default"].join(this.rootDir, ...filePath.split("/"));
2596
+ try {
2597
+ this.host.writeUf8File(absPath, data);
2598
+ updatedFiles.push(absPath);
2599
+ } catch (err) {
2600
+ errors[absPath] = {
2601
+ message: err instanceof Error ? err.message : "Unknown error",
2602
+ filePath
2603
+ };
2604
+ }
2605
+ }
2606
+ for (const [filePath, {
2607
+ patchId
2608
+ }] of Object.entries(preparedCommit.patchedBinaryFilesDescriptors)) {
2609
+ const absPath = fsPath__namespace["default"].join(this.rootDir, ...filePath.split("/"));
2610
+ try {
2611
+ this.host.copyFile(this.getBinaryFilePath(filePath, patchId), absPath);
2612
+ updatedFiles.push(absPath);
2613
+ } catch (err) {
2614
+ errors[absPath] = {
2615
+ message: err instanceof Error ? err.message : "Unknown error",
2616
+ filePath
2617
+ };
2618
+ }
2619
+ }
2620
+ for (const patchId of Object.values(preparedCommit.appliedPatches).flat()) {
2621
+ const appliedAt = {
2622
+ baseSha: await this.getBaseSha(),
2623
+ timestamp: new Date().toISOString()
2624
+ };
2625
+ const absPath = this.getPatchBaseFile(patchId);
2626
+ try {
2627
+ this.host.writeUf8File(absPath, JSON.stringify(appliedAt));
2628
+ } catch (err) {
2629
+ errors[absPath] = {
2630
+ message: err instanceof Error ? err.message : "Unknown error",
2631
+ filePath: absPath
2632
+ };
2633
+ }
2634
+ }
2635
+ return {
2636
+ updatedFiles,
2637
+ errors
2638
+ };
2639
+ }
2640
+ async getBinaryFile(filePath) {
2641
+ const absPath = fsPath__namespace["default"].join(this.rootDir, ...filePath.split("/"));
2642
+ if (!this.host.fileExists(absPath)) {
2643
+ return null;
2644
+ }
2645
+ const buffer = this.host.readBinaryFile(absPath);
2646
+ return buffer;
2647
+ }
2648
+ async getBinaryFileMetadata(filePath, type) {
2649
+ const buffer = await this.getBinaryFile(filePath);
2650
+ if (!buffer) {
2651
+ return {
2652
+ errors: [{
2653
+ message: "File not found",
2654
+ filePath
2655
+ }]
2656
+ };
2657
+ }
2658
+ const mimeType = guessMimeTypeFromPath(filePath);
2659
+ if (!mimeType) {
2660
+ return {
2661
+ errors: [{
2662
+ message: `Could not guess mime type of file ext: ${fsPath__namespace["default"].extname(filePath)}`,
2663
+ filePath
2664
+ }]
2665
+ };
2666
+ }
2667
+ return createMetadataFromBuffer(type, mimeType, buffer);
2668
+ }
2669
+
2670
+ // #region fs file path helpers
2671
+ getPatchesDir() {
2672
+ return fsPath__namespace["default"].join(this.rootDir, ValOpsFS.VAL_DIR, "patches");
2673
+ }
2674
+ getPatchDir(patchId) {
2675
+ return fsPath__namespace["default"].join(this.getPatchesDir(), patchId);
2676
+ }
2677
+ getBinaryFilePath(filename, patchId) {
2678
+ return fsPath__namespace["default"].join(this.getPatchDir(patchId), "files", filename, fsPath__namespace["default"].basename(filename));
2679
+ }
2680
+ getBinaryFileMetadataPath(filename, patchId) {
2681
+ return fsPath__namespace["default"].join(this.getPatchDir(patchId), "files", filename, "metadata.json");
2682
+ }
2683
+ getPatchFilePath(patchId) {
2684
+ return fsPath__namespace["default"].join(this.getPatchDir(patchId), "patch.json");
2685
+ }
2686
+ getPatchBaseFile(patchId) {
2687
+ return fsPath__namespace["default"].join(this.getPatchDir(patchId), "base.json");
2688
+ }
2689
+ }
2690
+ class FSOpsHost {
2691
+ constructor() {}
2692
+
2693
+ // TODO: do we want async operations here?
2694
+ deleteDir(dir) {
2695
+ if (this.directoryExists(dir)) {
2696
+ fs__default["default"].rmdirSync(dir, {
2697
+ recursive: true
2698
+ });
2699
+ }
2700
+ }
2701
+ directoryExists(path) {
2702
+ return ts__default["default"].sys.directoryExists(path);
2703
+ }
2704
+ readDirectory(path, extensions, exclude, include) {
2705
+ return ts__default["default"].sys.readDirectory(path, extensions, exclude, include);
2706
+ }
2707
+ fileExists(path) {
2708
+ return ts__default["default"].sys.fileExists(path);
2709
+ }
2710
+ readBinaryFile(path) {
2711
+ return fs__default["default"].readFileSync(path);
2712
+ }
2713
+ readUtf8File(path) {
2714
+ return fs__default["default"].readFileSync(path, "utf-8");
2715
+ }
2716
+ writeUf8File(path, data) {
2717
+ fs__default["default"].mkdirSync(fsPath__namespace["default"].dirname(path), {
2718
+ recursive: true
2719
+ });
2720
+ fs__default["default"].writeFileSync(path, data, "utf-8");
2721
+ }
2722
+ writeBinaryFile(path, data) {
2723
+ fs__default["default"].mkdirSync(fsPath__namespace["default"].dirname(path), {
2724
+ recursive: true
2725
+ });
2726
+ fs__default["default"].writeFileSync(path, data, "base64url");
2727
+ }
2728
+ copyFile(from, to) {
2729
+ fs__default["default"].mkdirSync(fsPath__namespace["default"].dirname(to), {
2730
+ recursive: true
2731
+ });
2732
+ fs__default["default"].copyFileSync(from, to);
2733
+ }
2734
+ }
2735
+ const FSPatch = z.z.object({
2736
+ path: z.z.string().refine(p => p.startsWith("/") && p.includes(".val."), "Path is not valid. Must start with '/' and include '.val.'"),
2737
+ patch: Patch,
2738
+ authorId: z.z.string().refine(p => true).nullable(),
2739
+ createdAt: z.z.string().datetime(),
2740
+ coreVersion: z.z.string().nullable() // TODO: use this to check if patch is compatible with current core version?
2741
+ });
2742
+ const FSPatchBase = z.z.object({
2743
+ baseSha: z.z.string().refine(p => true),
2744
+ timestamp: z.z.string().datetime()
2745
+ });
2746
+
2747
+ const textEncoder = new TextEncoder();
2748
+ const PatchId = z.z.string().refine(s => !!s); // TODO: validate
2749
+ const CommitSha = z.z.string().refine(s => !!s); // TODO: validate
2750
+ const BaseSha = z.z.string().refine(s => !!s); // TODO: validate
2751
+ const AuthorId = z.z.string().refine(s => !!s); // TODO: validate
2752
+ const ModuleFilePath = z.z.string().refine(s => !!s); // TODO: validate
2753
+ const Metadata = z.z.union([z.z.object({
2754
+ sha256: z.z.string(),
2755
+ mimeType: z.z.string(),
2756
+ width: z.z.number(),
2757
+ height: z.z.number()
2758
+ }), z.z.object({
2759
+ sha256: z.z.string(),
2760
+ mimeType: z.z.string()
2761
+ })]);
2762
+ const MetadataRes = z.z.object({
2763
+ filePath: ModuleFilePath,
2764
+ metadata: Metadata,
2765
+ type: z.z.union([z.z.literal("file"), z.z.literal("image")]).nullable()
2766
+ });
2767
+ const BasePatchResponse = z.z.object({
2768
+ path: ModuleFilePath,
2769
+ patchId: PatchId,
2770
+ authorId: AuthorId.nullable(),
2771
+ createdAt: z.z.string().datetime(),
2772
+ applied: z.z.object({
2773
+ baseSha: BaseSha,
2774
+ commitSha: CommitSha,
2775
+ appliedAt: z.z.string().datetime()
2776
+ }).nullable()
2777
+ });
2778
+ const GetPatches = z.z.object({
2779
+ patches: z.z.array(z.z.intersection(z.z.object({
2780
+ patch: Patch
2781
+ }), BasePatchResponse)),
2782
+ errors: z.z.array(z.z.object({
2783
+ patchId: PatchId.optional(),
2784
+ message: z.z.string()
2785
+ })).optional()
2786
+ });
2787
+ const SearchPatches = z.z.object({
2788
+ patches: z.z.array(BasePatchResponse)
2789
+ });
2790
+ const FilesResponse = z.z.object({
2791
+ files: z.z.array(z.z.union([z.z.object({
2792
+ filePath: z.z.string(),
2793
+ location: z.z.literal("patch"),
2794
+ patchId: PatchId,
2795
+ value: z.z.string()
2796
+ }), z.z.object({
2797
+ filePath: z.z.string(),
2798
+ location: z.z.literal("repo"),
2799
+ commitSha: CommitSha,
2800
+ value: z.z.string()
2801
+ })])),
2802
+ errors: z.z.array(z.z.union([z.z.object({
2803
+ filePath: z.z.string(),
2804
+ location: z.z.literal("patch"),
2805
+ patchId: PatchId,
2806
+ message: z.z.string()
2807
+ }), z.z.object({
2808
+ filePath: z.z.string(),
2809
+ location: z.z.literal("repo"),
2810
+ commitSha: CommitSha,
2811
+ message: z.z.string()
2812
+ })])).optional()
2813
+ });
2814
+ const SavePatchResponse = z.z.object({
2815
+ patchId: PatchId
2816
+ });
2817
+ const DeletePatchesResponse = z.z.object({
2818
+ deleted: z.z.array(PatchId),
2819
+ errors: z.z.array(z.z.object({
2820
+ message: z.z.string(),
2821
+ patchId: PatchId
2822
+ })).optional()
2823
+ });
2824
+ const SavePatchFileResponse = z.z.object({
2825
+ patchId: PatchId,
2826
+ filePath: ModuleFilePath
2827
+ });
2828
+ const CommitResponse = z.z.object({
2829
+ updatedFiles: z.z.array(z.z.string()),
2830
+ commit: CommitSha,
2831
+ branch: z.z.string()
2832
+ });
2833
+ class ValOpsHttp extends ValOps {
2834
+ constructor(hostUrl, project, commitSha, branch, apiKey, valModules, options) {
2835
+ super(valModules, options);
2836
+ this.hostUrl = hostUrl;
2837
+ this.project = project;
2838
+ this.commitSha = commitSha;
2839
+ this.branch = branch;
2840
+ this.authHeaders = {
2841
+ Authorization: `Bearer ${apiKey}`
2172
2842
  };
2843
+ this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
2173
2844
  }
2174
- async getMetadata() {
2175
- return undefined;
2845
+ async onInit() {
2846
+ // TODO: unused for now. Implement or remove
2176
2847
  }
2177
- async getFiles(filePath, query) {
2178
- if (query.sha256) {
2179
- const fileExists = this.host.fileExists(this.getFilePath(filePath, query.sha256));
2180
- if (fileExists) {
2181
- const metadataFileContent = this.host.readFile(this.getFileMetadataPath(filePath, query.sha256));
2182
- const fileContent = await this.readStaticBinaryFile(this.getFilePath(filePath, query.sha256));
2183
- if (!fileContent) {
2184
- throw Error("Could not read cached patch file / asset. Cache corrupted?");
2185
- }
2186
- if (!metadataFileContent) {
2187
- throw Error("Missing metadata of cached patch file / asset. Cache corrupted?");
2848
+ async getPatchOpsById(patchIds) {
2849
+ const params = new URLSearchParams();
2850
+ params.set("branch", this.branch);
2851
+ if (patchIds.length > 0) {
2852
+ params.set("patch_ids", encodeURIComponent(patchIds.join(",")));
2853
+ }
2854
+ return fetch(`${this.hostUrl}/v1/${this.project}/patches${params.size > 0 ? "?" + params : ""}`, {
2855
+ headers: {
2856
+ ...this.authHeaders,
2857
+ "Content-Type": "application/json"
2858
+ }
2859
+ }).then(async res => {
2860
+ const patches = {};
2861
+ if (res.ok) {
2862
+ const json = await res.json();
2863
+ const parsed = GetPatches.safeParse(json);
2864
+ if (parsed.success) {
2865
+ const data = parsed.data;
2866
+ const errors = {};
2867
+ for (const patchesRes of data.patches) {
2868
+ patches[patchesRes.patchId] = {
2869
+ path: patchesRes.path,
2870
+ authorId: patchesRes.authorId,
2871
+ createdAt: patchesRes.createdAt,
2872
+ appliedAt: patchesRes.applied && {
2873
+ baseSha: patchesRes.applied.baseSha,
2874
+ timestamp: patchesRes.applied.appliedAt,
2875
+ git: {
2876
+ commitSha: patchesRes.applied.commitSha
2877
+ }
2878
+ },
2879
+ patch: patchesRes.patch
2880
+ };
2881
+ }
2882
+ return {
2883
+ patches,
2884
+ errors
2885
+ };
2188
2886
  }
2189
- const metadata = JSON.parse(metadataFileContent);
2190
2887
  return {
2191
- status: 200,
2192
- headers: {
2193
- "Content-Type": metadata.mimeType,
2194
- "Content-Length": fileContent.byteLength.toString(),
2195
- "Cache-Control": "public, max-age=31536000, immutable"
2196
- },
2197
- body: bufferToReadableStream(fileContent)
2888
+ patches,
2889
+ error: {
2890
+ message: `Could not parse get patches response. Error: ${zodValidationError.fromError(parsed.error)}`
2891
+ }
2198
2892
  };
2199
2893
  }
2200
- }
2201
- const buffer = await this.readStaticBinaryFile(path__namespace["default"].join(this.cwd, filePath));
2202
- const mimeType = guessMimeTypeFromPath(filePath) || "application/octet-stream";
2203
- if (!buffer) {
2204
2894
  return {
2205
- status: 404,
2206
- json: {
2207
- message: "File not found"
2895
+ patches,
2896
+ error: {
2897
+ message: "Could not get patches. HTTP error: " + res.status + " " + res.statusText
2208
2898
  }
2209
2899
  };
2900
+ });
2901
+ }
2902
+ async findPatches(filters) {
2903
+ const params = new URLSearchParams();
2904
+ params.set("branch", this.branch);
2905
+ if (filters.authors && filters.authors.length > 0) {
2906
+ params.set("author_ids", encodeURIComponent(filters.authors.join(",")));
2210
2907
  }
2211
- if (query.sha256) {
2212
- const sha256 = getSha256(mimeType, buffer);
2213
- if (sha256 === query.sha256) {
2908
+ return fetch(`${this.hostUrl}/v1/${this.project}/search/patches${params.size > 0 ? "?" + params : ""}`, {
2909
+ headers: {
2910
+ ...this.authHeaders,
2911
+ "Content-Type": "application/json"
2912
+ }
2913
+ }).then(async res => {
2914
+ const patches = {};
2915
+ if (res.ok) {
2916
+ const parsed = SearchPatches.safeParse(await res.json());
2917
+ if (parsed.success) {
2918
+ for (const patchesRes of parsed.data.patches) {
2919
+ patches[patchesRes.patchId] = {
2920
+ path: patchesRes.path,
2921
+ authorId: patchesRes.authorId,
2922
+ createdAt: patchesRes.createdAt,
2923
+ appliedAt: patchesRes.applied && {
2924
+ baseSha: patchesRes.applied.baseSha,
2925
+ timestamp: patchesRes.applied.appliedAt,
2926
+ git: {
2927
+ commitSha: patchesRes.applied.commitSha
2928
+ }
2929
+ }
2930
+ };
2931
+ }
2932
+ return {
2933
+ patches
2934
+ };
2935
+ }
2214
2936
  return {
2215
- status: 200,
2216
- headers: {
2217
- "Content-Type": mimeType,
2218
- "Content-Length": buffer.byteLength.toString(),
2219
- "Cache-Control": "public, max-age=31536000, immutable"
2220
- },
2221
- body: bufferToReadableStream(buffer)
2937
+ patches,
2938
+ error: {
2939
+ message: `Could not parse search patches response. Error: ${zodValidationError.fromError(parsed.error)}`
2940
+ }
2222
2941
  };
2223
2942
  }
2224
- }
2225
- return {
2226
- status: 200,
2943
+ return {
2944
+ patches,
2945
+ error: {
2946
+ message: "Could not find patches. HTTP error: " + res.status + " " + res.statusText
2947
+ }
2948
+ };
2949
+ });
2950
+ }
2951
+ async saveSourceFilePatch(path, patch, authorId) {
2952
+ return fetch(`${this.hostUrl}/v1/${this.project}/patches`, {
2953
+ method: "POST",
2227
2954
  headers: {
2228
- "Content-Type": mimeType,
2229
- "Content-Length": buffer.byteLength.toString()
2955
+ ...this.authHeaders,
2956
+ "Content-Type": "application/json"
2230
2957
  },
2231
- body: bufferToReadableStream(buffer)
2232
- };
2233
- }
2234
- async getPatches(query) {
2235
- const patchesCacheDir = path__namespace["default"].join(this.patchesRootPath, LocalValServer.PATCHES_DIR);
2236
- let files = [];
2237
- try {
2238
- if (!this.host.directoryExists || this.host.directoryExists && this.host.directoryExists(patchesCacheDir)) {
2239
- files = this.host.readDirectory(patchesCacheDir, [""], [], []);
2240
- }
2241
- } catch (e) {
2242
- console.debug("Failed to read directory (no patches yet?)", e);
2243
- }
2244
- const res = {};
2245
- const sortedPatchIds = files.map(file => parseInt(path__namespace["default"].basename(file), 10)).sort();
2246
- for (const patchIdStr of sortedPatchIds) {
2247
- const patchId = patchIdStr.toString();
2248
- if (query.id && query.id.length > 0 && !query.id.includes(patchId)) {
2249
- continue;
2250
- }
2251
- try {
2252
- const currentParsedPatches = z.z.record(Patch).safeParse(JSON.parse(this.host.readFile(path__namespace["default"].join(this.patchesRootPath, LocalValServer.PATCHES_DIR, `${patchId}`)) || ""));
2253
- if (!currentParsedPatches.success) {
2254
- const msg = "Unexpected error reading patch. Patch did not parse correctly. Is there a mismatch in Val versions? Perhaps Val is misconfigured?";
2255
- console.error(`Val: ${msg}`, {
2256
- patchId,
2257
- error: currentParsedPatches.error
2258
- });
2958
+ body: JSON.stringify({
2959
+ path,
2960
+ patch,
2961
+ authorId,
2962
+ commit: this.commitSha,
2963
+ branch: this.branch,
2964
+ coreVersion: core.Internal.VERSION.core
2965
+ })
2966
+ }).then(async res => {
2967
+ if (res.ok) {
2968
+ const parsed = SavePatchResponse.safeParse(await res.json());
2969
+ if (parsed.success) {
2259
2970
  return {
2260
- status: 500,
2261
- json: {
2262
- message: msg,
2263
- details: {
2264
- patchId,
2265
- error: currentParsedPatches.error
2266
- }
2267
- }
2971
+ patchId: parsed.data.patchId
2268
2972
  };
2269
2973
  }
2270
- const createdAt = patchId;
2271
- for (const moduleIdStr in currentParsedPatches.data) {
2272
- const moduleId = moduleIdStr;
2273
- if (!res[moduleId]) {
2274
- res[moduleId] = [];
2275
- }
2276
- res[moduleId].push({
2277
- patch: currentParsedPatches.data[moduleId],
2278
- patch_id: patchId,
2279
- created_at: new Date(Number(createdAt)).toISOString()
2280
- });
2281
- }
2282
- } catch (err) {
2283
- const msg = `Unexpected error while reading patch file. The cache may be corrupted or Val may be misconfigured. Try deleting the cache directory.`;
2284
- console.error(`Val: ${msg}`, {
2285
- patchId,
2286
- error: err,
2287
- dir: this.patchesRootPath
2288
- });
2289
2974
  return {
2290
- status: 500,
2291
- json: {
2292
- message: msg,
2293
- details: {
2294
- patchId,
2295
- error: err === null || err === void 0 ? void 0 : err.toString()
2296
- }
2975
+ error: {
2976
+ message: `Could not parse save patch response. Error: ${zodValidationError.fromError(parsed.error)}`
2297
2977
  }
2298
2978
  };
2299
2979
  }
2300
- }
2301
- return {
2302
- status: 200,
2303
- json: res
2304
- };
2305
- }
2306
- getFilePath(filename, sha256) {
2307
- return path__namespace["default"].join(this.patchesRootPath, LocalValServer.FILES_DIR, filename, sha256, "file");
2308
- }
2309
- getFileMetadataPath(filename, sha256) {
2310
- return path__namespace["default"].join(this.patchesRootPath, LocalValServer.FILES_DIR, filename, sha256, "metadata.json");
2311
- }
2312
- getPatchFilePath(patchId) {
2313
- return path__namespace["default"].join(this.patchesRootPath, LocalValServer.PATCHES_DIR, patchId.toString());
2980
+ return {
2981
+ error: {
2982
+ message: "Could not save patch. HTTP error: " + res.status + " " + res.statusText
2983
+ }
2984
+ };
2985
+ }).catch(e => {
2986
+ return {
2987
+ error: {
2988
+ message: `Could save source file patch (connection error?): ${e instanceof Error ? e.message : e.toString()}`
2989
+ }
2990
+ };
2991
+ });
2314
2992
  }
2315
- badRequest() {
2316
- return {
2317
- status: 400,
2318
- json: {
2319
- message: "Local server does not handle this request"
2993
+ async saveBase64EncodedBinaryFileFromPatch(filePath, patchId, data, type, metadata) {
2994
+ return fetch(`${this.hostUrl}/v1/${this.project}/patches/${patchId}/files`, {
2995
+ method: "POST",
2996
+ headers: {
2997
+ ...this.authHeaders,
2998
+ "Content-Type": "application/json"
2999
+ },
3000
+ body: JSON.stringify({
3001
+ filePath: filePath,
3002
+ data,
3003
+ type,
3004
+ metadata
3005
+ })
3006
+ }).then(async res => {
3007
+ if (res.ok) {
3008
+ const parsed = SavePatchFileResponse.safeParse(await res.json());
3009
+ if (parsed.success) {
3010
+ return {
3011
+ patchId: parsed.data.patchId,
3012
+ filePath: parsed.data.filePath
3013
+ };
3014
+ }
3015
+ return {
3016
+ error: {
3017
+ message: `Could not parse save patch file response. Error: ${zodValidationError.fromError(parsed.error)}`
3018
+ }
3019
+ };
2320
3020
  }
2321
- };
3021
+ return {
3022
+ error: {
3023
+ message: "Could not save patch file. HTTP error: " + res.status + " " + res.statusText
3024
+ }
3025
+ };
3026
+ }).catch(e => {
3027
+ return {
3028
+ error: {
3029
+ message: `Could save source binary file in patch (connection error?): ${e.toString()}`
3030
+ }
3031
+ };
3032
+ });
2322
3033
  }
2323
- async ensureInitialized() {
2324
- // No RemoteFS so nothing to ensure
2325
- return fp.result.ok(undefined);
2326
- }
2327
- getSerializedModules() {
2328
- return Promise.all(this.valModules.modules.map(({
2329
- def
2330
- }, i) => {
2331
- return def().then(({
2332
- default: valModule
2333
- }) => {
2334
- var _Internal$getSchema;
2335
- const path = core.Internal.getValPath(valModule);
2336
- if (!path) {
2337
- throw Error(`Module defined at pos: ${i} is missing path`);
2338
- }
2339
- const source = core.Internal.getSource(valModule);
2340
- if (!source) {
2341
- // TODO
2342
- throw Error(`Module defined at pos: ${i} is missing source`);
2343
- }
2344
- const schema = (_Internal$getSchema = core.Internal.getSchema(valModule)) === null || _Internal$getSchema === void 0 ? void 0 : _Internal$getSchema.serialize();
2345
- if (!schema) {
2346
- // TODO
2347
- throw Error(`Module defined at pos: ${i} is missing schema`);
3034
+ async getHttpFiles(files) {
3035
+ const params = new URLSearchParams();
3036
+ const stringifiedFiles = JSON.stringify({
3037
+ files,
3038
+ root: this.root
3039
+ });
3040
+ params.set("body_sha",
3041
+ // We use this for cache invalidation
3042
+ core.Internal.getSHA256Hash(textEncoder.encode(stringifiedFiles)));
3043
+ return fetch(`${this.hostUrl}/v1/${this.project}/files?${params}`, {
3044
+ method: "PUT",
3045
+ // Yes, PUT is weird. Weirder to have a body in a GET request.
3046
+ headers: {
3047
+ ...this.authHeaders,
3048
+ "Content-Type": "application/json"
3049
+ },
3050
+ body: stringifiedFiles
3051
+ }).then(async res => {
3052
+ if (res.ok) {
3053
+ const json = await res.json();
3054
+ // TODO: Check that all requested paths are in the response
3055
+ const parsedFileResponse = FilesResponse.safeParse(json);
3056
+ if (parsedFileResponse.success) {
3057
+ return parsedFileResponse.data;
2348
3058
  }
2349
3059
  return {
2350
- path,
2351
- source: source,
2352
- errors: false,
2353
- //valModule[GetSchema]?.validate(path, source),
2354
- schema: schema
3060
+ error: {
3061
+ message: `Could not parse file response. Error: ${zodValidationError.fromError(parsedFileResponse.error)}`
3062
+ }
2355
3063
  };
2356
- });
2357
- }));
2358
- }
2359
- getModule(moduleId) {
2360
- // TODO: do not get all modules - we only should only get the ones we need
2361
- return this.getSerializedModules().then(all => {
2362
- const found = all.find(valModule => valModule.path === moduleId);
2363
- if (!found) {
2364
- throw Error(`Module ${moduleId} not found`);
2365
3064
  }
2366
- return found;
3065
+ return {
3066
+ error: {
3067
+ message: "Could not get files. HTTP error: " + res.status + " " + res.statusText
3068
+ }
3069
+ };
3070
+ }).catch(e => {
3071
+ return {
3072
+ error: {
3073
+ message: `Could not get file (connection error?): ${e instanceof Error ? e.message : e.toString()}`
3074
+ }
3075
+ };
2367
3076
  });
2368
3077
  }
2369
- async getAllModules(treePath) {
2370
- const moduleIds = (await this.getSerializedModules()).filter(({
2371
- path
2372
- }) => {
2373
- if (treePath) {
2374
- return path.startsWith(treePath);
2375
- }
2376
- return true;
2377
- }).map(({
2378
- path
2379
- }) => path);
2380
- return moduleIds;
2381
- }
2382
- async execCommit(patches) {
2383
- for (const [patchId, moduleId, patch] of patches) {
2384
- // TODO: patch the entire module content directly by using a { path: "", op: "replace", value: patchedData }?
2385
- // Reason: that would be more atomic? Not doing it now, because there are currently already too many moving pieces.
2386
- // Other things we could do would be to patch in a temp directory and ONLY when all patches are applied we move back in.
2387
- // This would improve reliability
2388
- this.host.rmFile(this.getPatchFilePath(patchId));
2389
- await this.options.service.patch(moduleId, patch);
3078
+ async getSourceFile(path) {
3079
+ const filesRes = await this.getHttpFiles([{
3080
+ filePath: path,
3081
+ location: "repo",
3082
+ root: this.root,
3083
+ commitSha: this.commitSha
3084
+ }]);
3085
+ if (filesRes.error) {
3086
+ return filesRes;
3087
+ }
3088
+ const file = filesRes.files.find(f => f.filePath === path);
3089
+ if (!file) {
3090
+ return {
3091
+ error: {
3092
+ message: `Could not find file ${path} in response`
3093
+ }
3094
+ };
2390
3095
  }
2391
3096
  return {
2392
- status: 200,
2393
- json: await this.getPatchedModules(patches)
3097
+ data: Buffer.from(file.value, "base64").toString("utf-8")
2394
3098
  };
2395
3099
  }
2396
-
2397
- /* Bad requests on Local Server: */
2398
-
2399
- async authorize() {
2400
- return this.badRequest();
2401
- }
2402
- async callback() {
2403
- return this.badRequest();
2404
- }
2405
- async logout() {
2406
- return this.badRequest();
2407
- }
2408
- }
2409
-
2410
- function decodeJwt(token, secretKey) {
2411
- const [headerBase64, payloadBase64, signatureBase64, ...rest] = token.split(".");
2412
- if (!headerBase64 || !payloadBase64 || !signatureBase64 || rest.length > 0) {
2413
- console.debug("Invalid JWT: format is not exactly {header}.{payload}.{signature}", token);
2414
- return null;
2415
- }
2416
- try {
2417
- const parsedHeader = JSON.parse(Buffer.from(headerBase64, "base64").toString("utf8"));
2418
- const headerVerification = JwtHeaderSchema.safeParse(parsedHeader);
2419
- if (!headerVerification.success) {
2420
- console.debug("Invalid JWT: invalid header", parsedHeader);
3100
+ async getBinaryFile(filePath) {
3101
+ // 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
3102
+ const filesRes = await this.getHttpFiles([{
3103
+ filePath: filePath,
3104
+ location: "repo",
3105
+ root: this.root,
3106
+ commitSha: this.commitSha
3107
+ }]);
3108
+ if (filesRes.error) {
2421
3109
  return null;
2422
3110
  }
2423
- if (headerVerification.data.typ !== jwtHeader.typ) {
2424
- console.debug("Invalid JWT: invalid header typ", parsedHeader);
3111
+ const file = filesRes.files.find(f => f.filePath === filePath);
3112
+ if (!file) {
2425
3113
  return null;
2426
3114
  }
2427
- if (headerVerification.data.alg !== jwtHeader.alg) {
2428
- console.debug("Invalid JWT: invalid header alg", parsedHeader);
3115
+ return Buffer.from(file.value, "base64");
3116
+ }
3117
+ async getBase64EncodedBinaryFileFromPatch(filePath, patchId) {
3118
+ const filesRes = await this.getHttpFiles([{
3119
+ filePath: filePath,
3120
+ location: "patch",
3121
+ patchId
3122
+ }]);
3123
+ if (filesRes.error) {
2429
3124
  return null;
2430
3125
  }
2431
- } catch (err) {
2432
- console.debug("Invalid JWT: could not parse header", err);
2433
- return null;
2434
- }
2435
- if (secretKey) {
2436
- const signature = crypto__default["default"].createHmac("sha256", secretKey).update(`${headerBase64}.${payloadBase64}`).digest("base64");
2437
- if (signature !== signatureBase64) {
2438
- console.debug("Invalid JWT: invalid signature");
3126
+ const file = filesRes.files.find(f => f.filePath === filePath);
3127
+ if (!file) {
2439
3128
  return null;
2440
3129
  }
3130
+ return Buffer.from(file.value, "base64");
2441
3131
  }
2442
- try {
2443
- const parsedPayload = JSON.parse(Buffer.from(payloadBase64, "base64").toString("utf8"));
2444
- return parsedPayload;
2445
- } catch (err) {
2446
- console.debug("Invalid JWT: could not parse payload", err);
2447
- return null;
2448
- }
2449
- }
2450
- function getExpire() {
2451
- return Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 4; // 4 days
2452
- }
2453
- const JwtHeaderSchema = z.z.object({
2454
- alg: z.z.literal("HS256"),
2455
- typ: z.z.literal("JWT")
2456
- });
2457
- const jwtHeader = {
2458
- alg: "HS256",
2459
- typ: "JWT"
2460
- };
2461
- const jwtHeaderBase64 = Buffer.from(JSON.stringify(jwtHeader)).toString("base64");
2462
- function encodeJwt(payload, sessionKey) {
2463
- // NOTE: this is only used for authentication, not for authorization (i.e. what a user can do) - this is handled when actually doing operations
2464
- const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString("base64");
2465
- return `${jwtHeaderBase64}.${payloadBase64}.${crypto__default["default"].createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
2466
- }
2467
-
2468
- class ProxyValServer extends ValServer {
2469
- moduleCache = null;
2470
- constructor(cwd, valModules, options, apiOptions, callbacks) {
2471
- super(cwd, valModules, options, callbacks);
2472
- this.cwd = cwd;
2473
- this.valModules = valModules;
2474
- this.options = options;
2475
- this.apiOptions = apiOptions;
2476
- this.callbacks = callbacks;
2477
- this.moduleCache = null;
2478
- }
2479
-
2480
- // TODO: restructure this
2481
-
2482
- getSerializedModules() {
2483
- return Promise.all(this.valModules.modules.map(({
2484
- def
2485
- }, i) => {
2486
- return def().then(({
2487
- default: valModule
2488
- }) => {
2489
- var _Internal$getSchema;
2490
- const path = core.Internal.getValPath(valModule);
2491
- if (!path) {
2492
- throw Error(`Module defined at pos: ${i} is missing path`);
2493
- }
2494
- const source = core.Internal.getSource(valModule);
2495
- if (!source) {
2496
- // TODO
2497
- throw Error(`Module defined at pos: ${i} is missing source`);
3132
+ async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
3133
+ const params = new URLSearchParams();
3134
+ params.set("file_path", filePath);
3135
+ try {
3136
+ const metadataRes = await fetch(`${this.hostUrl}/v1/${this.project}/patches/${patchId}/metadata?${params}`, {
3137
+ headers: {
3138
+ ...this.authHeaders,
3139
+ "Content-Type": "application/json"
2498
3140
  }
2499
- const schema = (_Internal$getSchema = core.Internal.getSchema(valModule)) === null || _Internal$getSchema === void 0 ? void 0 : _Internal$getSchema.serialize();
2500
- if (!schema) {
2501
- // TODO
2502
- throw Error(`Module defined at pos: ${i} is missing schema`);
3141
+ });
3142
+ if (metadataRes.ok) {
3143
+ const json = await metadataRes.json();
3144
+ const parsed = MetadataRes.safeParse(json);
3145
+ if (parsed.success) {
3146
+ return {
3147
+ metadata: parsed.data.metadata
3148
+ };
2503
3149
  }
2504
3150
  return {
2505
- path,
2506
- source: source,
2507
- errors: false,
2508
- //valModule[GetSchema]?.validate(path, source),
2509
- schema: schema
3151
+ errors: [{
3152
+ message: `Could not parse metadata response. Error: ${zodValidationError.fromError(parsed.error)}`,
3153
+ filePath
3154
+ }]
2510
3155
  };
2511
- });
2512
- }));
2513
- }
2514
- getModule(moduleId) {
2515
- // TODO: do not get all modules - we only should only get the ones we need
2516
- return this.getSerializedModules().then(all => {
2517
- const found = all.find(valModule => valModule.path === moduleId);
2518
- if (!found) {
2519
- throw Error(`Module ${moduleId} not found`);
2520
3156
  }
2521
- return found;
2522
- });
3157
+ return {
3158
+ errors: [{
3159
+ message: "Could not get metadata. HTTP error: " + metadataRes.status + " " + metadataRes.statusText,
3160
+ filePath
3161
+ }]
3162
+ };
3163
+ } catch (err) {
3164
+ return {
3165
+ errors: [{
3166
+ message: "Could not get metadata (connection error?): " + (err instanceof Error ? err.message : (err === null || err === void 0 ? void 0 : err.toString()) || "unknown error")
3167
+ }]
3168
+ };
3169
+ }
2523
3170
  }
2524
- async getAllModules(treePath) {
2525
- const moduleIds = (await this.getSerializedModules()).filter(({
2526
- path
2527
- }) => {
2528
- if (treePath) {
2529
- return path.startsWith(treePath);
2530
- }
2531
- return true;
2532
- }).map(({
2533
- path
2534
- }) => path);
2535
- return moduleIds;
2536
- }
2537
- execCommit(patches, cookies) {
2538
- return withAuth(this.options.valSecret, cookies, "execCommit", async ({
2539
- token
2540
- }) => {
2541
- const commit = this.options.git.commit;
2542
- if (!commit) {
2543
- return {
2544
- status: 400,
2545
- json: {
2546
- message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
3171
+ async getBinaryFileMetadata(filePath, type) {
3172
+ // TODO: call get metadata on this instance which caches + returns the metadata for this filepath / commit
3173
+ // something like this:
3174
+ // const params = new URLSearchParams();
3175
+ // params.set("path", filePath);
3176
+ // params.set("type", type);
3177
+ // return fetch(new URL(`${this.route}/files/metadata?${params}`, baseUrl)).then(
3178
+ // (res) => {
3179
+ // return res.json();
3180
+ // }
3181
+ // );
3182
+ return {
3183
+ errors: [{
3184
+ message: "Not implemented: " + type,
3185
+ filePath
3186
+ }]
3187
+ };
3188
+ }
3189
+ async deletePatches(patchIds) {
3190
+ return fetch(`${this.hostUrl}/v1/${this.project}/patches`, {
3191
+ method: "DELETE",
3192
+ headers: {
3193
+ ...this.authHeaders,
3194
+ "Content-Type": "application/json"
3195
+ },
3196
+ body: JSON.stringify({
3197
+ patchIds
3198
+ })
3199
+ }).then(async res => {
3200
+ if (res.ok) {
3201
+ const parsed = DeletePatchesResponse.safeParse(await res.json());
3202
+ if (parsed.success) {
3203
+ const errors = {};
3204
+ for (const err of parsed.data.errors || []) {
3205
+ errors[err.patchId] = err;
2547
3206
  }
2548
- };
2549
- }
2550
- const params = createParams({
2551
- root: this.apiOptions.root,
2552
- commit,
2553
- ext: ["ts", "js", "json"],
2554
- package: ["@valbuild/core@" + this.options.versions.core, "@valbuild/next@" + this.options.versions.next],
2555
- include: ["**/*.val.{js,ts},package.json,tsconfig.json,jsconfig.json"]
2556
- });
2557
- const url = new URL(`/v1/commit/${this.options.remote}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
2558
- const patchIds = patches.map(([patchId]) => patchId);
2559
- const fetchRes = await fetch(url, {
2560
- method: "POST",
2561
- headers: getAuthHeaders(token, "application/json"),
2562
- body: JSON.stringify({
2563
- patchIds
2564
- })
2565
- });
2566
- if (fetchRes.status === 200) {
3207
+ if (Object.keys(errors).length === 0) {
3208
+ return {
3209
+ deleted: parsed.data.deleted
3210
+ };
3211
+ }
3212
+ return {
3213
+ deleted: parsed.data.deleted,
3214
+ errors
3215
+ };
3216
+ }
2567
3217
  return {
2568
- status: fetchRes.status,
2569
- json: await fetchRes.json()
3218
+ error: {
3219
+ message: `Could not parse delete patches response. Error: ${zodValidationError.fromError(parsed.error)}`
3220
+ }
2570
3221
  };
2571
- } else {
2572
- return createJsonError(fetchRes);
2573
3222
  }
3223
+ return {
3224
+ error: {
3225
+ message: "Could not delete patches. HTTP error: " + res.status + " " + res.statusText
3226
+ }
3227
+ };
3228
+ }).catch(e => {
3229
+ return {
3230
+ error: {
3231
+ message: `Could not delete patches (connection error?): ${e instanceof Error ? e.message : e.toString()}`
3232
+ }
3233
+ };
2574
3234
  });
2575
3235
  }
2576
- async init(
2577
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2578
- _commit,
2579
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2580
- _token) {
2581
- return {
2582
- status: 200
2583
- };
2584
- }
2585
- async ensureInitialized(errorMessageType, cookies) {
2586
- const commit = this.options.git.commit;
2587
- if (!commit) {
2588
- return fp.result.err({
2589
- status: 400,
2590
- json: {
2591
- message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
2592
- }
3236
+ async commit(prepared, message, committer, newBranch) {
3237
+ try {
3238
+ const existingBranch = this.branch;
3239
+ const res = await fetch(`${this.hostUrl}/v1/${this.project}/commit`, {
3240
+ method: "POST",
3241
+ headers: {
3242
+ ...this.authHeaders,
3243
+ "Content-Type": "application/json"
3244
+ },
3245
+ body: JSON.stringify({
3246
+ patchedSourceFiles: prepared.patchedSourceFiles,
3247
+ patchedBinaryFilesDescriptors: prepared.patchedBinaryFilesDescriptors,
3248
+ appliedPatches: prepared.appliedPatches,
3249
+ commit: this.commitSha,
3250
+ root: this.root,
3251
+ baseSha: await this.getBaseSha(),
3252
+ committer,
3253
+ message,
3254
+ existingBranch,
3255
+ newBranch
3256
+ })
2593
3257
  });
2594
- }
2595
- const res = await withAuth(this.options.valSecret, cookies, errorMessageType, async data => {
2596
- if (!this.moduleCache) {
2597
- return this.init(commit, data.token);
2598
- } else {
3258
+ if (res.ok) {
3259
+ const parsed = CommitResponse.safeParse(await res.json());
3260
+ if (parsed.success) {
3261
+ return {
3262
+ updatedFiles: parsed.data.updatedFiles,
3263
+ commit: parsed.data.commit,
3264
+ branch: parsed.data.branch
3265
+ };
3266
+ }
2599
3267
  return {
2600
- status: 200
3268
+ error: {
3269
+ message: `Could not parse commit response. Error: ${zodValidationError.fromError(parsed.error)}`
3270
+ }
2601
3271
  };
2602
3272
  }
2603
- });
2604
- if (res.status === 200) {
2605
- return fp.result.ok(undefined);
3273
+ return {
3274
+ error: {
3275
+ message: "Could not commit. HTTP error: " + res.status + " " + res.statusText
3276
+ }
3277
+ };
3278
+ } catch (err) {
3279
+ return {
3280
+ error: {
3281
+ message: `Could not commit (connection error?): ${err instanceof Error ? err.message : (err === null || err === void 0 ? void 0 : err.toString()) || "unknown error"}`
3282
+ }
3283
+ };
3284
+ }
3285
+ }
3286
+ }
3287
+
3288
+ /* eslint-disable @typescript-eslint/no-unused-vars */
3289
+ class ValServer {
3290
+ constructor(valModules, options, callbacks) {
3291
+ this.valModules = valModules;
3292
+ this.options = options;
3293
+ this.callbacks = callbacks;
3294
+ if (options.mode === "fs") {
3295
+ this.serverOps = new ValOpsFS(options.cwd, valModules, {
3296
+ formatter: options.formatter
3297
+ });
3298
+ } else if (options.mode === "http") {
3299
+ this.serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
3300
+ formatter: options.formatter,
3301
+ root: options.root
3302
+ });
2606
3303
  } else {
2607
- return fp.result.err(res);
3304
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3305
+ throw new Error("Invalid mode: " + (options === null || options === void 0 ? void 0 : options.mode));
2608
3306
  }
2609
3307
  }
2610
- /* Auth endpoints */
2611
3308
 
3309
+ //#region auth
3310
+ async enable(query) {
3311
+ const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
3312
+ if (typeof redirectToRes !== "string") {
3313
+ return redirectToRes;
3314
+ }
3315
+ await this.callbacks.onEnable(true);
3316
+ return {
3317
+ cookies: {
3318
+ [internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
3319
+ },
3320
+ status: 302,
3321
+ redirectTo: redirectToRes
3322
+ };
3323
+ }
3324
+ async disable(query) {
3325
+ const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
3326
+ if (typeof redirectToRes !== "string") {
3327
+ return redirectToRes;
3328
+ }
3329
+ await this.callbacks.onDisable(true);
3330
+ return {
3331
+ cookies: {
3332
+ [internal.VAL_ENABLE_COOKIE_NAME]: {
3333
+ value: "false"
3334
+ }
3335
+ },
3336
+ status: 302,
3337
+ redirectTo: redirectToRes
3338
+ };
3339
+ }
2612
3340
  async authorize(query) {
2613
3341
  if (typeof query.redirect_to !== "string") {
2614
3342
  return {
@@ -2618,7 +3346,7 @@ class ProxyValServer extends ValServer {
2618
3346
  }
2619
3347
  };
2620
3348
  }
2621
- const token = crypto__default["default"].randomUUID();
3349
+ const token = crypto.randomUUID();
2622
3350
  const redirectUrl = new URL(query.redirect_to);
2623
3351
  const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
2624
3352
  return {
@@ -2640,6 +3368,28 @@ class ProxyValServer extends ValServer {
2640
3368
  };
2641
3369
  }
2642
3370
  async callback(query, cookies) {
3371
+ if (!this.options.project) {
3372
+ return {
3373
+ status: 302,
3374
+ cookies: {
3375
+ [internal.VAL_STATE_COOKIE]: {
3376
+ value: null
3377
+ }
3378
+ },
3379
+ redirectTo: this.getAppErrorUrl("Project is not set")
3380
+ };
3381
+ }
3382
+ if (!this.options.valSecret) {
3383
+ return {
3384
+ status: 302,
3385
+ cookies: {
3386
+ [internal.VAL_STATE_COOKIE]: {
3387
+ value: null
3388
+ }
3389
+ },
3390
+ redirectTo: this.getAppErrorUrl("Secret is not set")
3391
+ };
3392
+ }
2643
3393
  const {
2644
3394
  success: callbackReqSuccess,
2645
3395
  error: callbackReqError
@@ -2668,10 +3418,22 @@ class ProxyValServer extends ValServer {
2668
3418
  };
2669
3419
  }
2670
3420
  const exp = getExpire();
3421
+ const valSecret = this.options.valSecret;
3422
+ if (!valSecret) {
3423
+ return {
3424
+ status: 302,
3425
+ cookies: {
3426
+ [internal.VAL_STATE_COOKIE]: {
3427
+ value: null
3428
+ }
3429
+ },
3430
+ redirectTo: this.getAppErrorUrl("Setup is not correct: secret is missing")
3431
+ };
3432
+ }
2671
3433
  const cookie = encodeJwt({
2672
3434
  ...data,
2673
3435
  exp // this is the client side exp
2674
- }, this.options.valSecret);
3436
+ }, valSecret);
2675
3437
  return {
2676
3438
  status: 302,
2677
3439
  cookies: {
@@ -2693,7 +3455,7 @@ class ProxyValServer extends ValServer {
2693
3455
  redirectTo: callbackReqSuccess.redirect_uri || "/"
2694
3456
  };
2695
3457
  }
2696
- async logout() {
3458
+ async log() {
2697
3459
  return {
2698
3460
  status: 200,
2699
3461
  cookies: {
@@ -2707,8 +3469,41 @@ class ProxyValServer extends ValServer {
2707
3469
  };
2708
3470
  }
2709
3471
  async session(cookies) {
3472
+ if (this.serverOps instanceof ValOpsFS) {
3473
+ return {
3474
+ status: 200,
3475
+ json: {
3476
+ mode: "local",
3477
+ enabled: await this.callbacks.isEnabled()
3478
+ }
3479
+ };
3480
+ }
3481
+ if (!this.options.project) {
3482
+ return {
3483
+ status: 500,
3484
+ json: {
3485
+ message: "Project is not set"
3486
+ }
3487
+ };
3488
+ }
3489
+ if (!this.options.valSecret) {
3490
+ return {
3491
+ status: 500,
3492
+ json: {
3493
+ message: "Secret is not set"
3494
+ }
3495
+ };
3496
+ }
2710
3497
  return withAuth(this.options.valSecret, cookies, "session", async data => {
2711
- const url = new URL(`/api/val/${this.options.remote}/auth/session`, this.options.valBuildUrl);
3498
+ if (!this.options.valBuildUrl) {
3499
+ return {
3500
+ status: 500,
3501
+ json: {
3502
+ message: "Val is not correctly setup. Build url is missing"
3503
+ }
3504
+ };
3505
+ }
3506
+ const url = new URL(`/api/val/${this.options.project}/auth/session`, this.options.valBuildUrl);
2712
3507
  const fetchRes = await fetch(url, {
2713
3508
  headers: getAuthHeaders(data.token, "application/json")
2714
3509
  });
@@ -2733,8 +3528,17 @@ class ProxyValServer extends ValServer {
2733
3528
  });
2734
3529
  }
2735
3530
  async consumeCode(code) {
2736
- const url = new URL(`/api/val/${this.options.remote}/auth/token`, this.options.valBuildUrl);
3531
+ if (!this.options.project) {
3532
+ throw new Error("Project is not set");
3533
+ }
3534
+ if (!this.options.valBuildUrl) {
3535
+ throw new Error("Val build url is not set");
3536
+ }
3537
+ const url = new URL(`/api/val/${this.options.project}/auth/token`, this.options.valBuildUrl);
2737
3538
  url.searchParams.set("code", encodeURIComponent(code));
3539
+ if (!this.options.apiKey) {
3540
+ return null;
3541
+ }
2738
3542
  return fetch(url, {
2739
3543
  method: "POST",
2740
3544
  headers: getAuthHeaders(this.options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
@@ -2758,186 +3562,453 @@ class ProxyValServer extends ValServer {
2758
3562
  return null;
2759
3563
  });
2760
3564
  }
2761
- getAuthorizeUrl(publicValApiRoute, token) {
2762
- const url = new URL(`/auth/${this.options.remote}/authorize`, this.options.valBuildUrl);
2763
- url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRoute}/callback`));
3565
+ getAuthorizeUrl(publicValApiRe, token) {
3566
+ if (!this.options.project) {
3567
+ throw new Error("Project is not set");
3568
+ }
3569
+ if (!this.options.valBuildUrl) {
3570
+ throw new Error("Val build url is not set");
3571
+ }
3572
+ const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
3573
+ url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRe}/callback`));
2764
3574
  url.searchParams.set("state", token);
2765
3575
  return url.toString();
2766
3576
  }
2767
3577
  getAppErrorUrl(error) {
2768
- const url = new URL(`/auth/${this.options.remote}/authorize`, this.options.valBuildUrl);
3578
+ if (!this.options.project) {
3579
+ throw new Error("Project is not set");
3580
+ }
3581
+ if (!this.options.valBuildUrl) {
3582
+ throw new Error("Val build url is not set");
3583
+ }
3584
+ const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
2769
3585
  url.searchParams.set("error", encodeURIComponent(error));
2770
3586
  return url.toString();
2771
3587
  }
3588
+ getAuth(cookies) {
3589
+ const cookie = cookies[internal.VAL_SESSION_COOKIE];
3590
+ if (!this.options.valSecret) {
3591
+ if (this.serverOps instanceof ValOpsFS) {
3592
+ return {
3593
+ error: null,
3594
+ id: null
3595
+ };
3596
+ } else {
3597
+ return {
3598
+ error: "Setup is not correct: secret is missing"
3599
+ };
3600
+ }
3601
+ }
3602
+ if (typeof cookie === "string") {
3603
+ const decodedToken = decodeJwt(cookie, this.options.valSecret);
3604
+ if (!decodedToken) {
3605
+ if (this.serverOps instanceof ValOpsFS) {
3606
+ return {
3607
+ error: null,
3608
+ id: null
3609
+ };
3610
+ }
3611
+ return {
3612
+ error: "Could not verify session (invalid token). You will need to login again."
3613
+ };
3614
+ }
3615
+ const verification = IntegratedServerJwtPayload.safeParse(decodedToken);
3616
+ if (!verification.success) {
3617
+ if (this.serverOps instanceof ValOpsFS) {
3618
+ return {
3619
+ error: null,
3620
+ id: null
3621
+ };
3622
+ }
3623
+ return {
3624
+ error: "Session invalid or, most likely, expired. You will need to login again."
3625
+ };
3626
+ }
3627
+ return {
3628
+ id: verification.data.sub
3629
+ };
3630
+ } else {
3631
+ if (this.serverOps instanceof ValOpsFS) {
3632
+ return {
3633
+ error: null,
3634
+ id: null
3635
+ };
3636
+ }
3637
+ return {
3638
+ error: "Login required: cookie not found"
3639
+ };
3640
+ }
3641
+ }
3642
+ async logout() {
3643
+ return {
3644
+ status: 200,
3645
+ cookies: {
3646
+ [internal.VAL_SESSION_COOKIE]: {
3647
+ value: null
3648
+ },
3649
+ [internal.VAL_STATE_COOKIE]: {
3650
+ value: null
3651
+ }
3652
+ }
3653
+ };
3654
+ }
2772
3655
 
2773
- /* Patch endpoints */
2774
- async deletePatches(
2775
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2776
- query,
2777
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2778
- cookies) {
2779
- return withAuth(this.options.valSecret, cookies, "deletePatches", async ({
2780
- token
2781
- }) => {
2782
- const patchIds = query.id || [];
2783
- const url = new URL(`/v1/patches/${this.options.remote}/heads/${this.options.git.branch}/~`, this.options.valContentUrl);
2784
- const fetchRes = await fetch(url, {
2785
- method: "DELETE",
2786
- headers: getAuthHeaders(token, "application/json"),
2787
- body: JSON.stringify({
2788
- patches: patchIds
2789
- })
3656
+ //#region patches
3657
+ async getPatches(query, cookies) {
3658
+ const auth = this.getAuth(cookies);
3659
+ if (auth.error) {
3660
+ return {
3661
+ status: 401,
3662
+ json: {
3663
+ message: auth.error
3664
+ }
3665
+ };
3666
+ }
3667
+ const authors = query.authors;
3668
+ const patches = await this.serverOps.findPatches({
3669
+ authors
3670
+ });
3671
+ if (patches.errors && Object.keys(patches.errors).length > 0) {
3672
+ console.error("Val: Failed to get patches", patches.errors);
3673
+ return {
3674
+ status: 500,
3675
+ json: {
3676
+ message: "Failed to get patches",
3677
+ details: patches.errors
3678
+ }
3679
+ };
3680
+ }
3681
+ const res = {};
3682
+ for (const [patchIdS, patchData] of Object.entries(patches.patches)) {
3683
+ var _patchData$appliedAt;
3684
+ const patchId = patchIdS;
3685
+ if (!res[patchData.path]) {
3686
+ res[patchData.path] = [];
3687
+ }
3688
+ res[patchData.path].push({
3689
+ patch_id: patchId,
3690
+ created_at: patchData.createdAt,
3691
+ applied_at_base_sha: ((_patchData$appliedAt = patchData.appliedAt) === null || _patchData$appliedAt === void 0 ? void 0 : _patchData$appliedAt.baseSha) || null,
3692
+ author: patchData.authorId ?? undefined
2790
3693
  });
2791
- if (fetchRes.status === 200) {
2792
- return {
2793
- status: fetchRes.status,
2794
- json: await fetchRes.json()
3694
+ }
3695
+ return {
3696
+ status: 200,
3697
+ json: res
3698
+ };
3699
+ }
3700
+ async deletePatches(query, cookies) {
3701
+ const auth = this.getAuth(cookies);
3702
+ if (auth.error) {
3703
+ return {
3704
+ status: 401,
3705
+ json: {
3706
+ message: auth.error
3707
+ }
3708
+ };
3709
+ }
3710
+ if (this.options.mode === "http" && !("id" in auth)) {
3711
+ return {
3712
+ status: 401,
3713
+ json: {
3714
+ message: "Unauthorized"
3715
+ }
3716
+ };
3717
+ }
3718
+ const ids = query.id;
3719
+ const deleteRes = await this.serverOps.deletePatches(ids);
3720
+ if (deleteRes.errors && Object.keys(deleteRes.errors).length > 0) {
3721
+ console.error("Val: Failed to delete patches", deleteRes.errors);
3722
+ return {
3723
+ status: 500,
3724
+ json: {
3725
+ message: "Failed to delete patches",
3726
+ details: deleteRes.errors
3727
+ }
3728
+ };
3729
+ }
3730
+ return {
3731
+ status: 200,
3732
+ json: ids
3733
+ };
3734
+ }
3735
+
3736
+ //#region tree ops
3737
+ async getSchema(cookies) {
3738
+ const auth = this.getAuth(cookies);
3739
+ if (auth.error) {
3740
+ return {
3741
+ status: 401,
3742
+ json: {
3743
+ message: auth.error
3744
+ }
3745
+ };
3746
+ }
3747
+ const moduleErrors = await this.serverOps.getModuleErrors();
3748
+ if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
3749
+ console.error("Val: Module errors", moduleErrors);
3750
+ return {
3751
+ status: 500,
3752
+ json: {
3753
+ message: "Val is not correctly setup. Check the val.modules file",
3754
+ details: moduleErrors
3755
+ }
3756
+ };
3757
+ }
3758
+ const schemaSha = await this.serverOps.getSchemaSha();
3759
+ const schemas = await this.serverOps.getSchemas();
3760
+ const serializedSchemas = {};
3761
+ for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
3762
+ const moduleFilePath = moduleFilePathS;
3763
+ serializedSchemas[moduleFilePath] = schema.serialize();
3764
+ }
3765
+ return {
3766
+ status: 200,
3767
+ json: {
3768
+ schemaSha,
3769
+ schemas: serializedSchemas
3770
+ }
3771
+ };
3772
+ }
3773
+ async putTree(body, treePath, query, cookies) {
3774
+ var _bodyRes$data, _bodyRes$data2, _bodyRes$data3;
3775
+ const auth = this.getAuth(cookies);
3776
+ if (auth.error) {
3777
+ return {
3778
+ status: 401,
3779
+ json: {
3780
+ message: auth.error
3781
+ }
3782
+ };
3783
+ }
3784
+ // TODO: move
3785
+ const PutTreeBody = z.z.object({
3786
+ patchIds: z.z.array(z.z.string().refine(id => true // TODO:
3787
+ )).optional(),
3788
+ addPatch: z.z.object({
3789
+ path: z.z.string().refine(path => true // TODO:
3790
+ ),
3791
+ patch: Patch
3792
+ }).optional()
3793
+ }).optional();
3794
+ const moduleErrors = await this.serverOps.getModuleErrors();
3795
+ if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
3796
+ console.error("Val: Module errors", moduleErrors);
3797
+ return {
3798
+ status: 500,
3799
+ json: {
3800
+ message: "Val is not correctly setup. Check the val.modules file",
3801
+ details: moduleErrors
3802
+ }
3803
+ };
3804
+ }
3805
+ const bodyRes = PutTreeBody.safeParse(body);
3806
+ if (!bodyRes.success) {
3807
+ return {
3808
+ status: 400,
3809
+ json: {
3810
+ message: "Invalid body: " + zodValidationError.fromError(bodyRes.error).toString(),
3811
+ details: bodyRes.error.errors
3812
+ }
3813
+ };
3814
+ }
3815
+ let tree;
3816
+ let patchAnalysis = null;
3817
+ if ((_bodyRes$data = bodyRes.data) !== null && _bodyRes$data !== void 0 && _bodyRes$data.patchIds && ((_bodyRes$data2 = bodyRes.data) === null || _bodyRes$data2 === void 0 || (_bodyRes$data2 = _bodyRes$data2.patchIds) === null || _bodyRes$data2 === void 0 ? void 0 : _bodyRes$data2.length) > 0 || (_bodyRes$data3 = bodyRes.data) !== null && _bodyRes$data3 !== void 0 && _bodyRes$data3.addPatch) {
3818
+ var _bodyRes$data4, _bodyRes$data5;
3819
+ // TODO: validate patches_sha
3820
+ const patchIds = (_bodyRes$data4 = bodyRes.data) === null || _bodyRes$data4 === void 0 ? void 0 : _bodyRes$data4.patchIds;
3821
+ const patchOps = patchIds && patchIds.length > 0 ? await this.serverOps.getPatchOpsById(patchIds) : {
3822
+ patches: {}
3823
+ };
3824
+ let patchErrors = undefined;
3825
+ for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
3826
+ const patchId = patchIdS;
3827
+ if (!patchErrors) {
3828
+ patchErrors = {};
3829
+ }
3830
+ patchErrors[patchId] = {
3831
+ message: error.message
3832
+ };
3833
+ }
3834
+ if ((_bodyRes$data5 = bodyRes.data) !== null && _bodyRes$data5 !== void 0 && _bodyRes$data5.addPatch) {
3835
+ const newPatchModuleFilePath = bodyRes.data.addPatch.path;
3836
+ const newPatchOps = bodyRes.data.addPatch.patch;
3837
+ const authorId = null; // TODO:
3838
+ const createPatchRes = await this.serverOps.createPatch(newPatchModuleFilePath, newPatchOps, authorId);
3839
+ if (createPatchRes.error) {
3840
+ return {
3841
+ status: 500,
3842
+ json: {
3843
+ message: "Failed to create patch: " + createPatchRes.error.message,
3844
+ details: createPatchRes.error
3845
+ }
3846
+ };
3847
+ }
3848
+ for (const fileRes of createPatchRes.files) {
3849
+ if (fileRes.error) {
3850
+ // clean up broken patch:
3851
+ await this.serverOps.deletePatches([createPatchRes.patchId]);
3852
+ return {
3853
+ status: 500,
3854
+ json: {
3855
+ message: "Failed to create patch",
3856
+ details: fileRes.error
3857
+ }
3858
+ };
3859
+ }
3860
+ }
3861
+ patchOps.patches[createPatchRes.patchId] = {
3862
+ path: newPatchModuleFilePath,
3863
+ patch: newPatchOps,
3864
+ authorId,
3865
+ createdAt: createPatchRes.createdAt,
3866
+ appliedAt: null
2795
3867
  };
2796
- } else {
2797
- return createJsonError(fetchRes);
2798
3868
  }
2799
- });
2800
- }
2801
- async getPatches(query, cookies) {
2802
- return withAuth(this.options.valSecret, cookies, "getPatches", async ({
2803
- token
2804
- }) => {
2805
- const commit = this.options.git.commit;
2806
- if (!commit) {
2807
- return {
2808
- status: 400,
2809
- json: {
2810
- message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
3869
+ // TODO: errors
3870
+ patchAnalysis = this.serverOps.analyzePatches(patchOps.patches);
3871
+ tree = {
3872
+ ...(await this.serverOps.getTree({
3873
+ ...patchAnalysis,
3874
+ ...patchOps
3875
+ }))
3876
+ };
3877
+ if (query.validate_all === "true") {
3878
+ const allTree = await this.serverOps.getTree();
3879
+ tree = {
3880
+ sources: {
3881
+ ...allTree.sources,
3882
+ ...tree.sources
3883
+ },
3884
+ errors: {
3885
+ ...allTree.errors,
3886
+ ...tree.errors
2811
3887
  }
2812
3888
  };
2813
3889
  }
2814
- const patchIds = query.id || [];
2815
- const params = patchIds.length > 0 ? `commit=${encodeURIComponent(commit)}&${patchIds.map(id => `id=${encodeURIComponent(id)}`).join("&")}` : `commit=${encodeURIComponent(commit)}`;
2816
- const url = new URL(`/v1/patches/${this.options.remote}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
2817
- // Proxy patch to val.build
2818
- const fetchRes = await fetch(url, {
2819
- method: "GET",
2820
- headers: getAuthHeaders(token, "application/json")
2821
- });
2822
- if (fetchRes.status === 200) {
2823
- return {
2824
- status: fetchRes.status,
2825
- json: await fetchRes.json()
3890
+ } else {
3891
+ tree = await this.serverOps.getTree();
3892
+ }
3893
+ if (tree.errors && Object.keys(tree.errors).length > 0) {
3894
+ console.error("Val: Failed to get tree", JSON.stringify(tree.errors));
3895
+ }
3896
+ if (query.validate_sources === "true" || query.validate_binary_files === "true") {
3897
+ const schemas = await this.serverOps.getSchemas();
3898
+ const sourcesValidation = await this.serverOps.validateSources(schemas, tree.sources);
3899
+
3900
+ // TODO: send validation errors
3901
+ if (query.validate_binary_files === "true") {
3902
+ await this.serverOps.validateFiles(schemas, tree.sources, sourcesValidation.files);
3903
+ }
3904
+ }
3905
+ const schemaSha = await this.serverOps.getSchemaSha();
3906
+ const modules = {};
3907
+ for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
3908
+ const moduleFilePath = moduleFilePathS;
3909
+ if (moduleFilePath.startsWith(treePath)) {
3910
+ modules[moduleFilePath] = {
3911
+ source: module,
3912
+ patches: patchAnalysis ? {
3913
+ applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
3914
+ } : undefined
2826
3915
  };
2827
- } else {
2828
- return createJsonError(fetchRes);
2829
3916
  }
2830
- });
3917
+ }
3918
+ return {
3919
+ status: 200,
3920
+ json: {
3921
+ schemaSha,
3922
+ modules
3923
+ }
3924
+ };
2831
3925
  }
2832
- async postPatches(body, cookies) {
2833
- const commit = this.options.git.commit;
2834
- if (!commit) {
3926
+ async postSave(body, cookies) {
3927
+ const auth = this.getAuth(cookies);
3928
+ if (auth.error) {
2835
3929
  return {
2836
3930
  status: 401,
2837
3931
  json: {
2838
- message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
3932
+ message: auth.error
2839
3933
  }
2840
3934
  };
2841
3935
  }
2842
- const params = new URLSearchParams({
2843
- commit
3936
+ const PostSaveBody = z.z.object({
3937
+ patchIds: z.z.array(z.z.string().refine(id => true // TODO:
3938
+ ))
2844
3939
  });
2845
- return withAuth(this.options.valSecret, cookies, "postPatches", async ({
2846
- token
2847
- }) => {
2848
- // First validate that the body has the right structure
2849
- const parsedPatches = z.z.record(Patch).safeParse(body);
2850
- if (!parsedPatches.success) {
2851
- return {
2852
- status: 400,
2853
- json: {
2854
- message: "Invalid patch(es)",
2855
- details: parsedPatches.error.issues
2856
- }
2857
- };
2858
- }
2859
- const patches = parsedPatches.data;
2860
- const url = new URL(`/v1/patches/${this.options.remote}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
2861
- // Proxy patch to val.build
2862
- const fetchRes = await fetch(url, {
2863
- method: "POST",
2864
- headers: getAuthHeaders(token, "application/json"),
2865
- body: JSON.stringify(patches)
2866
- });
2867
- if (fetchRes.status === 200) {
3940
+ const bodyRes = PostSaveBody.safeParse(body);
3941
+ if (!bodyRes.success) {
3942
+ return {
3943
+ status: 400,
3944
+ json: {
3945
+ message: "Invalid body: " + zodValidationError.fromError(bodyRes.error).toString(),
3946
+ details: bodyRes.error.errors
3947
+ }
3948
+ };
3949
+ }
3950
+ const {
3951
+ patchIds
3952
+ } = bodyRes.data;
3953
+ const patches = await this.serverOps.getPatchOpsById(patchIds);
3954
+ const analysis = this.serverOps.analyzePatches(patches.patches);
3955
+ const preparedCommit = await this.serverOps.prepare({
3956
+ ...analysis,
3957
+ ...patches
3958
+ });
3959
+ if (this.serverOps instanceof ValOpsFS) {
3960
+ await this.serverOps.saveFiles(preparedCommit);
3961
+ return {
3962
+ status: 200,
3963
+ json: {} // TODO:
3964
+ };
3965
+ } else if (this.serverOps instanceof ValOpsHttp) {
3966
+ if (auth.error === undefined && auth.id) {
3967
+ await this.serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
2868
3968
  return {
2869
- status: fetchRes.status,
2870
- json: await fetchRes.json()
3969
+ status: 200,
3970
+ json: {} // TODO:
2871
3971
  };
2872
- } else {
2873
- return createJsonError(fetchRes);
2874
- }
2875
- });
2876
- }
2877
- async getMetadata(filePath, sha256) {
2878
- const url = new URL(`/v1/metadata/${this.options.remote}${filePath}?commit=${this.options.git.commit}${sha256 ? `&sha256=${sha256}` : ""}`, this.options.valContentUrl);
2879
- const fetchRes = await fetch(url, {
2880
- headers: {
2881
- Authorization: `Bearer ${this.options.apiKey}`
2882
- }
2883
- });
2884
- if (fetchRes.status === 200) {
2885
- const json = await fetchRes.json();
2886
- if (json.type === "file") {
2887
- return json;
2888
- } else if (json.type === "image") {
2889
- return json;
2890
3972
  }
3973
+ return {
3974
+ status: 401,
3975
+ json: {
3976
+ message: "Unauthorized"
3977
+ }
3978
+ };
3979
+ } else {
3980
+ throw new Error("Invalid server ops");
2891
3981
  }
2892
- return undefined;
2893
3982
  }
2894
- async getFiles(filePath, query, _cookies, reqHeaders) {
2895
- const url = new URL(`/v1/files/${this.options.remote}${filePath}`, this.options.valContentUrl);
2896
- if (typeof query.sha256 === "string") {
2897
- url.searchParams.append("sha256", query.sha256);
3983
+
3984
+ //#region files
3985
+ async getFiles(filePath, query) {
3986
+ // NOTE: no auth here since you would need the patch_id to get something that is not published.
3987
+ // For everything that is published, well they are already public so no auth required there...
3988
+ // We could imagine adding auth just to be a 200% certain,
3989
+ // However that won't work since images are requested by the nextjs backend as a part of image optimization (again: as an example) which is a backend-to-backend op (no cookies, ...).
3990
+ // So: 1) patch ids are not possible to guess (but possible to brute force)
3991
+ // 2) the process of shimming a patch into the frontend would be quite challenging (so just trying out this attack would require a lot of effort)
3992
+ // 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
3993
+ // Thus: attack surface + ease of attack + benefit = low probability of attack
3994
+ // If we couldn't argue that patch ids are secret enough, then this would be a problem.
3995
+ let fileBuffer;
3996
+ if (query.patch_id) {
3997
+ fileBuffer = await this.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
3998
+ } else {
3999
+ fileBuffer = await this.serverOps.getBinaryFile(filePath);
2898
4000
  }
2899
- const fetchRes = await fetch(url, {
2900
- headers: {
2901
- Authorization: `Bearer ${this.options.apiKey}`
2902
- }
2903
- });
2904
- if (fetchRes.status === 200) {
2905
- // TODO: does this stream data?
2906
- if (fetchRes.body) {
2907
- return {
2908
- status: fetchRes.status,
2909
- headers: {
2910
- "Content-Type": fetchRes.headers.get("Content-Type") || "",
2911
- "Content-Length": fetchRes.headers.get("Content-Length") || "0",
2912
- "Cache-Control": fetchRes.headers.get("Cache-Control") || ""
2913
- },
2914
- body: fetchRes.body
2915
- };
2916
- } else {
2917
- return {
2918
- status: 500,
2919
- json: {
2920
- message: "No body in response"
2921
- }
2922
- };
2923
- }
4001
+ if (fileBuffer) {
4002
+ return {
4003
+ status: 200,
4004
+ body: bufferToReadableStream(fileBuffer)
4005
+ };
2924
4006
  } else {
2925
- if (!(reqHeaders.host && reqHeaders["x-forwarded-proto"])) {
2926
- return {
2927
- status: 500,
2928
- json: {
2929
- message: "Missing host or x-forwarded-proto header"
2930
- }
2931
- };
2932
- }
2933
- const host = `${reqHeaders["x-forwarded-proto"]}://${reqHeaders["host"]}`;
2934
- const staticPublicUrl = new URL(filePath.slice("/public".length), host).toString();
2935
- const fetchRes = await fetch(staticPublicUrl);
2936
4007
  return {
2937
- status: fetchRes.status,
2938
- // forward headers from static public url
2939
- headers: Object.fromEntries(fetchRes.headers.entries()),
2940
- body: fetchRes.body
4008
+ status: 404,
4009
+ json: {
4010
+ message: "File not found"
4011
+ }
2941
4012
  };
2942
4013
  }
2943
4014
  }
@@ -3042,22 +4113,6 @@ function getStateFromCookie(stateCookie) {
3042
4113
  };
3043
4114
  }
3044
4115
  }
3045
- async function createJsonError(fetchRes) {
3046
- var _fetchRes$headers$get;
3047
- if ((_fetchRes$headers$get = fetchRes.headers.get("Content-Type")) !== null && _fetchRes$headers$get !== void 0 && _fetchRes$headers$get.includes("application/json")) {
3048
- return {
3049
- status: fetchRes.status,
3050
- json: await fetchRes.json()
3051
- };
3052
- }
3053
- console.error("Unexpected failure (did not get a json) - Val down?", fetchRes.status, await fetchRes.text());
3054
- return {
3055
- status: fetchRes.status,
3056
- json: {
3057
- message: "Unexpected failure (did not get a json) - Val down?"
3058
- }
3059
- };
3060
- }
3061
4116
  function createStateCookie(state) {
3062
4117
  return Buffer.from(JSON.stringify(state), "utf8").toString("base64");
3063
4118
  }
@@ -3129,42 +4184,147 @@ function getAuthHeaders(token, type) {
3129
4184
  Authorization: `Bearer ${token}`
3130
4185
  };
3131
4186
  }
3132
- function createParams(params) {
3133
- let paramIdx = 0;
3134
- let paramsString = "";
3135
- for (const key in params) {
3136
- const param = params[key];
3137
- if (Array.isArray(param)) {
3138
- for (const value of param) {
3139
- paramsString += `${key}=${encodeURIComponent(value)}&`;
4187
+ const ENABLE_COOKIE_VALUE = {
4188
+ value: "true",
4189
+ options: {
4190
+ httpOnly: false,
4191
+ sameSite: "lax"
4192
+ }
4193
+ };
4194
+ const chunkSize = 1024 * 1024;
4195
+ function bufferToReadableStream(buffer) {
4196
+ const stream = new ReadableStream({
4197
+ start(controller) {
4198
+ let offset = 0;
4199
+ while (offset < buffer.length) {
4200
+ const chunk = buffer.subarray(offset, offset + chunkSize);
4201
+ controller.enqueue(chunk);
4202
+ offset += chunkSize;
3140
4203
  }
3141
- } else if (param) {
3142
- paramsString += `${key}=${encodeURIComponent(param)}`;
3143
- }
3144
- if (paramIdx < Object.keys(params).length - 1) {
3145
- paramsString += "&";
4204
+ controller.close();
3146
4205
  }
3147
- paramIdx++;
4206
+ });
4207
+ return stream;
4208
+ }
4209
+ function getRedirectUrl(query, overrideHost) {
4210
+ if (typeof query.redirect_to !== "string") {
4211
+ return {
4212
+ status: 400,
4213
+ json: {
4214
+ message: "Missing redirect_to query param"
4215
+ }
4216
+ };
3148
4217
  }
3149
- return paramsString;
4218
+ if (overrideHost) {
4219
+ return overrideHost + "?redirect_to=" + encodeURIComponent(query.redirect_to);
4220
+ }
4221
+ return query.redirect_to;
3150
4222
  }
3151
4223
 
3152
- async function createValServer(valModules, route, opts, callbacks) {
3153
- const serverOpts = await initHandlerOptions(route, opts);
3154
- if (serverOpts.mode === "proxy") {
3155
- const projectRoot = process.cwd(); //[process.cwd(), opts.root || ""] .filter((seg) => seg) .join("/");
3156
- return new ProxyValServer(projectRoot, valModules, serverOpts, opts, callbacks);
3157
- } else {
3158
- return new LocalValServer(valModules, serverOpts, callbacks);
4224
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
4225
+ const COMMON_MIME_TYPES = {
4226
+ aac: "audio/aac",
4227
+ abw: "application/x-abiword",
4228
+ arc: "application/x-freearc",
4229
+ avif: "image/avif",
4230
+ avi: "video/x-msvideo",
4231
+ azw: "application/vnd.amazon.ebook",
4232
+ bin: "application/octet-stream",
4233
+ bmp: "image/bmp",
4234
+ bz: "application/x-bzip",
4235
+ bz2: "application/x-bzip2",
4236
+ cda: "application/x-cdf",
4237
+ csh: "application/x-csh",
4238
+ css: "text/css",
4239
+ csv: "text/csv",
4240
+ doc: "application/msword",
4241
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
4242
+ eot: "application/vnd.ms-fontobject",
4243
+ epub: "application/epub+zip",
4244
+ gz: "application/gzip",
4245
+ gif: "image/gif",
4246
+ htm: "text/html",
4247
+ html: "text/html",
4248
+ ico: "image/vnd.microsoft.icon",
4249
+ ics: "text/calendar",
4250
+ jar: "application/java-archive",
4251
+ jpeg: "image/jpeg",
4252
+ jpg: "image/jpeg",
4253
+ js: "text/javascript",
4254
+ json: "application/json",
4255
+ jsonld: "application/ld+json",
4256
+ mid: "audio/midi",
4257
+ midi: "audio/midi",
4258
+ mjs: "text/javascript",
4259
+ mp3: "audio/mpeg",
4260
+ mp4: "video/mp4",
4261
+ mpeg: "video/mpeg",
4262
+ mpkg: "application/vnd.apple.installer+xml",
4263
+ odp: "application/vnd.oasis.opendocument.presentation",
4264
+ ods: "application/vnd.oasis.opendocument.spreadsheet",
4265
+ odt: "application/vnd.oasis.opendocument.text",
4266
+ oga: "audio/ogg",
4267
+ ogv: "video/ogg",
4268
+ ogx: "application/ogg",
4269
+ opus: "audio/opus",
4270
+ otf: "font/otf",
4271
+ png: "image/png",
4272
+ pdf: "application/pdf",
4273
+ php: "application/x-httpd-php",
4274
+ ppt: "application/vnd.ms-powerpoint",
4275
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
4276
+ rar: "application/vnd.rar",
4277
+ rtf: "application/rtf",
4278
+ sh: "application/x-sh",
4279
+ svg: "image/svg+xml",
4280
+ tar: "application/x-tar",
4281
+ tif: "image/tiff",
4282
+ tiff: "image/tiff",
4283
+ ts: "video/mp2t",
4284
+ ttf: "font/ttf",
4285
+ txt: "text/plain",
4286
+ vsd: "application/vnd.visio",
4287
+ wav: "audio/wav",
4288
+ weba: "audio/webm",
4289
+ webm: "video/webm",
4290
+ webp: "image/webp",
4291
+ woff: "font/woff",
4292
+ woff2: "font/woff2",
4293
+ xhtml: "application/xhtml+xml",
4294
+ xls: "application/vnd.ms-excel",
4295
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
4296
+ xml: "application/xml",
4297
+ xul: "application/vnd.mozilla.xul+xml",
4298
+ zip: "application/zip",
4299
+ "3gp": "video/3gpp; audio/3gpp if it doesn't contain video",
4300
+ "3g2": "video/3gpp2; audio/3gpp2 if it doesn't contain video",
4301
+ "7z": "application/x-7z-compressed"
4302
+ };
4303
+ function guessMimeTypeFromPath(filePath) {
4304
+ const fileExt = filePath.split(".").pop();
4305
+ if (fileExt) {
4306
+ return COMMON_MIME_TYPES[fileExt.toLowerCase()] || null;
3159
4307
  }
4308
+ return null;
4309
+ }
4310
+
4311
+ async function createValServer(valModules, route, opts, callbacks, formatter) {
4312
+ const valServerConfig = await initHandlerOptions(route, opts);
4313
+ return new ValServer(valModules, {
4314
+ formatter,
4315
+ ...valServerConfig
4316
+ }, callbacks);
3160
4317
  }
3161
4318
  async function initHandlerOptions(route, opts) {
3162
4319
  const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
3163
4320
  const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
3164
4321
  const isProxyMode = opts.mode === "proxy" || opts.mode === undefined && (maybeApiKey || maybeValSecret);
4322
+ const valEnableRedirectUrl = opts.valEnableRedirectUrl || process.env.VAL_ENABLE_REDIRECT_URL;
4323
+ const valDisableRedirectUrl = opts.valDisableRedirectUrl || process.env.VAL_DISABLE_REDIRECT_URL;
4324
+ const maybeValProject = opts.project || process.env.VAL_PROJECT;
4325
+ const valBuildUrl = opts.valBuildUrl || process.env.VAL_BUILD_URL || "https://app.val.build";
3165
4326
  if (isProxyMode) {
3166
4327
  var _opts$versions, _opts$versions2;
3167
- const valBuildUrl = opts.valBuildUrl || process.env.VAL_BUILD_URL || "https://app.val.build";
3168
4328
  if (!maybeApiKey || !maybeValSecret) {
3169
4329
  throw new Error("VAL_API_KEY and VAL_SECRET env vars must both be set in proxy mode");
3170
4330
  }
@@ -3177,9 +4337,8 @@ async function initHandlerOptions(route, opts) {
3177
4337
  if (!maybeGitBranch) {
3178
4338
  throw new Error("VAL_GIT_BRANCH env var must be set in proxy mode");
3179
4339
  }
3180
- const maybeValRemote = opts.remote || process.env.VAL_REMOTE;
3181
- if (!maybeValRemote) {
3182
- throw new Error("Proxy mode does not work unless the 'remote' option in val.config is defined or the VAL_REMOTE env var is set.");
4340
+ if (!maybeValProject) {
4341
+ throw new Error("Proxy mode does not work unless the 'project' option in val.config is defined or the VAL_PROJECT env var is set.");
3183
4342
  }
3184
4343
  const coreVersion = (_opts$versions = opts.versions) === null || _opts$versions === void 0 ? void 0 : _opts$versions.core;
3185
4344
  if (!coreVersion) {
@@ -3190,43 +4349,40 @@ async function initHandlerOptions(route, opts) {
3190
4349
  throw new Error("Could not determine version of @valbuild/next");
3191
4350
  }
3192
4351
  return {
3193
- mode: "proxy",
4352
+ mode: "http",
3194
4353
  route,
3195
4354
  apiKey: maybeApiKey,
3196
4355
  valSecret: maybeValSecret,
3197
- valBuildUrl,
4356
+ commit: maybeGitCommit,
4357
+ branch: maybeGitBranch,
4358
+ root: opts.root,
4359
+ project: maybeValProject,
4360
+ valEnableRedirectUrl,
4361
+ valDisableRedirectUrl,
3198
4362
  valContentUrl,
3199
- versions: {
3200
- core: coreVersion,
3201
- next: nextVersion
3202
- },
3203
- git: {
3204
- commit: maybeGitCommit,
3205
- branch: maybeGitBranch
3206
- },
3207
- remote: maybeValRemote,
3208
- valEnableRedirectUrl: opts.valEnableRedirectUrl || process.env.VAL_ENABLE_REDIRECT_URL,
3209
- valDisableRedirectUrl: opts.valDisableRedirectUrl || process.env.VAL_DISABLE_REDIRECT_URL
4363
+ valBuildUrl
3210
4364
  };
3211
4365
  } else {
3212
4366
  const cwd = process.cwd();
3213
- const service = await createService(cwd, opts);
3214
- const git = await safeReadGit(cwd);
4367
+ const valBuildUrl = opts.valBuildUrl || process.env.VAL_BUILD_URL || "https://app.val.build";
3215
4368
  return {
3216
- mode: "local",
3217
- service,
3218
- valEnableRedirectUrl: opts.valEnableRedirectUrl || process.env.VAL_ENABLE_REDIRECT_URL,
3219
- valDisableRedirectUrl: opts.valDisableRedirectUrl || process.env.VAL_DISABLE_REDIRECT_URL,
3220
- git: {
3221
- commit: process.env.VAL_GIT_COMMIT || git.commit,
3222
- branch: process.env.VAL_GIT_BRANCH || git.branch
3223
- }
4369
+ mode: "fs",
4370
+ cwd,
4371
+ route,
4372
+ valDisableRedirectUrl,
4373
+ valEnableRedirectUrl,
4374
+ valBuildUrl,
4375
+ apiKey: maybeApiKey,
4376
+ valSecret: maybeValSecret,
4377
+ project: maybeValProject
3224
4378
  };
3225
4379
  }
3226
4380
  }
4381
+
4382
+ // TODO: remove
3227
4383
  async function safeReadGit(cwd) {
3228
4384
  async function findGitHead(currentDir, depth) {
3229
- const gitHeadPath = path__namespace.join(currentDir, ".git", "HEAD");
4385
+ const gitHeadPath = fsPath__namespace.join(currentDir, ".git", "HEAD");
3230
4386
  if (depth > 1000) {
3231
4387
  console.error(`Reached max depth while scanning for .git folder. Current working dir: ${cwd}.`);
3232
4388
  return {
@@ -3250,7 +4406,7 @@ async function safeReadGit(cwd) {
3250
4406
  };
3251
4407
  }
3252
4408
  } catch (error) {
3253
- const parentDir = path__namespace.dirname(currentDir);
4409
+ const parentDir = fsPath__namespace.dirname(currentDir);
3254
4410
 
3255
4411
  // We've reached the root directory
3256
4412
  if (parentDir === currentDir) {
@@ -3274,7 +4430,7 @@ async function safeReadGit(cwd) {
3274
4430
  }
3275
4431
  async function readCommit(gitDir, branchName) {
3276
4432
  try {
3277
- return (await fs.promises.readFile(path__namespace.join(gitDir, ".git", "refs", "heads", branchName), "utf-8")).trim();
4433
+ return (await fs.promises.readFile(fsPath__namespace.join(gitDir, ".git", "refs", "heads", branchName), "utf-8")).trim();
3278
4434
  } catch (err) {
3279
4435
  return undefined;
3280
4436
  }
@@ -3291,10 +4447,6 @@ function createValApiRouter(route, valServerPromise, convert) {
3291
4447
  return async req => {
3292
4448
  var _req$method;
3293
4449
  const valServer = await valServerPromise;
3294
- const requestHeaders = {
3295
- host: req.headers.get("host"),
3296
- "x-forwarded-proto": req.headers.get("x-forwarded-proto")
3297
- };
3298
4450
  const url = new URL(req.url);
3299
4451
  if (!url.pathname.startsWith(route)) {
3300
4452
  const error = {
@@ -3355,35 +4507,40 @@ function createValApiRouter(route, valServerPromise, convert) {
3355
4507
  return convert(await valServer.disable({
3356
4508
  redirect_to: url.searchParams.get("redirect_to") || undefined
3357
4509
  }));
3358
- } else if (method === "POST" && path === "/commit") {
3359
- const body = await req.json();
3360
- return convert(await valServer.postCommit(body, getCookies(req, [VAL_SESSION_COOKIE]), requestHeaders));
3361
- } else if (method === "POST" && path === "/validate") {
4510
+ } else if (method === "POST" && path === "/save") {
3362
4511
  const body = await req.json();
3363
- return convert(await valServer.postValidate(body, getCookies(req, [VAL_SESSION_COOKIE]), requestHeaders));
3364
- } else if (method === "GET" && path.startsWith(TREE_PATH_PREFIX)) {
3365
- return withTreePath(path, TREE_PATH_PREFIX)(async treePath => convert(await valServer.getTree(treePath, {
3366
- patch: url.searchParams.get("patch") || undefined,
3367
- schema: url.searchParams.get("schema") || undefined,
3368
- source: url.searchParams.get("source") || undefined,
3369
- validate: url.searchParams.get("validate") || undefined
3370
- }, getCookies(req, [VAL_SESSION_COOKIE]), requestHeaders)));
4512
+ return convert(await valServer.postSave(body, getCookies(req, [VAL_SESSION_COOKIE])));
4513
+ // } else if (method === "POST" && path === "/validate") {
4514
+ // const body = (await req.json()) as unknown;
4515
+ // return convert(
4516
+ // await valServer.postValidate(
4517
+ // body,
4518
+ // getCookies(req, [VAL_SESSION_COOKIE]),
4519
+ // requestHeaders
4520
+ // )
4521
+ // );
4522
+ } else if (method === "GET" && path === "/schema") {
4523
+ return convert(await valServer.getSchema(getCookies(req, [VAL_SESSION_COOKIE])));
4524
+ } else if (method === "PUT" && path.startsWith(TREE_PATH_PREFIX)) {
4525
+ return withTreePath(path, TREE_PATH_PREFIX)(async treePath => convert(await valServer.putTree(await req.json(), treePath, {
4526
+ patches_sha: url.searchParams.get("patches_sha") || undefined,
4527
+ validate_all: url.searchParams.get("validate_all") || undefined,
4528
+ validate_binary_files: url.searchParams.get("validate_binary_files") || undefined,
4529
+ validate_sources: url.searchParams.get("validate_sources") || undefined
4530
+ }, getCookies(req, [VAL_SESSION_COOKIE]))));
3371
4531
  } else if (method === "GET" && path.startsWith(PATCHES_PATH_PREFIX)) {
3372
4532
  return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.getPatches({
3373
- id: url.searchParams.getAll("id")
4533
+ authors: url.searchParams.getAll("author")
3374
4534
  }, getCookies(req, [VAL_SESSION_COOKIE]))));
3375
- } else if (method === "POST" && path.startsWith(PATCHES_PATH_PREFIX)) {
3376
- const body = await req.json();
3377
- return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.postPatches(body, getCookies(req, [VAL_SESSION_COOKIE]))));
3378
4535
  } else if (method === "DELETE" && path.startsWith(PATCHES_PATH_PREFIX)) {
3379
4536
  return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.deletePatches({
3380
4537
  id: url.searchParams.getAll("id")
3381
4538
  }, getCookies(req, [VAL_SESSION_COOKIE]))));
3382
- } else if (path.startsWith(FILES_PATH_PREFIX)) {
4539
+ } else if (method === "GET" && path.startsWith(FILES_PATH_PREFIX)) {
3383
4540
  const treePath = path.slice(FILES_PATH_PREFIX.length);
3384
4541
  return convert(await valServer.getFiles(treePath, {
3385
- sha256: url.searchParams.get("sha256") || undefined
3386
- }, getCookies(req, [VAL_SESSION_COOKIE]), requestHeaders));
4542
+ patch_id: url.searchParams.get("patch_id") || undefined
4543
+ }));
3387
4544
  } else {
3388
4545
  return convert({
3389
4546
  status: 404,
@@ -3435,10 +4592,10 @@ class ValFSHost {
3435
4592
  return this.currentDirectory;
3436
4593
  }
3437
4594
  getCanonicalFileName(fileName) {
3438
- if (path__namespace["default"].isAbsolute(fileName)) {
3439
- return path__namespace["default"].normalize(fileName);
4595
+ if (fsPath__namespace["default"].isAbsolute(fileName)) {
4596
+ return fsPath__namespace["default"].normalize(fileName);
3440
4597
  }
3441
- return path__namespace["default"].resolve(this.getCurrentDirectory(), fileName);
4598
+ return fsPath__namespace["default"].resolve(this.getCurrentDirectory(), fileName);
3442
4599
  }
3443
4600
  fileExists(fileName) {
3444
4601
  return this.valFS.fileExists(fileName);
@@ -3451,6 +4608,14 @@ class ValFSHost {
3451
4608
  }
3452
4609
  }
3453
4610
 
4611
+ function getValidationErrorFileRef(validationError) {
4612
+ const maybeRef = validationError.value && typeof validationError.value === "object" && core.FILE_REF_PROP in validationError.value && typeof validationError.value[core.FILE_REF_PROP] === "string" ? validationError.value[core.FILE_REF_PROP] : undefined;
4613
+ if (!maybeRef) {
4614
+ return null;
4615
+ }
4616
+ return maybeRef;
4617
+ }
4618
+
3454
4619
  // TODO: find a better name? transformFixesToPatch?
3455
4620
  async function createFixPatch(config, apply, sourcePath, validationError) {
3456
4621
  async function getImageMetadata() {
@@ -3459,7 +4624,7 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
3459
4624
  // TODO:
3460
4625
  throw Error("Cannot fix image without a file reference");
3461
4626
  }
3462
- const filename = path__namespace["default"].join(config.projectRoot, fileRef);
4627
+ const filename = fsPath__namespace["default"].join(config.projectRoot, fileRef);
3463
4628
  const buffer = fs__default["default"].readFileSync(filename);
3464
4629
  return extractImageMetadata(filename, buffer);
3465
4630
  }
@@ -3469,7 +4634,7 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
3469
4634
  // TODO:
3470
4635
  throw Error("Cannot fix file without a file reference");
3471
4636
  }
3472
- const filename = path__namespace["default"].join(config.projectRoot, fileRef);
4637
+ const filename = fsPath__namespace["default"].join(config.projectRoot, fileRef);
3473
4638
  const buffer = fs__default["default"].readFileSync(filename);
3474
4639
  return extractFileMetadata(fileRef, buffer);
3475
4640
  }
@@ -3635,7 +4800,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
3635
4800
  };
3636
4801
  }
3637
4802
 
3638
- exports.LocalValServer = LocalValServer;
3639
4803
  exports.Patch = Patch;
3640
4804
  exports.PatchJSON = PatchJSON;
3641
4805
  exports.Service = Service;