@valbuild/server 0.60.27 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,1218 +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, 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) {
1361
+ this.valModules = valModules;
1419
1362
  this.options = options;
1420
- 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));
1421
1377
  }
1422
1378
 
1423
- /* Auth endpoints: */
1424
- async enable(query) {
1425
- const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
1426
- if (typeof redirectToRes !== "string") {
1427
- 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;
1428
1453
  }
1429
- await this.callbacks.onEnable(true);
1430
1454
  return {
1431
- cookies: {
1432
- [internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
1433
- },
1434
- status: 302,
1435
- redirectTo: redirectToRes
1455
+ baseSha: this.baseSha,
1456
+ schemaSha: this.schemaSha,
1457
+ sources: this.sources,
1458
+ schemas: this.schemas,
1459
+ moduleErrors: this.modulesErrors
1436
1460
  };
1437
1461
  }
1438
- async disable(query) {
1439
- const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
1440
- if (typeof redirectToRes !== "string") {
1441
- return redirectToRes;
1442
- }
1443
- await this.callbacks.onDisable(true);
1444
- return {
1445
- cookies: {
1446
- [internal.VAL_ENABLE_COOKIE_NAME]: {
1447
- value: "false"
1448
- }
1449
- },
1450
- status: 302,
1451
- redirectTo: redirectToRes
1452
- };
1462
+ async init() {
1463
+ const {
1464
+ baseSha,
1465
+ schemaSha
1466
+ } = await this.initTree();
1467
+ await this.onInit(baseSha, schemaSha);
1453
1468
  }
1454
- async getTree(treePath,
1455
- // TODO: use the params: patch, schema, source now we return everything, every time
1456
- query, cookies, requestHeaders) {
1457
- const ensureRes = await debugTiming("ensureInitialized", () => this.ensureInitialized("getTree", cookies));
1458
- if (fp.result.isErr(ensureRes)) {
1459
- return ensureRes.error;
1460
- }
1461
- const applyPatches = query.patch === "true";
1462
- const includeSource = query.source === "true";
1463
- const includeSchema = query.schema === "true";
1464
- const moduleIds = await debugTiming("getAllModules", () => this.getAllModules(treePath));
1465
- let {
1466
- patchIdsByModuleId,
1467
- patchesById,
1468
- fileUpdates
1469
- } = {
1470
- patchIdsByModuleId: {},
1471
- patchesById: {},
1472
- fileUpdates: {}
1473
- };
1474
- if (applyPatches) {
1475
- const res = await debugTiming("readPatches", () => this.readPatches(cookies));
1476
- if (fp.result.isErr(res)) {
1477
- return res.error;
1478
- }
1479
- patchIdsByModuleId = res.value.patchIdsByModuleId;
1480
- patchesById = res.value.patchesById;
1481
- fileUpdates = res.value.fileUpdates;
1482
- }
1483
- const validate = false;
1484
- const possiblyPatchedContent = await debugTiming("applyAllPatchesThenValidate", () => Promise.all(moduleIds.map(async moduleId => {
1485
- return this.applyAllPatchesThenValidate(moduleId, patchIdsByModuleId, patchesById, fileUpdates, cookies, requestHeaders, applyPatches, validate, includeSource, includeSchema);
1486
- })));
1487
- const modules = Object.fromEntries(possiblyPatchedContent.map(serializedModuleContent => {
1488
- const module = {
1489
- schema: serializedModuleContent.schema,
1490
- source: serializedModuleContent.source,
1491
- errors: serializedModuleContent.errors
1492
- };
1493
- return [serializedModuleContent.path, module];
1494
- }));
1495
- const apiTreeResponse = {
1496
- modules,
1497
- git: this.options.git
1498
- };
1499
- return {
1500
- status: 200,
1501
- json: apiTreeResponse
1502
- };
1469
+ async getBaseSources() {
1470
+ return this.initTree().then(result => result.sources);
1503
1471
  }
1504
- async postValidate(rawBody, cookies, requestHeaders) {
1505
- const ensureRes = await this.ensureInitialized("postValidate", cookies);
1506
- if (fp.result.isErr(ensureRes)) {
1507
- return ensureRes.error;
1508
- }
1509
- return this.validateThenMaybeCommit(rawBody, false, cookies, requestHeaders);
1472
+ async getSchemas() {
1473
+ return this.initTree().then(result => result.schemas);
1510
1474
  }
1511
- async postCommit(rawBody, cookies, requestHeaders) {
1512
- const ensureRes = await this.ensureInitialized("postCommit", cookies);
1513
- if (fp.result.isErr(ensureRes)) {
1514
- return ensureRes.error;
1515
- }
1516
- const res = await this.validateThenMaybeCommit(rawBody, true, cookies, requestHeaders);
1517
- if (res.status === 200) {
1518
- if (res.json.validationErrors) {
1519
- return {
1520
- status: 400,
1521
- json: {
1522
- ...res.json
1523
- }
1524
- };
1525
- }
1526
- return {
1527
- status: 200,
1528
- json: {
1529
- ...res.json,
1530
- git: this.options.git
1531
- }
1532
- };
1533
- }
1534
- 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);
1535
1483
  }
1536
1484
 
1537
- /* */
1538
- async applyAllPatchesThenValidate(moduleId, patchIdsByModuleId, patchesById, fileUpdates, cookies, requestHeaders, applyPatches, validate, includeSource, includeSchema) {
1539
- const serializedModuleContent = await this.getModule(moduleId, {
1540
- source: includeSource,
1541
- schema: includeSchema
1542
- });
1543
- if (!applyPatches) {
1544
- return serializedModuleContent;
1545
- }
1546
- const schema = serializedModuleContent.schema;
1547
- const maybeSource = serializedModuleContent.source;
1548
- if (serializedModuleContent.errors && (serializedModuleContent.errors.fatal || serializedModuleContent.errors.invalidModuleId)) {
1549
- return serializedModuleContent;
1550
- }
1551
- if (!maybeSource || !schema) {
1552
- return serializedModuleContent;
1553
- }
1554
- let source = maybeSource;
1555
- await debugTiming("applyPatches:" + moduleId, async () => {
1556
- for (const patchId of patchIdsByModuleId[moduleId] ?? []) {
1557
- const patch$1 = patchesById[patchId];
1558
- if (!patch$1) {
1559
- continue;
1560
- }
1561
- const patchRes = patch.applyPatch(source, ops, patch$1.filter(core.Internal.notFileOp));
1562
- if (fp.result.isOk(patchRes)) {
1563
- source = patchRes.value;
1564
- } else {
1565
- console.error("Val: got an unexpected error while applying patch. Is there a mismatch in Val versions? Perhaps Val is misconfigured?", {
1566
- patchId,
1567
- moduleId,
1568
- patch: JSON.stringify(patch$1, null, 2),
1569
- error: patchRes.error
1570
- });
1571
- return {
1572
- path: moduleId,
1573
- schema,
1574
- source,
1575
- errors: {
1576
- fatal: [{
1577
- message: "Unexpected error applying patch",
1578
- type: "invalid-patch"
1579
- }]
1580
- }
1581
- };
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;
1582
1498
  }
1583
1499
  }
1584
- });
1585
- if (validate) {
1586
- const validationErrors = await debugTiming("validate:" + moduleId, async () => core.deserializeSchema(schema).validate(moduleId, source));
1587
- if (validationErrors) {
1588
- const revalidated = await debugTiming("revalidate image/file:" + moduleId, async () => this.revalidateImageAndFileValidation(validationErrors, fileUpdates, cookies, requestHeaders));
1589
- return {
1590
- path: moduleId,
1591
- schema,
1592
- source,
1593
- errors: revalidated && {
1594
- validation: revalidated
1595
- }
1596
- };
1500
+ if (!patchesByModule[path]) {
1501
+ patchesByModule[path] = [];
1597
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));
1598
1510
  }
1599
1511
  return {
1600
- path: moduleId,
1601
- schema,
1602
- source,
1603
- errors: false
1512
+ patchesByModule,
1513
+ fileLastUpdatedByPatchId
1604
1514
  };
1605
1515
  }
1606
- // TODO: name this better: we need to check for image and file validation errors
1607
- // since they cannot be handled directly inside the validation function.
1608
- // The reason is that validate will be called inside QuickJS (in the future, hopefully),
1609
- // which does not have access to the filesystem, at least not at the time of writing this comment.
1610
- // If you are reading this, and we still are not using QuickJS to validate, this assumption might be wrong.
1611
- async revalidateImageAndFileValidation(validationErrors, fileUpdates, cookies, reqHeaders) {
1612
- const revalidatedValidationErrors = {};
1613
- for (const pathStr in validationErrors) {
1614
- const errorSourcePath = pathStr;
1615
- const errors = validationErrors[errorSourcePath];
1616
- revalidatedValidationErrors[errorSourcePath] = [];
1617
- for (const error of errors) {
1618
- var _error$fixes;
1619
- 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
1620
- )) {
1621
- const fileRef = getValidationErrorFileRef(error);
1622
- if (fileRef) {
1623
- var _fileUpdates$fileRef;
1624
- const filePath = path__namespace["default"].join(this.cwd, fileRef);
1625
- let expectedMetadata = await this.getMetadata(fileRef, (_fileUpdates$fileRef = fileUpdates[fileRef]) === null || _fileUpdates$fileRef === void 0 ? void 0 : _fileUpdates$fileRef.sha256);
1626
-
1627
- // if this is a new file or we have an actual FS, we read the file and get the metadata
1628
- if (!expectedMetadata) {
1629
- let fileBuffer = undefined;
1630
- const updatedFileMetadata = fileUpdates[fileRef];
1631
- if (updatedFileMetadata) {
1632
- const fileRes = await this.getFiles(fileRef, {
1633
- sha256: updatedFileMetadata.sha256
1634
- }, cookies, reqHeaders);
1635
- if (fileRes.status === 200 && fileRes.body) {
1636
- const res = new Response(fileRes.body);
1637
- fileBuffer = Buffer.from(await res.arrayBuffer());
1638
- } else {
1639
- console.error("Val: unexpected error while fetching image / file:", fileRef, {
1640
- error: fileRes
1641
- });
1642
- }
1643
- }
1644
- // try fetch file directly via http
1645
- if (fileRef.startsWith("/public")) {
1646
- const host = `${reqHeaders["x-forwarded-proto"]}://${reqHeaders["host"]}`;
1647
- const fileUrl = fileRef.slice("/public".length);
1648
- const fetchRes = await fetch(new URL(fileUrl, host));
1649
- if (fetchRes.status === 200) {
1650
- fileBuffer = Buffer.from(await fetchRes.arrayBuffer());
1651
- } else {
1652
- console.error("Val: unexpected error while fetching image / file:", fileRef, {
1653
- error: {
1654
- status: fetchRes.status,
1655
- url: fetchRes.url
1656
- }
1657
- });
1658
- }
1659
- } else {
1660
- console.error("Val: unexpected while getting public image / file (file reference did not start with /public)", fileRef);
1661
- }
1662
- if (!fileBuffer) {
1663
- revalidatedValidationErrors[errorSourcePath].push({
1664
- message: `Could not read file: ${filePath}`
1665
- });
1666
- continue;
1667
- }
1668
- if (error.fixes.some(fix => fix === "image:replace-metadata")) {
1669
- expectedMetadata = await extractImageMetadata(filePath, fileBuffer);
1670
- } else {
1671
- expectedMetadata = await extractFileMetadata(filePath, fileBuffer);
1672
- }
1673
- }
1674
- if (!expectedMetadata) {
1675
- revalidatedValidationErrors[errorSourcePath].push({
1676
- message: `Could not read file metadata. Is the reference to the file: ${fileRef} correct?`
1677
- });
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
+ }];
1678
1575
  } else {
1679
- const actualMetadata = getValidationErrorMetadata(error);
1680
- const revalidatedError = validateMetadata(actualMetadata, expectedMetadata);
1681
- if (!revalidatedError) {
1682
- // no errors anymore:
1683
- continue;
1684
- }
1685
- const errorMsgs = (revalidatedError.globalErrors || []).concat(Object.values(revalidatedError.erroneousMetadata || {})).concat(Object.values(revalidatedError.missingMetadata || []).map(missingKey => {
1686
- var _expectedMetadata;
1687
- return `Required key: '${missingKey}' is not defined. Should be: '${JSON.stringify((_expectedMetadata = expectedMetadata) === null || _expectedMetadata === void 0 ? void 0 : _expectedMetadata[missingKey])}'`;
1688
- }));
1689
- revalidatedValidationErrors[errorSourcePath].push(...errorMsgs.map(message => ({
1690
- message
1691
- })));
1576
+ applicableOps.push(op);
1692
1577
  }
1693
- } else {
1694
- revalidatedValidationErrors[errorSourcePath].push(error);
1695
1578
  }
1696
- } else {
1697
- revalidatedValidationErrors[errorSourcePath].push(error);
1698
- }
1699
- }
1700
- }
1701
- const hasErrors = Object.values(revalidatedValidationErrors).some(errors => errors.length > 0);
1702
- if (hasErrors) {
1703
- return revalidatedValidationErrors;
1704
- }
1705
- return hasErrors;
1706
- }
1707
- sortPatchIds(patchesByModule) {
1708
- return Object.values(patchesByModule).flatMap(modulePatches => modulePatches).sort((a, b) => {
1709
- return a.created_at.localeCompare(b.created_at);
1710
- }).map(patchData => patchData.patch_id);
1711
- }
1712
-
1713
- // can be overridden if FS cannot read from static assets / public folder (because of bundlers or what not)
1714
- async readStaticBinaryFile(filePath) {
1715
- return fs__default["default"].promises.readFile(filePath);
1716
- }
1717
- async readPatches(cookies) {
1718
- const res = await this.getPatches({},
1719
- // {} means no ids, so get all patches
1720
- cookies);
1721
- if (res.status === 400 || res.status === 401 || res.status === 403 || res.status === 404 || res.status === 500 || res.status === 501) {
1722
- return fp.result.err(res);
1723
- } else if (res.status === 200 || res.status === 201) {
1724
- const patchesByModule = res.json;
1725
- const patches = [];
1726
- const patchIdsByModuleId = {};
1727
- const patchesById = {};
1728
- for (const [moduleIdS, modulePatchData] of Object.entries(patchesByModule)) {
1729
- const moduleId = moduleIdS;
1730
- patchIdsByModuleId[moduleId] = modulePatchData.map(patch => patch.patch_id);
1731
- for (const patchData of modulePatchData) {
1732
- patches.push([patchData.patch_id, moduleId, patchData.patch]);
1733
- patchesById[patchData.patch_id] = patchData.patch;
1734
- }
1735
- }
1736
- const fileUpdates = {};
1737
- const sortedPatchIds = this.sortPatchIds(patchesByModule);
1738
- for (const sortedPatchId of sortedPatchIds) {
1739
- const patchId = sortedPatchId;
1740
- for (const op of patchesById[patchId] || []) {
1741
- if (op.op === "file") {
1742
- const parsedFileOp = z.z.object({
1743
- sha256: z.z.string(),
1744
- mimeType: z.z.string()
1745
- }).safeParse(op.value);
1746
- if (!parsedFileOp.success) {
1747
- return fp.result.err({
1748
- status: 500,
1749
- json: {
1750
- message: "Unexpected error: file op value must be transformed into object",
1751
- details: {
1752
- value: "First 200 chars: " + JSON.stringify(op.value).slice(0, 200),
1753
- patchId
1754
- }
1755
- }
1756
- });
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] = [];
1757
1583
  }
1758
- fileUpdates[op.filePath] = {
1759
- ...parsedFileOp.data
1760
- };
1584
+ errors[path].push({
1585
+ patchId: patchId,
1586
+ error: patchRes.error
1587
+ });
1588
+ } else {
1589
+ patchedSources[path] = patchRes.value;
1761
1590
  }
1762
1591
  }
1763
1592
  }
1764
- return fp.result.ok({
1765
- patches,
1766
- patchIdsByModuleId,
1767
- patchesById,
1768
- fileUpdates
1769
- });
1770
- } else {
1771
- return fp.result.err({
1772
- status: 500,
1773
- json: {
1774
- message: "Unknown error"
1775
- }
1776
- });
1777
1593
  }
1594
+ return {
1595
+ sources: patchedSources,
1596
+ errors
1597
+ };
1778
1598
  }
1779
- async validateThenMaybeCommit(rawBody, commit, cookies, requestHeaders) {
1780
- const filterPatchesByModuleIdRes = z.z.object({
1781
- patches: z.z.record(z.z.array(z.z.string())).optional()
1782
- }).safeParse(rawBody);
1783
- if (!filterPatchesByModuleIdRes.success) {
1784
- return {
1785
- status: 404,
1786
- json: {
1787
- message: "Could not parse body",
1788
- details: filterPatchesByModuleIdRes.error
1789
- }
1790
- };
1791
- }
1792
- const res = await this.readPatches(cookies);
1793
- if (fp.result.isErr(res)) {
1794
- return res.error;
1795
- }
1796
- const {
1797
- patchIdsByModuleId,
1798
- patchesById,
1799
- patches,
1800
- fileUpdates
1801
- } = res.value;
1802
- const validationErrorsByModuleId = {};
1803
- for (const moduleIdStr of await this.getAllModules("/")) {
1804
- const moduleId = moduleIdStr;
1805
- const serializedModuleContent = await this.applyAllPatchesThenValidate(moduleId, filterPatchesByModuleIdRes.data.patches ||
1806
- // TODO: refine to ModuleId and PatchId when parsing
1807
- patchIdsByModuleId, patchesById, fileUpdates, cookies, requestHeaders, true, true, true, commit);
1808
- if (serializedModuleContent.errors) {
1809
- validationErrorsByModuleId[moduleId] = serializedModuleContent;
1810
- }
1811
- }
1812
- if (Object.keys(validationErrorsByModuleId).length > 0) {
1813
- const modules = {};
1814
- for (const [patchId, moduleId] of patches) {
1815
- if (!modules[moduleId]) {
1816
- modules[moduleId] = {
1817
- patches: {
1818
- applied: []
1819
- }
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: {}
1820
1616
  };
1821
1617
  }
1822
- if (validationErrorsByModuleId[moduleId]) {
1823
- var _modules$moduleId$pat;
1824
- if (!modules[moduleId].patches.failed) {
1825
- modules[moduleId].patches.failed = [];
1618
+ errors[path] = {
1619
+ ...errors[path],
1620
+ invalidSource: {
1621
+ message: `Module at path: '${path}' does not exist`
1826
1622
  }
1827
- (_modules$moduleId$pat = modules[moduleId].patches.failed) === null || _modules$moduleId$pat === void 0 || _modules$moduleId$pat.push(patchId);
1828
- } else {
1829
- modules[moduleId].patches.applied.push(patchId);
1830
- }
1623
+ };
1624
+ continue;
1831
1625
  }
1832
- return {
1833
- status: 200,
1834
- json: {
1835
- modules,
1836
- 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
+ }
1837
1652
  }
1838
- };
1839
- }
1840
- let modules;
1841
- if (commit) {
1842
- const commitRes = await this.execCommit(patches, cookies);
1843
- if (commitRes.status !== 200) {
1844
- return commitRes;
1845
1653
  }
1846
- modules = commitRes.json;
1847
- } else {
1848
- modules = await this.getPatchedModules(patches);
1849
1654
  }
1850
1655
  return {
1851
- status: 200,
1852
- json: {
1853
- modules,
1854
- 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
+ };
1855
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;
1856
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);
1857
1797
  }
1858
- async getPatchedModules(patches) {
1859
- const modules = {};
1860
- for (const [patchId, moduleId] of patches) {
1861
- if (!modules[moduleId]) {
1862
- modules[moduleId] = {
1863
- patches: {
1864
- 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
+ });
1865
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
1866
1873
  };
1867
1874
  }
1868
- 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
+ }
1869
1900
  }
1870
- 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;
1871
1929
  }
1872
1930
 
1873
- /* Abstract methods */
1874
-
1875
- /**
1876
- * Runs before remoteFS dependent methods (e.g.getModule, ...) are called to make sure that:
1877
- * 1) The remote FS, if applicable, is initialized
1878
- * 2) The error is returned via API if the remote FS could not be initialized
1879
- * */
1880
-
1881
- /* Abstract endpoints */
1882
-
1883
- /* Abstract auth endpoints: */
1884
-
1885
- /* Abstract patch endpoints: */
1886
- }
1887
- const chunkSize = 1024 * 1024;
1888
- function bufferToReadableStream(buffer) {
1889
- const stream = new ReadableStream({
1890
- start(controller) {
1891
- let offset = 0;
1892
- while (offset < buffer.length) {
1893
- const chunk = buffer.subarray(offset, offset + chunkSize);
1894
- controller.enqueue(chunk);
1895
- 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
+ }
1896
1996
  }
1897
- controller.close();
1898
1997
  }
1899
- });
1900
- return stream;
1901
- }
1902
- const ENABLE_COOKIE_VALUE = {
1903
- value: "true",
1904
- options: {
1905
- httpOnly: false,
1906
- sameSite: "lax"
1907
- }
1908
- };
1909
- function getRedirectUrl(query, overrideHost) {
1910
- if (typeof query.redirect_to !== "string") {
1911
- return {
1912
- status: 400,
1913
- json: {
1914
- 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
+ };
1915
2073
  }
2074
+ }));
2075
+ return {
2076
+ patchId,
2077
+ files: saveFileRes,
2078
+ createdAt: new Date().toISOString()
1916
2079
  };
1917
2080
  }
1918
- if (overrideHost) {
1919
- 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;
1920
2088
  }
1921
- return query.redirect_to;
2089
+ return false;
1922
2090
  }
1923
- const base64DataAttr = "data:";
1924
- function getMimeTypeFromBase64(content) {
1925
- const dataIndex = content.indexOf(base64DataAttr);
1926
- const base64Index = content.indexOf(";base64,");
1927
- if (dataIndex > -1 || base64Index > -1) {
1928
- const mimeType = content.slice(dataIndex + base64DataAttr.length, base64Index);
1929
- 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;
1930
2094
  }
1931
- return null;
2095
+ return false;
1932
2096
  }
1933
- function bufferFromDataUrl(dataUrl, contentType) {
1934
- let base64Data;
1935
- if (!contentType) {
1936
- const base64Index = dataUrl.indexOf(";base64,");
1937
- if (base64Index > -1) {
1938
- base64Data = dataUrl.slice(base64Index + ";base64,".length);
1939
- }
1940
- } else {
1941
- const dataUrlEncodingHeader = `${base64DataAttr}${contentType};base64,`;
1942
- if (dataUrl.slice(0, dataUrlEncodingHeader.length) === dataUrlEncodingHeader) {
1943
- base64Data = dataUrl.slice(dataUrlEncodingHeader.length);
1944
- }
2097
+ function getFieldsForType(type) {
2098
+ if (type === "file") {
2099
+ return ["sha256", "mimeType"];
2100
+ } else if (type === "image") {
2101
+ return ["sha256", "mimeType", "height", "width"];
1945
2102
  }
1946
- if (base64Data) {
1947
- return Buffer.from(base64Data, "base64" // TODO: why does it not work with base64url?
1948
- );
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
+ };
2122
+ }
2123
+ availableMetadata = {
2124
+ sha256: sha256,
2125
+ mimeType,
2126
+ height,
2127
+ width
2128
+ };
2129
+ } else {
2130
+ availableMetadata = {
2131
+ sha256: sha256,
2132
+ mimeType
2133
+ };
1949
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
+ });
2145
+ }
2146
+ }
2147
+ if (errors.length > 0) {
2148
+ return {
2149
+ errors
2150
+ };
2151
+ }
2152
+ return {
2153
+ metadata
2154
+ };
1950
2155
  }
1951
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
1952
- const COMMON_MIME_TYPES = {
1953
- aac: "audio/aac",
1954
- abw: "application/x-abiword",
1955
- arc: "application/x-freearc",
1956
- avif: "image/avif",
1957
- avi: "video/x-msvideo",
1958
- azw: "application/vnd.amazon.ebook",
1959
- bin: "application/octet-stream",
1960
- bmp: "image/bmp",
1961
- bz: "application/x-bzip",
1962
- bz2: "application/x-bzip2",
1963
- cda: "application/x-cdf",
1964
- csh: "application/x-csh",
1965
- css: "text/css",
1966
- csv: "text/csv",
1967
- doc: "application/msword",
1968
- docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1969
- eot: "application/vnd.ms-fontobject",
1970
- epub: "application/epub+zip",
1971
- gz: "application/gzip",
1972
- gif: "image/gif",
1973
- htm: "text/html",
1974
- html: "text/html",
1975
- ico: "image/vnd.microsoft.icon",
1976
- ics: "text/calendar",
1977
- jar: "application/java-archive",
1978
- jpeg: "image/jpeg",
1979
- jpg: "image/jpeg",
1980
- js: "text/javascript",
1981
- json: "application/json",
1982
- jsonld: "application/ld+json",
1983
- mid: "audio/midi",
1984
- midi: "audio/midi",
1985
- mjs: "text/javascript",
1986
- mp3: "audio/mpeg",
1987
- mp4: "video/mp4",
1988
- mpeg: "video/mpeg",
1989
- mpkg: "application/vnd.apple.installer+xml",
1990
- odp: "application/vnd.oasis.opendocument.presentation",
1991
- ods: "application/vnd.oasis.opendocument.spreadsheet",
1992
- odt: "application/vnd.oasis.opendocument.text",
1993
- oga: "audio/ogg",
1994
- ogv: "video/ogg",
1995
- ogx: "application/ogg",
1996
- opus: "audio/opus",
1997
- otf: "font/otf",
1998
- png: "image/png",
1999
- pdf: "application/pdf",
2000
- php: "application/x-httpd-php",
2001
- ppt: "application/vnd.ms-powerpoint",
2002
- pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
2003
- rar: "application/vnd.rar",
2004
- rtf: "application/rtf",
2005
- sh: "application/x-sh",
2006
- svg: "image/svg+xml",
2007
- tar: "application/x-tar",
2008
- tif: "image/tiff",
2009
- tiff: "image/tiff",
2010
- ts: "video/mp2t",
2011
- ttf: "font/ttf",
2012
- txt: "text/plain",
2013
- vsd: "application/vnd.visio",
2014
- wav: "audio/wav",
2015
- weba: "audio/webm",
2016
- webm: "video/webm",
2017
- webp: "image/webp",
2018
- woff: "font/woff",
2019
- woff2: "font/woff2",
2020
- xhtml: "application/xhtml+xml",
2021
- xls: "application/vnd.ms-excel",
2022
- xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
2023
- xml: "application/xml",
2024
- xul: "application/vnd.mozilla.xul+xml",
2025
- zip: "application/zip",
2026
- "3gp": "video/3gpp; audio/3gpp if it doesn't contain video",
2027
- "3g2": "video/3gpp2; audio/3gpp2 if it doesn't contain video",
2028
- "7z": "application/x-7z-compressed"
2029
- };
2030
- function guessMimeTypeFromPath(filePath) {
2031
- const fileExt = filePath.split(".").pop();
2032
- if (fileExt) {
2033
- 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;
2034
2164
  }
2035
2165
  return null;
2036
2166
  }
2037
- function isCachedPatchFileOp(op) {
2038
- 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");
2039
- }
2040
- async function debugTiming(id, fn) {
2041
- if (process.env["VAL_DEBUG_TIMING"] === "true") {
2042
- const start = Date.now();
2043
- const r = await fn();
2044
- console.log(`Timing: ${id} took: ${Date.now() - start}ms (${new Date().toISOString()})`);
2045
- return r;
2046
- } else {
2047
- 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
+ );
2048
2176
  }
2049
2177
  }
2050
2178
 
2051
- const textEncoder = new TextEncoder();
2052
- class LocalValServer extends ValServer {
2053
- static PATCHES_DIR = "patches";
2054
- static FILES_DIR = "files";
2055
- constructor(options, callbacks) {
2056
- super(options.service.sourceFileHandler.projectRoot, options, callbacks);
2057
- this.options = options;
2058
- this.callbacks = callbacks;
2059
- this.patchesRootPath = options.cacheDir || path__namespace["default"].join(options.service.sourceFileHandler.projectRoot, ".val");
2060
- 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
2061
2265
  }
2062
- async session() {
2063
- return {
2064
- status: 200,
2065
- json: {
2066
- mode: "local",
2067
- 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(", "));
2068
2278
  }
2069
- };
2070
- }
2071
- async deletePatches(query) {
2072
- const deletedPatches = [];
2073
- for (const patchId of query.id ?? []) {
2074
- const rawPatchFileContent = this.host.readFile(this.getPatchFilePath(patchId));
2075
- if (!rawPatchFileContent) {
2076
- console.warn("Val: Patch not found", patchId);
2279
+ const patchId = patchIdNum.toString();
2280
+ if (includes && !includes.includes(patchId)) {
2077
2281
  continue;
2078
2282
  }
2079
- const parsedPatchesRes = z.z.record(Patch).safeParse(JSON.parse(rawPatchFileContent));
2080
- if (!parsedPatchesRes.success) {
2081
- 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))) {
2082
2322
  continue;
2083
2323
  }
2084
- const files = Object.values(parsedPatchesRes.data).flatMap(ops => ops.filter(isCachedPatchFileOp).map(op => ({
2085
- filePath: op.filePath,
2086
- sha256: op.value.sha256
2087
- })));
2088
- for (const file of files) {
2089
- this.host.rmFile(this.getFilePath(file.filePath, file.sha256));
2090
- 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;
2091
2333
  }
2092
- this.host.rmFile(path__namespace["default"].join(this.patchesRootPath, LocalValServer.PATCHES_DIR, patchId));
2093
- deletedPatches.push(patchId);
2334
+ }
2335
+ if (errors && Object.keys(errors).length > 0) {
2336
+ return {
2337
+ patches,
2338
+ errors
2339
+ };
2094
2340
  }
2095
2341
  return {
2096
- status: 200,
2097
- json: deletedPatches
2342
+ patches
2098
2343
  };
2099
2344
  }
2100
- async postPatches(body) {
2101
- const patches = z.z.record(Patch).safeParse(body);
2102
- if (!patches.success) {
2345
+
2346
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2347
+ parseJsonFile(filePath, parser) {
2348
+ if (!this.host.fileExists(filePath)) {
2103
2349
  return {
2104
- status: 404,
2105
- json: {
2106
- message: `Invalid patch: ${patches.error.message}`,
2107
- details: patches.error.issues
2350
+ error: {
2351
+ message: `File not found: ${filePath}`,
2352
+ filePath
2108
2353
  }
2109
2354
  };
2110
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
2416
+ }
2417
+ };
2418
+ }
2419
+ }
2420
+ async saveSourceFilePatch(path, patch, authorId) {
2111
2421
  let fileId = Date.now();
2112
- while (this.host.fileExists(this.getPatchFilePath(fileId.toString()))) {
2113
- // ensure unique file / patch id
2114
- 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
+ };
2115
2452
  }
2116
- const patchId = fileId.toString();
2117
- const res = {};
2118
- const parsedPatches = {};
2119
- for (const moduleIdStr in patches.data) {
2120
- const moduleId = moduleIdStr; // TODO: validate that this is a valid module id
2121
- res[moduleId] = {
2122
- patch_id: patchId
2123
- };
2124
- parsedPatches[moduleId] = [];
2125
- for (const op of patches.data[moduleId]) {
2126
- // We do not want to include value of a file op in the patch as they potentially contain a lot of data,
2127
- // therefore we store the file in a separate file and only store the sha256 hash in the patch.
2128
- // I.e. the patch that frontend sends is not the same as the one stored.
2129
- // 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.
2130
- //
2131
- // 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
2132
- // In the worst scenario we imagine this being potentially crashing the server runtime, especially on smaller edge runtimes.
2133
- // Potential crashes are bad enough to warrant this workaround.
2134
- if (core.Internal.isFileOp(op)) {
2135
- const sha256 = core.Internal.getSHA256Hash(textEncoder.encode(op.value));
2136
- const mimeType = getMimeTypeFromBase64(op.value);
2137
- if (!mimeType) {
2138
- console.error("Val: Cannot determine mimeType from base64 data", op);
2139
- 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
2140
2479
  }
2141
- const buffer = bufferFromDataUrl(op.value, mimeType);
2142
- if (!buffer) {
2143
- console.error("Val: Cannot parse base64 data", op);
2144
- 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)
2145
2498
  }
2146
- this.host.writeFile(this.getFilePath(op.filePath, sha256), buffer, "binary");
2147
- this.host.writeFile(this.getFileMetadataPath(op.filePath, sha256), JSON.stringify({
2148
- mimeType,
2149
- sha256,
2150
- // useful for debugging / manual inspection
2151
- patchId,
2152
- createdAt: new Date().toISOString()
2153
- }, null, 2), "utf8");
2154
- parsedPatches[moduleId].push({
2155
- ...op,
2156
- value: {
2157
- sha256,
2158
- mimeType
2159
- }
2160
- });
2161
- } else {
2162
- 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"
2163
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
+ });
2164
2547
  }
2165
2548
  }
2166
- this.host.writeFile(this.getPatchFilePath(patchId), JSON.stringify(parsedPatches), "utf8");
2549
+ if (fieldErrors.length > 0) {
2550
+ return {
2551
+ errors: fieldErrors
2552
+ };
2553
+ }
2167
2554
  return {
2168
- status: 200,
2169
- 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}`
2170
2842
  };
2843
+ this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
2171
2844
  }
2172
- async getMetadata() {
2173
- return undefined;
2845
+ async onInit() {
2846
+ // TODO: unused for now. Implement or remove
2174
2847
  }
2175
- async getFiles(filePath, query) {
2176
- if (query.sha256) {
2177
- const fileExists = this.host.fileExists(this.getFilePath(filePath, query.sha256));
2178
- if (fileExists) {
2179
- const metadataFileContent = this.host.readFile(this.getFileMetadataPath(filePath, query.sha256));
2180
- const fileContent = await this.readStaticBinaryFile(this.getFilePath(filePath, query.sha256));
2181
- if (!fileContent) {
2182
- throw Error("Could not read cached patch file / asset. Cache corrupted?");
2183
- }
2184
- if (!metadataFileContent) {
2185
- 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
+ };
2186
2886
  }
2187
- const metadata = JSON.parse(metadataFileContent);
2188
2887
  return {
2189
- status: 200,
2190
- headers: {
2191
- "Content-Type": metadata.mimeType,
2192
- "Content-Length": fileContent.byteLength.toString(),
2193
- "Cache-Control": "public, max-age=31536000, immutable"
2194
- },
2195
- body: bufferToReadableStream(fileContent)
2888
+ patches,
2889
+ error: {
2890
+ message: `Could not parse get patches response. Error: ${zodValidationError.fromError(parsed.error)}`
2891
+ }
2196
2892
  };
2197
2893
  }
2198
- }
2199
- const buffer = await this.readStaticBinaryFile(path__namespace["default"].join(this.cwd, filePath));
2200
- const mimeType = guessMimeTypeFromPath(filePath) || "application/octet-stream";
2201
- if (!buffer) {
2202
2894
  return {
2203
- status: 404,
2204
- json: {
2205
- message: "File not found"
2895
+ patches,
2896
+ error: {
2897
+ message: "Could not get patches. HTTP error: " + res.status + " " + res.statusText
2206
2898
  }
2207
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(",")));
2208
2907
  }
2209
- if (query.sha256) {
2210
- const sha256 = getSha256(mimeType, buffer);
2211
- 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
+ }
2212
2936
  return {
2213
- status: 200,
2214
- headers: {
2215
- "Content-Type": mimeType,
2216
- "Content-Length": buffer.byteLength.toString(),
2217
- "Cache-Control": "public, max-age=31536000, immutable"
2218
- },
2219
- body: bufferToReadableStream(buffer)
2937
+ patches,
2938
+ error: {
2939
+ message: `Could not parse search patches response. Error: ${zodValidationError.fromError(parsed.error)}`
2940
+ }
2220
2941
  };
2221
2942
  }
2222
- }
2223
- return {
2224
- 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",
2225
2954
  headers: {
2226
- "Content-Type": mimeType,
2227
- "Content-Length": buffer.byteLength.toString()
2955
+ ...this.authHeaders,
2956
+ "Content-Type": "application/json"
2228
2957
  },
2229
- body: bufferToReadableStream(buffer)
2230
- };
2231
- }
2232
- async getPatches(query) {
2233
- const patchesCacheDir = path__namespace["default"].join(this.patchesRootPath, LocalValServer.PATCHES_DIR);
2234
- let files = [];
2235
- try {
2236
- if (!this.host.directoryExists || this.host.directoryExists && this.host.directoryExists(patchesCacheDir)) {
2237
- files = this.host.readDirectory(patchesCacheDir, [""], [], []);
2238
- }
2239
- } catch (e) {
2240
- console.debug("Failed to read directory (no patches yet?)", e);
2241
- }
2242
- const res = {};
2243
- const sortedPatchIds = files.map(file => parseInt(path__namespace["default"].basename(file), 10)).sort();
2244
- for (const patchIdStr of sortedPatchIds) {
2245
- const patchId = patchIdStr.toString();
2246
- if (query.id && query.id.length > 0 && !query.id.includes(patchId)) {
2247
- continue;
2248
- }
2249
- try {
2250
- const currentParsedPatches = z.z.record(Patch).safeParse(JSON.parse(this.host.readFile(path__namespace["default"].join(this.patchesRootPath, LocalValServer.PATCHES_DIR, `${patchId}`)) || ""));
2251
- if (!currentParsedPatches.success) {
2252
- const msg = "Unexpected error reading patch. Patch did not parse correctly. Is there a mismatch in Val versions? Perhaps Val is misconfigured?";
2253
- console.error(`Val: ${msg}`, {
2254
- patchId,
2255
- error: currentParsedPatches.error
2256
- });
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) {
2257
2970
  return {
2258
- status: 500,
2259
- json: {
2260
- message: msg,
2261
- details: {
2262
- patchId,
2263
- error: currentParsedPatches.error
2264
- }
2265
- }
2971
+ patchId: parsed.data.patchId
2266
2972
  };
2267
2973
  }
2268
- const createdAt = patchId;
2269
- for (const moduleIdStr in currentParsedPatches.data) {
2270
- const moduleId = moduleIdStr;
2271
- if (!res[moduleId]) {
2272
- res[moduleId] = [];
2273
- }
2274
- res[moduleId].push({
2275
- patch: currentParsedPatches.data[moduleId],
2276
- patch_id: patchId,
2277
- created_at: new Date(Number(createdAt)).toISOString()
2278
- });
2279
- }
2280
- } catch (err) {
2281
- const msg = `Unexpected error while reading patch file. The cache may be corrupted or Val may be misconfigured. Try deleting the cache directory.`;
2282
- console.error(`Val: ${msg}`, {
2283
- patchId,
2284
- error: err,
2285
- dir: this.patchesRootPath
2286
- });
2287
2974
  return {
2288
- status: 500,
2289
- json: {
2290
- message: msg,
2291
- details: {
2292
- patchId,
2293
- error: err === null || err === void 0 ? void 0 : err.toString()
2294
- }
2975
+ error: {
2976
+ message: `Could not parse save patch response. Error: ${zodValidationError.fromError(parsed.error)}`
2295
2977
  }
2296
2978
  };
2297
2979
  }
2298
- }
2299
- return {
2300
- status: 200,
2301
- json: res
2302
- };
2303
- }
2304
- getFilePath(filename, sha256) {
2305
- return path__namespace["default"].join(this.patchesRootPath, LocalValServer.FILES_DIR, filename, sha256, "file");
2306
- }
2307
- getFileMetadataPath(filename, sha256) {
2308
- return path__namespace["default"].join(this.patchesRootPath, LocalValServer.FILES_DIR, filename, sha256, "metadata.json");
2309
- }
2310
- getPatchFilePath(patchId) {
2311
- 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
+ });
2312
2992
  }
2313
- badRequest() {
2314
- return {
2315
- status: 400,
2316
- json: {
2317
- 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
+ };
2318
3020
  }
2319
- };
2320
- }
2321
- async ensureInitialized() {
2322
- // No RemoteFS so nothing to ensure
2323
- return fp.result.ok(undefined);
2324
- }
2325
- getModule(moduleId, options) {
2326
- return this.options.service.get(moduleId, "", {
2327
- ...options,
2328
- validate: false
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
+ };
2329
3032
  });
2330
3033
  }
2331
- async getAllModules(treePath) {
2332
- const moduleIds = this.host.readDirectory(this.cwd, ["ts", "js"], ["node_modules", ".*"], ["**/*.val.ts", "**/*.val.js"]).filter(file => {
2333
- if (treePath) {
2334
- return file.replace(this.cwd, "").startsWith(treePath);
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;
3058
+ }
3059
+ return {
3060
+ error: {
3061
+ message: `Could not parse file response. Error: ${zodValidationError.fromError(parsedFileResponse.error)}`
3062
+ }
3063
+ };
2335
3064
  }
2336
- return true;
2337
- }).map(file => file.replace(this.cwd, "").replace(".val.js", "").replace(".val.ts", "").split(path__namespace["default"].sep).join("/"));
2338
- return moduleIds;
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
+ };
3076
+ });
2339
3077
  }
2340
- async execCommit(patches) {
2341
- for (const [patchId, moduleId, patch] of patches) {
2342
- // TODO: patch the entire module content directly by using a { path: "", op: "replace", value: patchedData }?
2343
- // Reason: that would be more atomic? Not doing it now, because there are currently already too many moving pieces.
2344
- // Other things we could do would be to patch in a temp directory and ONLY when all patches are applied we move back in.
2345
- // This would improve reliability
2346
- this.host.rmFile(this.getPatchFilePath(patchId));
2347
- 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
+ };
2348
3095
  }
2349
3096
  return {
2350
- status: 200,
2351
- json: await this.getPatchedModules(patches)
3097
+ data: Buffer.from(file.value, "base64").toString("utf-8")
2352
3098
  };
2353
3099
  }
2354
-
2355
- /* Bad requests on Local Server: */
2356
-
2357
- async authorize() {
2358
- return this.badRequest();
2359
- }
2360
- async callback() {
2361
- return this.badRequest();
2362
- }
2363
- async logout() {
2364
- return this.badRequest();
2365
- }
2366
- }
2367
-
2368
- function decodeJwt(token, secretKey) {
2369
- const [headerBase64, payloadBase64, signatureBase64, ...rest] = token.split(".");
2370
- if (!headerBase64 || !payloadBase64 || !signatureBase64 || rest.length > 0) {
2371
- console.debug("Invalid JWT: format is not exactly {header}.{payload}.{signature}", token);
2372
- return null;
2373
- }
2374
- try {
2375
- const parsedHeader = JSON.parse(Buffer.from(headerBase64, "base64").toString("utf8"));
2376
- const headerVerification = JwtHeaderSchema.safeParse(parsedHeader);
2377
- if (!headerVerification.success) {
2378
- 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) {
2379
3109
  return null;
2380
3110
  }
2381
- if (headerVerification.data.typ !== jwtHeader.typ) {
2382
- console.debug("Invalid JWT: invalid header typ", parsedHeader);
3111
+ const file = filesRes.files.find(f => f.filePath === filePath);
3112
+ if (!file) {
2383
3113
  return null;
2384
3114
  }
2385
- if (headerVerification.data.alg !== jwtHeader.alg) {
2386
- 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) {
2387
3124
  return null;
2388
3125
  }
2389
- } catch (err) {
2390
- console.debug("Invalid JWT: could not parse header", err);
2391
- return null;
2392
- }
2393
- if (secretKey) {
2394
- const signature = crypto__default["default"].createHmac("sha256", secretKey).update(`${headerBase64}.${payloadBase64}`).digest("base64");
2395
- if (signature !== signatureBase64) {
2396
- console.debug("Invalid JWT: invalid signature");
3126
+ const file = filesRes.files.find(f => f.filePath === filePath);
3127
+ if (!file) {
2397
3128
  return null;
2398
3129
  }
3130
+ return Buffer.from(file.value, "base64");
2399
3131
  }
2400
- try {
2401
- const parsedPayload = JSON.parse(Buffer.from(payloadBase64, "base64").toString("utf8"));
2402
- return parsedPayload;
2403
- } catch (err) {
2404
- console.debug("Invalid JWT: could not parse payload", err);
2405
- return null;
2406
- }
2407
- }
2408
- function getExpire() {
2409
- return Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 4; // 4 days
2410
- }
2411
- const JwtHeaderSchema = z.z.object({
2412
- alg: z.z.literal("HS256"),
2413
- typ: z.z.literal("JWT")
2414
- });
2415
- const jwtHeader = {
2416
- alg: "HS256",
2417
- typ: "JWT"
2418
- };
2419
- const jwtHeaderBase64 = Buffer.from(JSON.stringify(jwtHeader)).toString("base64");
2420
- function encodeJwt(payload, sessionKey) {
2421
- // NOTE: this is only used for authentication, not for authorization (i.e. what a user can do) - this is handled when actually doing operations
2422
- const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString("base64");
2423
- return `${jwtHeaderBase64}.${payloadBase64}.${crypto__default["default"].createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
2424
- }
2425
-
2426
- class ProxyValServer extends ValServer {
2427
- moduleCache = null;
2428
- constructor(cwd, options, apiOptions, callbacks) {
2429
- super(cwd, options, callbacks);
2430
- this.cwd = cwd;
2431
- this.options = options;
2432
- this.apiOptions = apiOptions;
2433
- this.callbacks = callbacks;
2434
- this.moduleCache = null;
2435
- }
2436
-
2437
- /** Remote FS dependent methods: */
2438
-
2439
- async getModule(moduleId,
2440
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2441
- _options) {
2442
- if (this.moduleCache) {
2443
- return this.moduleCache[moduleId];
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"
3140
+ }
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
+ };
3149
+ }
3150
+ return {
3151
+ errors: [{
3152
+ message: `Could not parse metadata response. Error: ${zodValidationError.fromError(parsed.error)}`,
3153
+ filePath
3154
+ }]
3155
+ };
3156
+ }
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
+ };
2444
3169
  }
2445
- throw new Error("Module cache not initialized");
2446
3170
  }
2447
- async getAllModules(treePath) {
2448
- if (!this.moduleCache) {
2449
- throw new Error("Module cache not initialized");
2450
- }
2451
- return Object.keys(this.moduleCache).filter(moduleId => moduleId.startsWith(treePath));
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
+ };
2452
3188
  }
2453
- execCommit(patches, cookies) {
2454
- return withAuth(this.options.valSecret, cookies, "execCommit", async ({
2455
- token
2456
- }) => {
2457
- const commit = this.options.git.commit;
2458
- if (!commit) {
2459
- return {
2460
- status: 400,
2461
- json: {
2462
- message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
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;
2463
3206
  }
2464
- };
2465
- }
2466
- const params = createParams({
2467
- root: this.apiOptions.root,
2468
- commit,
2469
- ext: ["ts", "js", "json"],
2470
- package: ["@valbuild/core@" + this.options.versions.core, "@valbuild/next@" + this.options.versions.next],
2471
- include: ["**/*.val.{js,ts},package.json,tsconfig.json,jsconfig.json"]
2472
- });
2473
- const url = new URL(`/v1/commit/${this.options.remote}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
2474
- const patchIds = patches.map(([patchId]) => patchId);
2475
- const fetchRes = await fetch(url, {
2476
- method: "POST",
2477
- headers: getAuthHeaders(token, "application/json"),
2478
- body: JSON.stringify({
2479
- patchIds
2480
- })
2481
- });
2482
- 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
+ }
2483
3217
  return {
2484
- status: fetchRes.status,
2485
- json: await fetchRes.json()
3218
+ error: {
3219
+ message: `Could not parse delete patches response. Error: ${zodValidationError.fromError(parsed.error)}`
3220
+ }
2486
3221
  };
2487
- } else {
2488
- return createJsonError(fetchRes);
2489
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
+ };
2490
3234
  });
2491
3235
  }
2492
- async init(commit, token) {
2493
- const params = createParams({
2494
- root: this.apiOptions.root,
2495
- commit,
2496
- ext: ["ts", "js", "json"],
2497
- package: ["@valbuild/core@" + this.options.versions.core, "@valbuild/next@" + this.options.versions.next],
2498
- include: ["**/*.val.{js,ts},package.json,tsconfig.json,jsconfig.json"]
2499
- });
2500
- const url = new URL(`/v1/eval/${this.options.remote}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
3236
+ async commit(prepared, message, committer, newBranch) {
2501
3237
  try {
2502
- const fetchRes = await fetch(url, {
2503
- headers: getAuthHeaders(token, "application/json")
2504
- });
2505
- if (fetchRes.status === 200) {
2506
- const json = await fetchRes.json();
2507
- let error = false;
2508
- if (typeof json !== "object") {
2509
- error = {
2510
- details: "Invalid response: not an object"
2511
- };
2512
- }
2513
- if (typeof json.git !== "object") {
2514
- error = {
2515
- details: "Invalid response: missing git"
2516
- };
2517
- }
2518
- if (typeof json.git.commit !== "string") {
2519
- error = {
2520
- details: "Invalid response: missing git.commit"
2521
- };
2522
- }
2523
- if (typeof json.modules !== "object" || json.modules === null) {
2524
- error = {
2525
- details: "Invalid response: missing modules"
2526
- };
2527
- }
2528
- if (error) {
2529
- console.error("Could not initialize remote modules", error);
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
+ })
3257
+ });
3258
+ if (res.ok) {
3259
+ const parsed = CommitResponse.safeParse(await res.json());
3260
+ if (parsed.success) {
2530
3261
  return {
2531
- status: 500,
2532
- json: {
2533
- message: "Failed to fetch remote modules",
2534
- ...error
2535
- }
3262
+ updatedFiles: parsed.data.updatedFiles,
3263
+ commit: parsed.data.commit,
3264
+ branch: parsed.data.branch
2536
3265
  };
2537
3266
  }
2538
- this.moduleCache = json.modules;
2539
3267
  return {
2540
- status: 200
3268
+ error: {
3269
+ message: `Could not parse commit response. Error: ${zodValidationError.fromError(parsed.error)}`
3270
+ }
2541
3271
  };
2542
- } else {
2543
- return createJsonError(fetchRes);
2544
3272
  }
3273
+ return {
3274
+ error: {
3275
+ message: "Could not commit. HTTP error: " + res.status + " " + res.statusText
3276
+ }
3277
+ };
2545
3278
  } catch (err) {
2546
3279
  return {
2547
- status: 500,
2548
- json: {
2549
- message: "Failed to fetch: check network connection"
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"}`
2550
3282
  }
2551
3283
  };
2552
3284
  }
2553
3285
  }
2554
- async ensureInitialized(errorMessageType, cookies) {
2555
- const commit = this.options.git.commit;
2556
- if (!commit) {
2557
- return fp.result.err({
2558
- status: 400,
2559
- json: {
2560
- message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
2561
- }
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
2562
3302
  });
2563
- }
2564
- const res = await withAuth(this.options.valSecret, cookies, errorMessageType, async data => {
2565
- if (!this.moduleCache) {
2566
- return this.init(commit, data.token);
2567
- } else {
2568
- return {
2569
- status: 200
2570
- };
2571
- }
2572
- });
2573
- if (res.status === 200) {
2574
- return fp.result.ok(undefined);
2575
3303
  } else {
2576
- 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));
2577
3306
  }
2578
3307
  }
2579
- /* Auth endpoints */
2580
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
+ }
2581
3340
  async authorize(query) {
2582
3341
  if (typeof query.redirect_to !== "string") {
2583
3342
  return {
@@ -2587,7 +3346,7 @@ class ProxyValServer extends ValServer {
2587
3346
  }
2588
3347
  };
2589
3348
  }
2590
- const token = crypto__default["default"].randomUUID();
3349
+ const token = crypto.randomUUID();
2591
3350
  const redirectUrl = new URL(query.redirect_to);
2592
3351
  const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
2593
3352
  return {
@@ -2609,6 +3368,28 @@ class ProxyValServer extends ValServer {
2609
3368
  };
2610
3369
  }
2611
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
+ }
2612
3393
  const {
2613
3394
  success: callbackReqSuccess,
2614
3395
  error: callbackReqError
@@ -2637,10 +3418,22 @@ class ProxyValServer extends ValServer {
2637
3418
  };
2638
3419
  }
2639
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
+ }
2640
3433
  const cookie = encodeJwt({
2641
3434
  ...data,
2642
3435
  exp // this is the client side exp
2643
- }, this.options.valSecret);
3436
+ }, valSecret);
2644
3437
  return {
2645
3438
  status: 302,
2646
3439
  cookies: {
@@ -2662,7 +3455,7 @@ class ProxyValServer extends ValServer {
2662
3455
  redirectTo: callbackReqSuccess.redirect_uri || "/"
2663
3456
  };
2664
3457
  }
2665
- async logout() {
3458
+ async log() {
2666
3459
  return {
2667
3460
  status: 200,
2668
3461
  cookies: {
@@ -2676,8 +3469,41 @@ class ProxyValServer extends ValServer {
2676
3469
  };
2677
3470
  }
2678
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
+ }
2679
3497
  return withAuth(this.options.valSecret, cookies, "session", async data => {
2680
- 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);
2681
3507
  const fetchRes = await fetch(url, {
2682
3508
  headers: getAuthHeaders(data.token, "application/json")
2683
3509
  });
@@ -2686,7 +3512,7 @@ class ProxyValServer extends ValServer {
2686
3512
  status: fetchRes.status,
2687
3513
  json: {
2688
3514
  mode: "proxy",
2689
- enabled: await this.callbacks.isEnabled(),
3515
+ d: await this.callbacks.isEnabled(),
2690
3516
  ...(await fetchRes.json())
2691
3517
  }
2692
3518
  };
@@ -2702,8 +3528,17 @@ class ProxyValServer extends ValServer {
2702
3528
  });
2703
3529
  }
2704
3530
  async consumeCode(code) {
2705
- 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);
2706
3538
  url.searchParams.set("code", encodeURIComponent(code));
3539
+ if (!this.options.apiKey) {
3540
+ return null;
3541
+ }
2707
3542
  return fetch(url, {
2708
3543
  method: "POST",
2709
3544
  headers: getAuthHeaders(this.options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
@@ -2727,186 +3562,453 @@ class ProxyValServer extends ValServer {
2727
3562
  return null;
2728
3563
  });
2729
3564
  }
2730
- getAuthorizeUrl(publicValApiRoute, token) {
2731
- const url = new URL(`/auth/${this.options.remote}/authorize`, this.options.valBuildUrl);
2732
- 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`));
2733
3574
  url.searchParams.set("state", token);
2734
3575
  return url.toString();
2735
3576
  }
2736
3577
  getAppErrorUrl(error) {
2737
- 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);
2738
3585
  url.searchParams.set("error", encodeURIComponent(error));
2739
3586
  return url.toString();
2740
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
+ }
2741
3655
 
2742
- /* Patch endpoints */
2743
- async deletePatches(
2744
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2745
- query,
2746
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2747
- cookies) {
2748
- return withAuth(this.options.valSecret, cookies, "deletePatches", async ({
2749
- token
2750
- }) => {
2751
- const patchIds = query.id || [];
2752
- const url = new URL(`/v1/patches/${this.options.remote}/heads/${this.options.git.branch}/~`, this.options.valContentUrl);
2753
- const fetchRes = await fetch(url, {
2754
- method: "DELETE",
2755
- headers: getAuthHeaders(token, "application/json"),
2756
- body: JSON.stringify({
2757
- patches: patchIds
2758
- })
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
2759
3693
  });
2760
- if (fetchRes.status === 200) {
2761
- return {
2762
- status: fetchRes.status,
2763
- 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
2764
3867
  };
2765
- } else {
2766
- return createJsonError(fetchRes);
2767
3868
  }
2768
- });
2769
- }
2770
- async getPatches(query, cookies) {
2771
- return withAuth(this.options.valSecret, cookies, "getPatches", async ({
2772
- token
2773
- }) => {
2774
- const commit = this.options.git.commit;
2775
- if (!commit) {
2776
- return {
2777
- status: 400,
2778
- json: {
2779
- 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
2780
3887
  }
2781
3888
  };
2782
3889
  }
2783
- const patchIds = query.id || [];
2784
- const params = patchIds.length > 0 ? `commit=${encodeURIComponent(commit)}&${patchIds.map(id => `id=${encodeURIComponent(id)}`).join("&")}` : `commit=${encodeURIComponent(commit)}`;
2785
- const url = new URL(`/v1/patches/${this.options.remote}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
2786
- // Proxy patch to val.build
2787
- const fetchRes = await fetch(url, {
2788
- method: "GET",
2789
- headers: getAuthHeaders(token, "application/json")
2790
- });
2791
- if (fetchRes.status === 200) {
2792
- return {
2793
- status: fetchRes.status,
2794
- 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
2795
3915
  };
2796
- } else {
2797
- return createJsonError(fetchRes);
2798
3916
  }
2799
- });
3917
+ }
3918
+ return {
3919
+ status: 200,
3920
+ json: {
3921
+ schemaSha,
3922
+ modules
3923
+ }
3924
+ };
2800
3925
  }
2801
- async postPatches(body, cookies) {
2802
- const commit = this.options.git.commit;
2803
- if (!commit) {
3926
+ async postSave(body, cookies) {
3927
+ const auth = this.getAuth(cookies);
3928
+ if (auth.error) {
2804
3929
  return {
2805
3930
  status: 401,
2806
3931
  json: {
2807
- message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
3932
+ message: auth.error
2808
3933
  }
2809
3934
  };
2810
3935
  }
2811
- const params = new URLSearchParams({
2812
- commit
3936
+ const PostSaveBody = z.z.object({
3937
+ patchIds: z.z.array(z.z.string().refine(id => true // TODO:
3938
+ ))
2813
3939
  });
2814
- return withAuth(this.options.valSecret, cookies, "postPatches", async ({
2815
- token
2816
- }) => {
2817
- // First validate that the body has the right structure
2818
- const parsedPatches = z.z.record(Patch).safeParse(body);
2819
- if (!parsedPatches.success) {
2820
- return {
2821
- status: 400,
2822
- json: {
2823
- message: "Invalid patch(es)",
2824
- details: parsedPatches.error.issues
2825
- }
2826
- };
2827
- }
2828
- const patches = parsedPatches.data;
2829
- const url = new URL(`/v1/patches/${this.options.remote}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
2830
- // Proxy patch to val.build
2831
- const fetchRes = await fetch(url, {
2832
- method: "POST",
2833
- headers: getAuthHeaders(token, "application/json"),
2834
- body: JSON.stringify(patches)
2835
- });
2836
- 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);
2837
3968
  return {
2838
- status: fetchRes.status,
2839
- json: await fetchRes.json()
3969
+ status: 200,
3970
+ json: {} // TODO:
2840
3971
  };
2841
- } else {
2842
- return createJsonError(fetchRes);
2843
- }
2844
- });
2845
- }
2846
- async getMetadata(filePath, sha256) {
2847
- const url = new URL(`/v1/metadata/${this.options.remote}${filePath}?commit=${this.options.git.commit}${sha256 ? `&sha256=${sha256}` : ""}`, this.options.valContentUrl);
2848
- const fetchRes = await fetch(url, {
2849
- headers: {
2850
- Authorization: `Bearer ${this.options.apiKey}`
2851
- }
2852
- });
2853
- if (fetchRes.status === 200) {
2854
- const json = await fetchRes.json();
2855
- if (json.type === "file") {
2856
- return json;
2857
- } else if (json.type === "image") {
2858
- return json;
2859
3972
  }
3973
+ return {
3974
+ status: 401,
3975
+ json: {
3976
+ message: "Unauthorized"
3977
+ }
3978
+ };
3979
+ } else {
3980
+ throw new Error("Invalid server ops");
2860
3981
  }
2861
- return undefined;
2862
3982
  }
2863
- async getFiles(filePath, query, _cookies, reqHeaders) {
2864
- const url = new URL(`/v1/files/${this.options.remote}${filePath}`, this.options.valContentUrl);
2865
- if (typeof query.sha256 === "string") {
2866
- 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);
2867
4000
  }
2868
- const fetchRes = await fetch(url, {
2869
- headers: {
2870
- Authorization: `Bearer ${this.options.apiKey}`
2871
- }
2872
- });
2873
- if (fetchRes.status === 200) {
2874
- // TODO: does this stream data?
2875
- if (fetchRes.body) {
2876
- return {
2877
- status: fetchRes.status,
2878
- headers: {
2879
- "Content-Type": fetchRes.headers.get("Content-Type") || "",
2880
- "Content-Length": fetchRes.headers.get("Content-Length") || "0",
2881
- "Cache-Control": fetchRes.headers.get("Cache-Control") || ""
2882
- },
2883
- body: fetchRes.body
2884
- };
2885
- } else {
2886
- return {
2887
- status: 500,
2888
- json: {
2889
- message: "No body in response"
2890
- }
2891
- };
2892
- }
4001
+ if (fileBuffer) {
4002
+ return {
4003
+ status: 200,
4004
+ body: bufferToReadableStream(fileBuffer)
4005
+ };
2893
4006
  } else {
2894
- if (!(reqHeaders.host && reqHeaders["x-forwarded-proto"])) {
2895
- return {
2896
- status: 500,
2897
- json: {
2898
- message: "Missing host or x-forwarded-proto header"
2899
- }
2900
- };
2901
- }
2902
- const host = `${reqHeaders["x-forwarded-proto"]}://${reqHeaders["host"]}`;
2903
- const staticPublicUrl = new URL(filePath.slice("/public".length), host).toString();
2904
- const fetchRes = await fetch(staticPublicUrl);
2905
4007
  return {
2906
- status: fetchRes.status,
2907
- // forward headers from static public url
2908
- headers: Object.fromEntries(fetchRes.headers.entries()),
2909
- body: fetchRes.body
4008
+ status: 404,
4009
+ json: {
4010
+ message: "File not found"
4011
+ }
2910
4012
  };
2911
4013
  }
2912
4014
  }
@@ -3011,22 +4113,6 @@ function getStateFromCookie(stateCookie) {
3011
4113
  };
3012
4114
  }
3013
4115
  }
3014
- async function createJsonError(fetchRes) {
3015
- var _fetchRes$headers$get;
3016
- if ((_fetchRes$headers$get = fetchRes.headers.get("Content-Type")) !== null && _fetchRes$headers$get !== void 0 && _fetchRes$headers$get.includes("application/json")) {
3017
- return {
3018
- status: fetchRes.status,
3019
- json: await fetchRes.json()
3020
- };
3021
- }
3022
- console.error("Unexpected failure (did not get a json) - Val down?", fetchRes.status, await fetchRes.text());
3023
- return {
3024
- status: fetchRes.status,
3025
- json: {
3026
- message: "Unexpected failure (did not get a json) - Val down?"
3027
- }
3028
- };
3029
- }
3030
4116
  function createStateCookie(state) {
3031
4117
  return Buffer.from(JSON.stringify(state), "utf8").toString("base64");
3032
4118
  }
@@ -3098,42 +4184,147 @@ function getAuthHeaders(token, type) {
3098
4184
  Authorization: `Bearer ${token}`
3099
4185
  };
3100
4186
  }
3101
- function createParams(params) {
3102
- let paramIdx = 0;
3103
- let paramsString = "";
3104
- for (const key in params) {
3105
- const param = params[key];
3106
- if (Array.isArray(param)) {
3107
- for (const value of param) {
3108
- 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;
3109
4203
  }
3110
- } else if (param) {
3111
- paramsString += `${key}=${encodeURIComponent(param)}`;
3112
- }
3113
- if (paramIdx < Object.keys(params).length - 1) {
3114
- paramsString += "&";
4204
+ controller.close();
3115
4205
  }
3116
- 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
+ };
3117
4217
  }
3118
- return paramsString;
4218
+ if (overrideHost) {
4219
+ return overrideHost + "?redirect_to=" + encodeURIComponent(query.redirect_to);
4220
+ }
4221
+ return query.redirect_to;
3119
4222
  }
3120
4223
 
3121
- async function createValServer(route, opts, callbacks) {
3122
- const serverOpts = await initHandlerOptions(route, opts);
3123
- if (serverOpts.mode === "proxy") {
3124
- const projectRoot = process.cwd(); //[process.cwd(), opts.root || ""] .filter((seg) => seg) .join("/");
3125
- return new ProxyValServer(projectRoot, serverOpts, opts, callbacks);
3126
- } else {
3127
- return new LocalValServer(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;
3128
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);
3129
4317
  }
3130
4318
  async function initHandlerOptions(route, opts) {
3131
4319
  const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
3132
4320
  const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
3133
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";
3134
4326
  if (isProxyMode) {
3135
4327
  var _opts$versions, _opts$versions2;
3136
- const valBuildUrl = opts.valBuildUrl || process.env.VAL_BUILD_URL || "https://app.val.build";
3137
4328
  if (!maybeApiKey || !maybeValSecret) {
3138
4329
  throw new Error("VAL_API_KEY and VAL_SECRET env vars must both be set in proxy mode");
3139
4330
  }
@@ -3146,9 +4337,8 @@ async function initHandlerOptions(route, opts) {
3146
4337
  if (!maybeGitBranch) {
3147
4338
  throw new Error("VAL_GIT_BRANCH env var must be set in proxy mode");
3148
4339
  }
3149
- const maybeValRemote = opts.remote || process.env.VAL_REMOTE;
3150
- if (!maybeValRemote) {
3151
- 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.");
3152
4342
  }
3153
4343
  const coreVersion = (_opts$versions = opts.versions) === null || _opts$versions === void 0 ? void 0 : _opts$versions.core;
3154
4344
  if (!coreVersion) {
@@ -3159,43 +4349,40 @@ async function initHandlerOptions(route, opts) {
3159
4349
  throw new Error("Could not determine version of @valbuild/next");
3160
4350
  }
3161
4351
  return {
3162
- mode: "proxy",
4352
+ mode: "http",
3163
4353
  route,
3164
4354
  apiKey: maybeApiKey,
3165
4355
  valSecret: maybeValSecret,
3166
- valBuildUrl,
4356
+ commit: maybeGitCommit,
4357
+ branch: maybeGitBranch,
4358
+ root: opts.root,
4359
+ project: maybeValProject,
4360
+ valEnableRedirectUrl,
4361
+ valDisableRedirectUrl,
3167
4362
  valContentUrl,
3168
- versions: {
3169
- core: coreVersion,
3170
- next: nextVersion
3171
- },
3172
- git: {
3173
- commit: maybeGitCommit,
3174
- branch: maybeGitBranch
3175
- },
3176
- remote: maybeValRemote,
3177
- valEnableRedirectUrl: opts.valEnableRedirectUrl || process.env.VAL_ENABLE_REDIRECT_URL,
3178
- valDisableRedirectUrl: opts.valDisableRedirectUrl || process.env.VAL_DISABLE_REDIRECT_URL
4363
+ valBuildUrl
3179
4364
  };
3180
4365
  } else {
3181
4366
  const cwd = process.cwd();
3182
- const service = await createService(cwd, opts);
3183
- const git = await safeReadGit(cwd);
4367
+ const valBuildUrl = opts.valBuildUrl || process.env.VAL_BUILD_URL || "https://app.val.build";
3184
4368
  return {
3185
- mode: "local",
3186
- service,
3187
- valEnableRedirectUrl: opts.valEnableRedirectUrl || process.env.VAL_ENABLE_REDIRECT_URL,
3188
- valDisableRedirectUrl: opts.valDisableRedirectUrl || process.env.VAL_DISABLE_REDIRECT_URL,
3189
- git: {
3190
- commit: process.env.VAL_GIT_COMMIT || git.commit,
3191
- branch: process.env.VAL_GIT_BRANCH || git.branch
3192
- }
4369
+ mode: "fs",
4370
+ cwd,
4371
+ route,
4372
+ valDisableRedirectUrl,
4373
+ valEnableRedirectUrl,
4374
+ valBuildUrl,
4375
+ apiKey: maybeApiKey,
4376
+ valSecret: maybeValSecret,
4377
+ project: maybeValProject
3193
4378
  };
3194
4379
  }
3195
4380
  }
4381
+
4382
+ // TODO: remove
3196
4383
  async function safeReadGit(cwd) {
3197
4384
  async function findGitHead(currentDir, depth) {
3198
- const gitHeadPath = path__namespace.join(currentDir, ".git", "HEAD");
4385
+ const gitHeadPath = fsPath__namespace.join(currentDir, ".git", "HEAD");
3199
4386
  if (depth > 1000) {
3200
4387
  console.error(`Reached max depth while scanning for .git folder. Current working dir: ${cwd}.`);
3201
4388
  return {
@@ -3219,7 +4406,7 @@ async function safeReadGit(cwd) {
3219
4406
  };
3220
4407
  }
3221
4408
  } catch (error) {
3222
- const parentDir = path__namespace.dirname(currentDir);
4409
+ const parentDir = fsPath__namespace.dirname(currentDir);
3223
4410
 
3224
4411
  // We've reached the root directory
3225
4412
  if (parentDir === currentDir) {
@@ -3243,7 +4430,7 @@ async function safeReadGit(cwd) {
3243
4430
  }
3244
4431
  async function readCommit(gitDir, branchName) {
3245
4432
  try {
3246
- 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();
3247
4434
  } catch (err) {
3248
4435
  return undefined;
3249
4436
  }
@@ -3260,10 +4447,6 @@ function createValApiRouter(route, valServerPromise, convert) {
3260
4447
  return async req => {
3261
4448
  var _req$method;
3262
4449
  const valServer = await valServerPromise;
3263
- const requestHeaders = {
3264
- host: req.headers.get("host"),
3265
- "x-forwarded-proto": req.headers.get("x-forwarded-proto")
3266
- };
3267
4450
  const url = new URL(req.url);
3268
4451
  if (!url.pathname.startsWith(route)) {
3269
4452
  const error = {
@@ -3324,35 +4507,40 @@ function createValApiRouter(route, valServerPromise, convert) {
3324
4507
  return convert(await valServer.disable({
3325
4508
  redirect_to: url.searchParams.get("redirect_to") || undefined
3326
4509
  }));
3327
- } else if (method === "POST" && path === "/commit") {
3328
- const body = await req.json();
3329
- return convert(await valServer.postCommit(body, getCookies(req, [VAL_SESSION_COOKIE]), requestHeaders));
3330
- } else if (method === "POST" && path === "/validate") {
4510
+ } else if (method === "POST" && path === "/save") {
3331
4511
  const body = await req.json();
3332
- return convert(await valServer.postValidate(body, getCookies(req, [VAL_SESSION_COOKIE]), requestHeaders));
3333
- } else if (method === "GET" && path.startsWith(TREE_PATH_PREFIX)) {
3334
- return withTreePath(path, TREE_PATH_PREFIX)(async treePath => convert(await valServer.getTree(treePath, {
3335
- patch: url.searchParams.get("patch") || undefined,
3336
- schema: url.searchParams.get("schema") || undefined,
3337
- source: url.searchParams.get("source") || undefined,
3338
- validate: url.searchParams.get("validate") || undefined
3339
- }, 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]))));
3340
4531
  } else if (method === "GET" && path.startsWith(PATCHES_PATH_PREFIX)) {
3341
4532
  return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.getPatches({
3342
- id: url.searchParams.getAll("id")
4533
+ authors: url.searchParams.getAll("author")
3343
4534
  }, getCookies(req, [VAL_SESSION_COOKIE]))));
3344
- } else if (method === "POST" && path.startsWith(PATCHES_PATH_PREFIX)) {
3345
- const body = await req.json();
3346
- return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.postPatches(body, getCookies(req, [VAL_SESSION_COOKIE]))));
3347
4535
  } else if (method === "DELETE" && path.startsWith(PATCHES_PATH_PREFIX)) {
3348
4536
  return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.deletePatches({
3349
4537
  id: url.searchParams.getAll("id")
3350
4538
  }, getCookies(req, [VAL_SESSION_COOKIE]))));
3351
- } else if (path.startsWith(FILES_PATH_PREFIX)) {
4539
+ } else if (method === "GET" && path.startsWith(FILES_PATH_PREFIX)) {
3352
4540
  const treePath = path.slice(FILES_PATH_PREFIX.length);
3353
4541
  return convert(await valServer.getFiles(treePath, {
3354
- sha256: url.searchParams.get("sha256") || undefined
3355
- }, getCookies(req, [VAL_SESSION_COOKIE]), requestHeaders));
4542
+ patch_id: url.searchParams.get("patch_id") || undefined
4543
+ }));
3356
4544
  } else {
3357
4545
  return convert({
3358
4546
  status: 404,
@@ -3404,10 +4592,10 @@ class ValFSHost {
3404
4592
  return this.currentDirectory;
3405
4593
  }
3406
4594
  getCanonicalFileName(fileName) {
3407
- if (path__namespace["default"].isAbsolute(fileName)) {
3408
- return path__namespace["default"].normalize(fileName);
4595
+ if (fsPath__namespace["default"].isAbsolute(fileName)) {
4596
+ return fsPath__namespace["default"].normalize(fileName);
3409
4597
  }
3410
- return path__namespace["default"].resolve(this.getCurrentDirectory(), fileName);
4598
+ return fsPath__namespace["default"].resolve(this.getCurrentDirectory(), fileName);
3411
4599
  }
3412
4600
  fileExists(fileName) {
3413
4601
  return this.valFS.fileExists(fileName);
@@ -3420,6 +4608,14 @@ class ValFSHost {
3420
4608
  }
3421
4609
  }
3422
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
+
3423
4619
  // TODO: find a better name? transformFixesToPatch?
3424
4620
  async function createFixPatch(config, apply, sourcePath, validationError) {
3425
4621
  async function getImageMetadata() {
@@ -3428,7 +4624,7 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
3428
4624
  // TODO:
3429
4625
  throw Error("Cannot fix image without a file reference");
3430
4626
  }
3431
- const filename = path__namespace["default"].join(config.projectRoot, fileRef);
4627
+ const filename = fsPath__namespace["default"].join(config.projectRoot, fileRef);
3432
4628
  const buffer = fs__default["default"].readFileSync(filename);
3433
4629
  return extractImageMetadata(filename, buffer);
3434
4630
  }
@@ -3438,7 +4634,7 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
3438
4634
  // TODO:
3439
4635
  throw Error("Cannot fix file without a file reference");
3440
4636
  }
3441
- const filename = path__namespace["default"].join(config.projectRoot, fileRef);
4637
+ const filename = fsPath__namespace["default"].join(config.projectRoot, fileRef);
3442
4638
  const buffer = fs__default["default"].readFileSync(filename);
3443
4639
  return extractFileMetadata(fileRef, buffer);
3444
4640
  }
@@ -3604,7 +4800,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
3604
4800
  };
3605
4801
  }
3606
4802
 
3607
- exports.LocalValServer = LocalValServer;
3608
4803
  exports.Patch = Patch;
3609
4804
  exports.PatchJSON = PatchJSON;
3610
4805
  exports.Service = Service;