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