@valbuild/server 0.61.0 → 0.62.1

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