@valbuild/server 0.63.5 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,12 +8,12 @@ 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 { VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE, VAL_SESSION_COOKIE, Api } from '@valbuild/shared/internal';
11
12
  import { createUIRequestHandler } from '@valbuild/ui/server';
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
13
  import crypto$1 from 'crypto';
14
14
  import z$1, { z } from 'zod';
15
15
  import sizeOf from 'image-size';
16
- import { fromError } from 'zod-validation-error';
16
+ import { fromError, fromZodError } from 'zod-validation-error';
17
17
 
18
18
  class ValSyntaxError {
19
19
  constructor(message, node) {
@@ -1243,56 +1243,6 @@ function encodeJwt(payload, sessionKey) {
1243
1243
  return `${jwtHeaderBase64}.${payloadBase64}.${crypto$1.createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
1244
1244
  }
1245
1245
 
1246
- const textEncoder$2 = new TextEncoder();
1247
- async function extractImageMetadata(filename, input) {
1248
- const imageSize = sizeOf(input);
1249
- let mimeType = null;
1250
- if (imageSize.type) {
1251
- const possibleMimeType = `image/${imageSize.type}`;
1252
- if (MIME_TYPES_TO_EXT[possibleMimeType]) {
1253
- mimeType = possibleMimeType;
1254
- }
1255
- const filenameBasedLookup = filenameToMimeType(filename);
1256
- if (filenameBasedLookup) {
1257
- mimeType = filenameBasedLookup;
1258
- }
1259
- }
1260
- if (!mimeType) {
1261
- mimeType = "application/octet-stream";
1262
- }
1263
- let {
1264
- width,
1265
- height
1266
- } = imageSize;
1267
- if (!width || !height) {
1268
- width = 0;
1269
- height = 0;
1270
- }
1271
- const sha256 = getSha256(mimeType, input);
1272
- return {
1273
- width,
1274
- height,
1275
- sha256,
1276
- mimeType
1277
- };
1278
- }
1279
- function getSha256(mimeType, input) {
1280
- return Internal.getSHA256Hash(textEncoder$2.encode(
1281
- // TODO: we should probably store the mimetype in the metadata and reuse it here
1282
- `data:${mimeType};base64,${input.toString("base64")}`));
1283
- }
1284
- async function extractFileMetadata(filename, input) {
1285
- let mimeType = filenameToMimeType(filename);
1286
- if (!mimeType) {
1287
- mimeType = "application/octet-stream";
1288
- }
1289
- const sha256 = getSha256(mimeType, input);
1290
- return {
1291
- sha256,
1292
- mimeType
1293
- };
1294
- }
1295
-
1296
1246
  /* eslint-disable @typescript-eslint/no-unused-vars */
1297
1247
  const textEncoder$1 = new TextEncoder();
1298
1248
  const jsonOps = new JSONOps();
@@ -1321,15 +1271,20 @@ class ValOps {
1321
1271
  this.modulesErrors = null;
1322
1272
  }
1323
1273
  hash(input) {
1324
- let str;
1325
- if (typeof input === "string") {
1326
- str = input;
1327
- } else {
1328
- str = JSON.stringify(input);
1329
- }
1330
- return Internal.getSHA256Hash(textEncoder$1.encode(str));
1274
+ return Internal.getSHA256Hash(textEncoder$1.encode(input));
1331
1275
  }
1332
1276
 
1277
+ // #region stat
1278
+ /**
1279
+ * Get the status from Val
1280
+ *
1281
+ * This works differently in ValOpsFS and ValOpsHttp:
1282
+ * - In ValOpsFS (for dev mode) works using long-polling operations since we cannot use WebSockets in the host Next.js server and we do not want to hammer the server with requests (though we could argue that it would be ok in dev, it is not up to our standards as a kick-ass CMS).
1283
+ * - In ValOpsHttp (in production) it returns a WebSocket URL so that the client can connect directly.
1284
+ *
1285
+ * The reason we do not use long polling in production is that Vercel (a very likely host for Next.js), bills by wall time and long polling would therefore be very expensive.
1286
+ */
1287
+
1333
1288
  // #region initTree
1334
1289
  async initTree() {
1335
1290
  if (this.baseSha === null || this.schemaSha === null || this.sources === null || this.schemas === null || this.modulesErrors === null) {
@@ -1386,17 +1341,25 @@ class ValOps {
1386
1341
  addModuleError(`source in ${path} is undefined`, moduleIdx, path);
1387
1342
  return;
1388
1343
  }
1344
+ let serializedSchema;
1345
+ try {
1346
+ serializedSchema = schema.serialize();
1347
+ } catch (e) {
1348
+ const message = e instanceof Error ? e.message : JSON.stringify(e);
1349
+ addModuleError(`Could not serialize module: '${path}'. Error: ${message}`, moduleIdx, path);
1350
+ return;
1351
+ }
1389
1352
  const pathM = path;
1390
1353
  currentSources[pathM] = source;
1391
1354
  currentSchemas[pathM] = schema;
1392
1355
  // make sure the checks above is enough that this does not fail - even if val modules are not set up correctly
1393
- baseSha += this.hash({
1356
+ baseSha = this.hash(baseSha + JSON.stringify({
1394
1357
  path,
1395
- schema: schema.serialize(),
1358
+ schema: serializedSchema,
1396
1359
  source,
1397
1360
  modulesErrors: currentModulesErrors
1398
- });
1399
- schemaSha += this.hash(schema.serialize());
1361
+ }));
1362
+ schemaSha = this.hash(schemaSha + JSON.stringify(serializedSchema));
1400
1363
  });
1401
1364
  }
1402
1365
  this.sources = currentSources;
@@ -1490,10 +1453,14 @@ class ValOps {
1490
1453
  if (!errors[path]) {
1491
1454
  errors[path] = [];
1492
1455
  }
1493
- errors[path].push({
1456
+ errors[path].push(...patches.map(({
1457
+ patchId
1458
+ }) => ({
1459
+ patchId,
1494
1460
  invalidPath: true,
1461
+ skipped: true,
1495
1462
  error: new PatchError(`Module at path: '${path}' not found`)
1496
- });
1463
+ })));
1497
1464
  }
1498
1465
  patchedSources[path] = sources[path];
1499
1466
  for (const {
@@ -1502,6 +1469,7 @@ class ValOps {
1502
1469
  if (errors[path]) {
1503
1470
  errors[path].push({
1504
1471
  patchId: patchId,
1472
+ skipped: true,
1505
1473
  error: new PatchError(`Cannot apply patch: previous errors exists`)
1506
1474
  });
1507
1475
  } else {
@@ -1509,6 +1477,7 @@ class ValOps {
1509
1477
  if (!patchData) {
1510
1478
  errors[path] = [{
1511
1479
  patchId: patchId,
1480
+ skipped: false,
1512
1481
  error: new PatchError(`Patch not found`)
1513
1482
  }];
1514
1483
  continue;
@@ -1539,6 +1508,7 @@ class ValOps {
1539
1508
  }
1540
1509
  errors[path].push({
1541
1510
  patchId: patchId,
1511
+ skipped: false,
1542
1512
  error: patchRes.error
1543
1513
  });
1544
1514
  } else {
@@ -1585,25 +1555,27 @@ class ValOps {
1585
1555
  }
1586
1556
  for (const [sourcePathS, validationErrors] of Object.entries(res)) {
1587
1557
  const sourcePath = sourcePathS;
1588
- for (const validationError of validationErrors) {
1589
- if (isOnlyFileCheckValidationError(validationError)) {
1590
- if (files[sourcePath]) {
1591
- throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
1592
- }
1593
- const value = validationError.value;
1594
- if (isFileSource(value)) {
1595
- files[sourcePath] = value;
1596
- }
1597
- } else {
1598
- if (!errors[path]) {
1599
- errors[path] = {
1600
- validations: {}
1601
- };
1602
- }
1603
- if (!errors[path].validations[sourcePath]) {
1604
- errors[path].validations[sourcePath] = [];
1558
+ if (validationErrors) {
1559
+ for (const validationError of validationErrors) {
1560
+ if (isOnlyFileCheckValidationError(validationError)) {
1561
+ if (files[sourcePath]) {
1562
+ throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
1563
+ }
1564
+ const value = validationError.value;
1565
+ if (isFileSource(value)) {
1566
+ files[sourcePath] = value;
1567
+ }
1568
+ } else {
1569
+ if (!errors[path]) {
1570
+ errors[path] = {
1571
+ validations: {}
1572
+ };
1573
+ }
1574
+ if (!errors[path].validations[sourcePath]) {
1575
+ errors[path].validations[sourcePath] = [];
1576
+ }
1577
+ errors[path].validations[sourcePath].push(validationError);
1605
1578
  }
1606
- errors[path].validations[sourcePath].push(validationError);
1607
1579
  }
1608
1580
  }
1609
1581
  }
@@ -1791,8 +1763,22 @@ class ValOps {
1791
1763
  const patchRes = applyPatch(tsSourceFile, tsOps, sourceFileOps);
1792
1764
  if (result.isErr(patchRes)) {
1793
1765
  if (Array.isArray(patchRes.error)) {
1766
+ for (const error of patchRes.error) {
1767
+ console.error("Could not patch", JSON.stringify({
1768
+ path,
1769
+ patchId,
1770
+ error,
1771
+ sourceFileOps
1772
+ }, null, 2));
1773
+ }
1794
1774
  errors.push(...patchRes.error);
1795
1775
  } else {
1776
+ console.error("Could not patch", JSON.stringify({
1777
+ path,
1778
+ patchId,
1779
+ error: patchRes.error,
1780
+ sourceFileOps
1781
+ }, null, 2));
1796
1782
  errors.push(patchRes.error);
1797
1783
  }
1798
1784
  triedPatches.push(patchId);
@@ -1886,16 +1872,23 @@ class ValOps {
1886
1872
  }
1887
1873
 
1888
1874
  // #region createPatch
1889
- async createPatch(path, patch, authorId) {
1890
- const {
1891
- sources,
1892
- schemas,
1893
- moduleErrors
1894
- } = await this.initTree();
1875
+ async createPatch(path, patchAnalysis, patch, authorId) {
1876
+ const initTree = await this.initTree();
1877
+ const schemas = initTree.schemas;
1878
+ const moduleErrors = initTree.moduleErrors;
1879
+ let sources = initTree.sources;
1880
+ if (patchAnalysis) {
1881
+ const tree = await this.getTree(patchAnalysis);
1882
+ sources = {
1883
+ ...sources,
1884
+ ...tree.sources
1885
+ };
1886
+ }
1895
1887
  const source = sources[path];
1896
1888
  const schema = schemas[path];
1897
1889
  const moduleError = moduleErrors.find(e => e.path === path);
1898
1890
  if (moduleError) {
1891
+ console.error(`Cannot patch. Module at path: '${path}' has fatal errors: "${moduleError.message}"`);
1899
1892
  return {
1900
1893
  error: {
1901
1894
  message: `Cannot patch. Module at path: '${path}' has fatal errors: ` + moduleErrors.map(m => `"${m.message}"`).join(" and ")
@@ -1903,6 +1896,7 @@ class ValOps {
1903
1896
  };
1904
1897
  }
1905
1898
  if (!source) {
1899
+ console.error(`Cannot patch. Module source at path: '${path}' does not exist`);
1906
1900
  return {
1907
1901
  error: {
1908
1902
  message: `Cannot patch. Module source at path: '${path}' does not exist`
@@ -1910,6 +1904,7 @@ class ValOps {
1910
1904
  };
1911
1905
  }
1912
1906
  if (!schema) {
1907
+ console.error(`Cannot patch. Module schema at path: '${path}' does not exist`);
1913
1908
  return {
1914
1909
  error: {
1915
1910
  message: `Cannot patch. Module schema at path: '${path}' does not exist`
@@ -1927,10 +1922,12 @@ class ValOps {
1927
1922
  filePath
1928
1923
  } = op;
1929
1924
  if (files[filePath]) {
1925
+ console.error(`Cannot have multiple files with same path in same patch. Path: ${filePath}`);
1930
1926
  files[filePath] = {
1931
1927
  error: new PatchError("Cannot have multiple files with same path in same patch")
1932
1928
  };
1933
1929
  } else if (typeof value !== "string") {
1930
+ console.error(`Value is not a string. Path: ${filePath}. Value: ${value}`);
1934
1931
  files[filePath] = {
1935
1932
  error: new PatchError("Value is not a string")
1936
1933
  };
@@ -1946,15 +1943,14 @@ class ValOps {
1946
1943
  path: op.path,
1947
1944
  filePath,
1948
1945
  nestedFilePath: op.nestedFilePath,
1949
- value: {
1950
- sha256
1951
- }
1946
+ value: sha256
1952
1947
  });
1953
1948
  }
1954
1949
  }
1955
1950
  }
1956
1951
  const saveRes = await this.saveSourceFilePatch(path, sourceFileOps, authorId);
1957
1952
  if (saveRes.error) {
1953
+ console.error(`Could not save source file patch at path: '${path}'. Error: ${saveRes.error.message}`);
1958
1954
  return {
1959
1955
  error: saveRes.error
1960
1956
  };
@@ -1978,17 +1974,20 @@ class ValOps {
1978
1974
  ? "image" : schemaAtPath instanceof FileSchema ? "file" : schemaAtPath.serialize().type;
1979
1975
  } catch (e) {
1980
1976
  if (e instanceof Error) {
1977
+ console.error(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`);
1981
1978
  return {
1982
1979
  filePath,
1983
1980
  error: new PatchError(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`)
1984
1981
  };
1985
1982
  }
1983
+ console.error(`Could not resolve file type at: ${modulePath}. Unknown error.`);
1986
1984
  return {
1987
1985
  filePath,
1988
1986
  error: new PatchError(`Could not resolve file type at: ${modulePath}. Unknown error.`)
1989
1987
  };
1990
1988
  }
1991
1989
  if (type !== "image" && type !== "file") {
1990
+ console.error("Unknown file type (resolved from schema): " + type);
1992
1991
  return {
1993
1992
  filePath,
1994
1993
  error: new PatchError("Unknown file type (resolved from schema): " + type)
@@ -1996,6 +1995,7 @@ class ValOps {
1996
1995
  }
1997
1996
  const mimeType = getMimeTypeFromBase64(data.value);
1998
1997
  if (!mimeType) {
1998
+ console.error("Could not get mimeType from base 64 encoded value");
1999
1999
  return {
2000
2000
  filePath,
2001
2001
  error: new PatchError("Could not get mimeType from base 64 encoded value. First chars were: " + data.value.slice(0, 20))
@@ -2003,6 +2003,7 @@ class ValOps {
2003
2003
  }
2004
2004
  const buffer = bufferFromDataUrl(data.value);
2005
2005
  if (!buffer) {
2006
+ console.error("Could not create buffer from base 64 encoded value");
2006
2007
  return {
2007
2008
  filePath,
2008
2009
  error: new PatchError("Could not create buffer from base 64 encoded value")
@@ -2010,6 +2011,7 @@ class ValOps {
2010
2011
  }
2011
2012
  const metadataOps = createMetadataFromBuffer(type, mimeType, buffer);
2012
2013
  if (metadataOps.errors) {
2014
+ console.error(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`);
2013
2015
  return {
2014
2016
  filePath,
2015
2017
  error: new PatchError(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`)
@@ -2031,6 +2033,14 @@ class ValOps {
2031
2033
  };
2032
2034
  }
2033
2035
  }));
2036
+ const errors = saveFileRes.filter(f => !!f.error);
2037
+ if (errors.length > 0) {
2038
+ return {
2039
+ error: {
2040
+ message: "Could not save patch: " + errors.map(e => e.error.message).join(", ")
2041
+ }
2042
+ };
2043
+ }
2034
2044
  return {
2035
2045
  patchId,
2036
2046
  files: saveFileRes,
@@ -2055,14 +2065,13 @@ function isFileSource(value) {
2055
2065
  }
2056
2066
  function getFieldsForType(type) {
2057
2067
  if (type === "file") {
2058
- return ["sha256", "mimeType"];
2068
+ return ["mimeType"];
2059
2069
  } else if (type === "image") {
2060
- return ["sha256", "mimeType", "height", "width"];
2070
+ return ["mimeType", "height", "width"];
2061
2071
  }
2062
2072
  throw new Error("Unknown type: " + type);
2063
2073
  }
2064
2074
  function createMetadataFromBuffer(type, mimeType, buffer) {
2065
- const sha256 = getSha256(mimeType, buffer);
2066
2075
  const errors = [];
2067
2076
  let availableMetadata;
2068
2077
  if (type === "image") {
@@ -2071,7 +2080,7 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
2071
2080
  height,
2072
2081
  type
2073
2082
  } = sizeOf(buffer);
2074
- const normalizedType = type === "jpg" ? "jpeg" : type;
2083
+ const normalizedType = type === "jpg" ? "jpeg" : type === "svg" ? "svg+xml" : type;
2075
2084
  if (type !== undefined && `image/${normalizedType}` !== mimeType) {
2076
2085
  return {
2077
2086
  errors: [{
@@ -2080,14 +2089,12 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
2080
2089
  };
2081
2090
  }
2082
2091
  availableMetadata = {
2083
- sha256: sha256,
2084
2092
  mimeType,
2085
2093
  height,
2086
2094
  width
2087
2095
  };
2088
2096
  } else {
2089
2097
  availableMetadata = {
2090
- sha256: sha256,
2091
2098
  mimeType
2092
2099
  };
2093
2100
  }
@@ -2207,9 +2214,7 @@ const OperationT = z$1.discriminatedUnion("op", [z$1.object({
2207
2214
  path: z$1.array(z$1.string()),
2208
2215
  filePath: z$1.string(),
2209
2216
  nestedFilePath: z$1.array(z$1.string()).optional(),
2210
- value: z$1.union([z$1.string(), z$1.object({
2211
- sha256: z$1.string()
2212
- })])
2217
+ value: z$1.string()
2213
2218
  }).strict()]);
2214
2219
  const Patch = z$1.array(OperationT);
2215
2220
 
@@ -2223,6 +2228,161 @@ class ValOpsFS extends ValOps {
2223
2228
  async onInit() {
2224
2229
  // do nothing
2225
2230
  }
2231
+ async getStat(params) {
2232
+ // In ValOpsFS, we don't have a websocket server to listen to file changes so we use long-polling.
2233
+ // If a file that Val depends on changes, we break the connection and tell the client to request again to get the latest values.
2234
+ try {
2235
+ var _this$options, _this$options2;
2236
+ const currentBaseSha = await this.getBaseSha();
2237
+ const currentSchemaSha = await this.getSchemaSha();
2238
+ const moduleFilePaths = Object.keys(await this.getSchemas());
2239
+ const patchData = await this.readPatches();
2240
+ const patches = [];
2241
+ // TODO: use proper patch sequences when available:
2242
+ for (const [patchId] of Object.entries(patchData.patches).sort(([, a], [, b]) => {
2243
+ return a.createdAt.localeCompare(b.createdAt, undefined);
2244
+ })) {
2245
+ patches.push(patchId);
2246
+ }
2247
+ // something changed: return immediately
2248
+ const didChange = !params || currentBaseSha !== params.baseSha || currentSchemaSha !== params.schemaSha || patches.length !== params.patches.length || patches.some((p, i) => p !== params.patches[i]);
2249
+ if (didChange) {
2250
+ return {
2251
+ type: "did-change",
2252
+ baseSha: currentBaseSha,
2253
+ schemaSha: currentSchemaSha,
2254
+ patches
2255
+ };
2256
+ }
2257
+ let fsWatcher = null;
2258
+ let stopPolling = false;
2259
+ const didDirectoryChangeUsingPolling = (dir, interval, setHandle) => {
2260
+ const mtimeInDir = {};
2261
+ if (fs.existsSync(dir)) {
2262
+ for (const file of fs.readdirSync(dir)) {
2263
+ mtimeInDir[file] = fs.statSync(fsPath__default.join(dir, file)).mtime.getTime();
2264
+ }
2265
+ }
2266
+ return new Promise(resolve => {
2267
+ const go = resolve => {
2268
+ const start = Date.now();
2269
+ if (fs.existsSync(dir)) {
2270
+ const subDirs = fs.readdirSync(dir);
2271
+ // amount of files changed
2272
+ if (subDirs.length !== Object.keys(mtimeInDir).length) {
2273
+ resolve("request-again");
2274
+ }
2275
+ for (const file of fs.readdirSync(dir)) {
2276
+ const mtime = fs.statSync(fsPath__default.join(dir, file)).mtime.getTime();
2277
+ if (mtime !== mtimeInDir[file]) {
2278
+ resolve("request-again");
2279
+ }
2280
+ }
2281
+ } else {
2282
+ // dir had files, but now is deleted
2283
+ if (Object.keys(mtimeInDir).length > 0) {
2284
+ resolve("request-again");
2285
+ }
2286
+ }
2287
+ if (Date.now() - start > interval) {
2288
+ console.warn("Val: polling interval of patches exceeded");
2289
+ }
2290
+ if (stopPolling) {
2291
+ return;
2292
+ }
2293
+ setHandle(setTimeout(() => go(resolve), interval));
2294
+ };
2295
+ setHandle(setTimeout(() => go(resolve), interval));
2296
+ });
2297
+ };
2298
+ const didFilesChangeUsingPolling = (files, interval, setHandle) => {
2299
+ const mtimes = {};
2300
+ for (const file of files) {
2301
+ if (fs.existsSync(file)) {
2302
+ mtimes[file] = fs.statSync(file).mtime.getTime();
2303
+ } else {
2304
+ mtimes[file] = -1;
2305
+ }
2306
+ }
2307
+ return new Promise(resolve => {
2308
+ const go = resolve => {
2309
+ const start = Date.now();
2310
+ for (const file of files) {
2311
+ const mtime = fs.existsSync(file) ? fs.statSync(file).mtime.getTime() : -1;
2312
+ if (mtime !== mtimes[file]) {
2313
+ resolve("request-again");
2314
+ }
2315
+ }
2316
+ if (Date.now() - start > interval) {
2317
+ console.warn("Val: polling interval of files exceeded");
2318
+ }
2319
+ setHandle(setTimeout(() => go(resolve), interval));
2320
+ };
2321
+ if (stopPolling) {
2322
+ return;
2323
+ }
2324
+ setHandle(setTimeout(() => go(resolve), interval));
2325
+ });
2326
+ };
2327
+ const statFilePollingInterval = ((_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.statFilePollingInterval) || 250; // relatively low interval, but there would typically not be that many files (less than 1000 at the very least) - hopefully if we have customers with more files than that, we also have devs working on Val that easily can fix this :) Besides this is just the default
2328
+ const disableFilePolling = ((_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.disableFilePolling) || false;
2329
+ let patchesDirHandle;
2330
+ let valFilesIntervalHandle;
2331
+ const type = await Promise.race([
2332
+ // we poll the patches directory for changes since fs.watch does not work reliably on all system (in particular on WSL) and just checking the patches dir is relatively cheap
2333
+ disableFilePolling ? new Promise(() => {}) : didDirectoryChangeUsingPolling(this.getPatchesDir(), statFilePollingInterval, handle => {
2334
+ patchesDirHandle = handle;
2335
+ }),
2336
+ // we poll the files that Val depends on for changes
2337
+ disableFilePolling ? new Promise(() => {}) : didFilesChangeUsingPolling([fsPath__default.join(this.rootDir, "val.config.ts"), fsPath__default.join(this.rootDir, "val.modules.ts"), fsPath__default.join(this.rootDir, "val.config.js"), fsPath__default.join(this.rootDir, "val.modules.js"), ...moduleFilePaths.map(p => fsPath__default.join(this.rootDir, p))], statFilePollingInterval, handle => {
2338
+ valFilesIntervalHandle = handle;
2339
+ }), new Promise(resolve => {
2340
+ fsWatcher = fs.watch(this.rootDir, {
2341
+ recursive: true
2342
+ }, (eventType, filename) => {
2343
+ if (!filename) {
2344
+ return;
2345
+ }
2346
+ const isChange = filename.startsWith(this.getPatchesDir().slice(this.rootDir.length + 1)) || filename.endsWith(".val.ts") || filename.endsWith(".val.js") || filename.endsWith("val.config.ts") || filename.endsWith("val.config.js") || filename.endsWith("val.modules.ts") || filename.endsWith("val.modules.js");
2347
+ if (isChange) {
2348
+ // a file that Val depends on just changed or a patch was created, break connection and request stat again to get the new values
2349
+ resolve("request-again");
2350
+ }
2351
+ });
2352
+ }), new Promise(resolve => {
2353
+ var _this$options3;
2354
+ return setTimeout(() => resolve("no-change"), ((_this$options3 = this.options) === null || _this$options3 === void 0 ? void 0 : _this$options3.statPollingInterval) || 20000);
2355
+ })]).finally(() => {
2356
+ if (fsWatcher) {
2357
+ fsWatcher.close();
2358
+ }
2359
+ stopPolling = true;
2360
+ clearInterval(patchesDirHandle);
2361
+ clearInterval(valFilesIntervalHandle);
2362
+ });
2363
+ return {
2364
+ type,
2365
+ baseSha: currentBaseSha,
2366
+ schemaSha: currentSchemaSha,
2367
+ patches
2368
+ };
2369
+ } catch (err) {
2370
+ if (err instanceof Error) {
2371
+ return {
2372
+ type: "error",
2373
+ error: {
2374
+ message: err.message
2375
+ }
2376
+ };
2377
+ }
2378
+ return {
2379
+ type: "error",
2380
+ error: {
2381
+ message: "Unknown error (getStat)"
2382
+ }
2383
+ };
2384
+ }
2385
+ }
2226
2386
  async readPatches(includes) {
2227
2387
  const patchesCacheDir = this.getPatchesDir();
2228
2388
  let patchJsonFiles = [];
@@ -2231,8 +2391,8 @@ class ValOpsFS extends ValOps {
2231
2391
  }
2232
2392
  const patches = {};
2233
2393
  const errors = {};
2234
- const sortedPatchIds = patchJsonFiles.map(file => parseInt(fsPath__default.basename(fsPath__default.dirname(file)), 10)).sort();
2235
- for (const patchIdNum of sortedPatchIds) {
2394
+ const parsedPatchIds = patchJsonFiles.map(file => parseInt(fsPath__default.basename(fsPath__default.dirname(file)), 10)).sort();
2395
+ for (const patchIdNum of parsedPatchIds) {
2236
2396
  if (Number.isNaN(patchIdNum)) {
2237
2397
  throw new Error("Could not parse patch id from file name. Files found: " + patchJsonFiles.join(", "));
2238
2398
  }
@@ -2273,6 +2433,12 @@ class ValOpsFS extends ValOps {
2273
2433
  errors: allErrors,
2274
2434
  patches: allPatches
2275
2435
  } = await this.readPatches(filters.patchIds);
2436
+ if (allErrors && Object.keys(allErrors).length > 0) {
2437
+ for (const [patchId, error] of Object.entries(allErrors)) {
2438
+ console.error("Error reading patch", patchId, error);
2439
+ errors[patchId] = error;
2440
+ }
2441
+ }
2276
2442
  for (const [patchIdS, patch] of Object.entries(allPatches)) {
2277
2443
  const patchId = patchIdS;
2278
2444
  if (filters.authors && !(patch.authorId === null || filters.authors.includes(patch.authorId))) {
@@ -2288,10 +2454,6 @@ class ValOpsFS extends ValOps {
2288
2454
  authorId: patch.authorId,
2289
2455
  appliedAt: patch.appliedAt
2290
2456
  };
2291
- const error = allErrors && allErrors[patchId];
2292
- if (error) {
2293
- errors[patchId] = error;
2294
- }
2295
2457
  }
2296
2458
  if (errors && Object.keys(errors).length > 0) {
2297
2459
  return {
@@ -2635,11 +2797,11 @@ class ValOpsFS extends ValOps {
2635
2797
  getPatchDir(patchId) {
2636
2798
  return fsPath__default.join(this.getPatchesDir(), patchId);
2637
2799
  }
2638
- getBinaryFilePath(filename, patchId) {
2639
- return fsPath__default.join(this.getPatchDir(patchId), "files", filename, fsPath__default.basename(filename));
2800
+ getBinaryFilePath(filePath, patchId) {
2801
+ return fsPath__default.join(this.getPatchDir(patchId), "files", filePath, fsPath__default.basename(filePath));
2640
2802
  }
2641
- getBinaryFileMetadataPath(filename, patchId) {
2642
- return fsPath__default.join(this.getPatchDir(patchId), "files", filename, "metadata.json");
2803
+ getBinaryFileMetadataPath(filePath, patchId) {
2804
+ return fsPath__default.join(this.getPatchDir(patchId), "files", filePath, "metadata.json");
2643
2805
  }
2644
2806
  getPatchFilePath(patchId) {
2645
2807
  return fsPath__default.join(this.getPatchDir(patchId), "patch.json");
@@ -2712,12 +2874,10 @@ const BaseSha = z.string().refine(s => !!s); // TODO: validate
2712
2874
  const AuthorId = z.string().refine(s => !!s); // TODO: validate
2713
2875
  const ModuleFilePath = z.string().refine(s => !!s); // TODO: validate
2714
2876
  const Metadata = z.union([z.object({
2715
- sha256: z.string(),
2716
2877
  mimeType: z.string(),
2717
2878
  width: z.number(),
2718
2879
  height: z.number()
2719
2880
  }), z.object({
2720
- sha256: z.string(),
2721
2881
  mimeType: z.string()
2722
2882
  })]);
2723
2883
  const MetadataRes = z.object({
@@ -2738,7 +2898,7 @@ const BasePatchResponse = z.object({
2738
2898
  });
2739
2899
  const GetPatches = z.object({
2740
2900
  patches: z.array(z.intersection(z.object({
2741
- patch: Patch
2901
+ patch: Patch.optional()
2742
2902
  }), BasePatchResponse)),
2743
2903
  errors: z.array(z.object({
2744
2904
  patchId: PatchId.optional(),
@@ -2789,7 +2949,9 @@ const CommitResponse = z.object({
2789
2949
  branch: z.string()
2790
2950
  });
2791
2951
  class ValOpsHttp extends ValOps {
2792
- constructor(hostUrl, project, commitSha, branch, apiKey, valModules, options) {
2952
+ constructor(hostUrl, project, commitSha,
2953
+ // TODO: CommitSha
2954
+ branch, apiKey, valModules, options) {
2793
2955
  super(valModules, options);
2794
2956
  this.hostUrl = hostUrl;
2795
2957
  this.project = project;
@@ -2803,7 +2965,150 @@ class ValOpsHttp extends ValOps {
2803
2965
  async onInit() {
2804
2966
  // TODO: unused for now. Implement or remove
2805
2967
  }
2968
+ async getStat(params) {
2969
+ if (!(params !== null && params !== void 0 && params.profileId)) {
2970
+ return {
2971
+ type: "error",
2972
+ error: {
2973
+ message: "No profileId provided"
2974
+ }
2975
+ };
2976
+ }
2977
+ const currentBaseSha = await this.getBaseSha();
2978
+ const currentSchemaSha = await this.getSchemaSha();
2979
+ const patchData = await this.fetchPatches({
2980
+ omitPatch: true,
2981
+ authors: undefined,
2982
+ patchIds: undefined,
2983
+ moduleFilePaths: undefined
2984
+ });
2985
+ const patches = [];
2986
+ // TODO: use proper patch sequences when available:
2987
+ for (const [patchId] of Object.entries(patchData.patches).sort(([, a], [, b]) => {
2988
+ return a.createdAt.localeCompare(b.createdAt, undefined);
2989
+ })) {
2990
+ patches.push(patchId);
2991
+ }
2992
+ const webSocketNonceRes = await this.getWebSocketNonce(params.profileId);
2993
+ if (webSocketNonceRes.status === "error") {
2994
+ return {
2995
+ type: "error",
2996
+ error: webSocketNonceRes.error
2997
+ };
2998
+ }
2999
+ const {
3000
+ nonce,
3001
+ url
3002
+ } = webSocketNonceRes.data;
3003
+ return {
3004
+ type: "use-websocket",
3005
+ url,
3006
+ nonce,
3007
+ baseSha: currentBaseSha,
3008
+ schemaSha: currentSchemaSha,
3009
+ patches,
3010
+ commitSha: this.commitSha
3011
+ };
3012
+ }
3013
+ async getWebSocketNonce(profileId) {
3014
+ return fetch(`${this.hostUrl}/v1/${this.project}/websocket/nonces`, {
3015
+ method: "POST",
3016
+ body: JSON.stringify({
3017
+ branch: this.branch,
3018
+ profileId
3019
+ }),
3020
+ headers: {
3021
+ ...this.authHeaders,
3022
+ "Content-Type": "application/json"
3023
+ }
3024
+ }).then(async res => {
3025
+ if (res.ok) {
3026
+ const json = await res.json();
3027
+ if (typeof json.nonce !== "string" || typeof json.url !== "string") {
3028
+ return {
3029
+ status: "error",
3030
+ error: {
3031
+ message: "Invalid nonce response: " + JSON.stringify(json)
3032
+ }
3033
+ };
3034
+ }
3035
+ if (!json.url.startsWith("ws://") && !json.url.startsWith("wss://")) {
3036
+ return {
3037
+ status: "error",
3038
+ error: {
3039
+ message: "Invalid websocket url: " + json.url
3040
+ }
3041
+ };
3042
+ }
3043
+ return {
3044
+ status: "success",
3045
+ data: {
3046
+ nonce: json.nonce,
3047
+ url: json.url
3048
+ }
3049
+ };
3050
+ }
3051
+ return {
3052
+ status: "error",
3053
+ error: {
3054
+ message: "Could not get nonce. HTTP error: " + res.status + " " + res.statusText
3055
+ }
3056
+ };
3057
+ }).catch(e => {
3058
+ console.error("Could not get nonce (connection error?):", e instanceof Error ? e.message : e.toString());
3059
+ return {
3060
+ status: "error",
3061
+ error: {
3062
+ message: "Could not get nonce. Error: " + (e instanceof Error ? e.message : e.toString())
3063
+ }
3064
+ };
3065
+ });
3066
+ }
2806
3067
  async fetchPatches(filters) {
3068
+ // Split patchIds into chunks to avoid too long query strings
3069
+ // NOTE: fetching patches results are cached, so this should reduce the pressure on the server
3070
+ const chunkSize = 100;
3071
+ const patchIds = filters.patchIds || [];
3072
+ const patchIdChunks = [];
3073
+ for (let i = 0; i < patchIds.length; i += chunkSize) {
3074
+ patchIdChunks.push(patchIds.slice(i, i + chunkSize));
3075
+ }
3076
+ let allPatches = {};
3077
+ let allErrors = {};
3078
+ if (patchIds === undefined || patchIds.length === 0) {
3079
+ return this.fetchPatchesInternal({
3080
+ patchIds: patchIds,
3081
+ authors: filters.authors,
3082
+ moduleFilePaths: filters.moduleFilePaths,
3083
+ omitPatch: filters.omitPatch
3084
+ });
3085
+ }
3086
+ for (const res of await Promise.all(patchIdChunks.map(patchIdChunk => this.fetchPatchesInternal({
3087
+ patchIds: patchIdChunk,
3088
+ authors: filters.authors,
3089
+ moduleFilePaths: filters.moduleFilePaths,
3090
+ omitPatch: filters.omitPatch
3091
+ })))) {
3092
+ if ("error" in res) {
3093
+ return res;
3094
+ }
3095
+ allPatches = {
3096
+ ...allPatches,
3097
+ ...res.patches
3098
+ };
3099
+ if (res.errors) {
3100
+ allErrors = {
3101
+ ...allErrors,
3102
+ ...res.errors
3103
+ };
3104
+ }
3105
+ }
3106
+ return {
3107
+ patches: allPatches,
3108
+ errors: Object.keys(allErrors).length > 0 ? allErrors : undefined
3109
+ };
3110
+ }
3111
+ async fetchPatchesInternal(filters) {
2807
3112
  const params = [];
2808
3113
  params.push(["branch", this.branch]);
2809
3114
  if (filters.patchIds) {
@@ -3052,7 +3357,7 @@ class ValOpsHttp extends ValOps {
3052
3357
  if (!file) {
3053
3358
  return null;
3054
3359
  }
3055
- return Buffer.from(file.value, "base64");
3360
+ return bufferFromDataUrl(file.value) ?? null;
3056
3361
  }
3057
3362
  async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
3058
3363
  const params = new URLSearchParams();
@@ -3160,6 +3465,7 @@ class ValOpsHttp extends ValOps {
3160
3465
  }
3161
3466
  async commit(prepared, message, committer, newBranch) {
3162
3467
  try {
3468
+ var _res$headers$get;
3163
3469
  const existingBranch = this.branch;
3164
3470
  const res = await fetch(`${this.hostUrl}/v1/${this.project}/commit`, {
3165
3471
  method: "POST",
@@ -3195,6 +3501,22 @@ class ValOpsHttp extends ValOps {
3195
3501
  }
3196
3502
  };
3197
3503
  }
3504
+ if ((_res$headers$get = res.headers.get("Content-Type")) !== null && _res$headers$get !== void 0 && _res$headers$get.includes("application/json")) {
3505
+ const json = await res.json();
3506
+ if (json.isNotFastForward) {
3507
+ return {
3508
+ isNotFastForward: true,
3509
+ error: {
3510
+ message: "Could not commit. Not a fast-forward commit"
3511
+ }
3512
+ };
3513
+ }
3514
+ return {
3515
+ error: {
3516
+ message: json.message
3517
+ }
3518
+ };
3519
+ }
3198
3520
  return {
3199
3521
  error: {
3200
3522
  message: "Could not commit. HTTP error: " + res.status + " " + res.statusText
@@ -3211,251 +3533,61 @@ class ValOpsHttp extends ValOps {
3211
3533
  }
3212
3534
 
3213
3535
  /* eslint-disable @typescript-eslint/no-unused-vars */
3214
- class ValServer {
3215
- constructor(valModules, options, callbacks) {
3216
- this.valModules = valModules;
3217
- this.options = options;
3218
- this.callbacks = callbacks;
3219
- if (options.mode === "fs") {
3220
- this.serverOps = new ValOpsFS(options.cwd, valModules, {
3221
- formatter: options.formatter
3222
- });
3223
- } else if (options.mode === "http") {
3224
- this.serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
3225
- formatter: options.formatter,
3226
- root: options.root
3227
- });
3228
- } else {
3229
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3230
- throw new Error("Invalid mode: " + (options === null || options === void 0 ? void 0 : options.mode));
3231
- }
3536
+ const ValServer = (valModules, options, callbacks) => {
3537
+ let serverOps;
3538
+ if (options.mode === "fs") {
3539
+ serverOps = new ValOpsFS(options.cwd, valModules, {
3540
+ formatter: options.formatter,
3541
+ config: options.config
3542
+ });
3543
+ } else if (options.mode === "http") {
3544
+ serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
3545
+ formatter: options.formatter,
3546
+ root: options.root,
3547
+ config: options.config
3548
+ });
3549
+ } else {
3550
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3551
+ throw new Error("Invalid mode: " + (options === null || options === void 0 ? void 0 : options.mode));
3232
3552
  }
3233
-
3234
- //#region auth
3235
- async enable(query) {
3236
- const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
3237
- if (typeof redirectToRes !== "string") {
3238
- return redirectToRes;
3553
+ const getAuthorizeUrl = (publicValApiRe, token) => {
3554
+ if (!options.project) {
3555
+ throw new Error("Project is not set");
3239
3556
  }
3240
- await this.callbacks.onEnable(true);
3241
- return {
3242
- cookies: {
3243
- [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
3244
- },
3245
- status: 302,
3246
- redirectTo: redirectToRes
3247
- };
3248
- }
3249
- async disable(query) {
3250
- const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
3251
- if (typeof redirectToRes !== "string") {
3252
- return redirectToRes;
3557
+ if (!options.valBuildUrl) {
3558
+ throw new Error("Val build url is not set");
3253
3559
  }
3254
- await this.callbacks.onDisable(true);
3255
- return {
3256
- cookies: {
3257
- [VAL_ENABLE_COOKIE_NAME]: {
3258
- value: "false"
3259
- }
3260
- },
3261
- status: 302,
3262
- redirectTo: redirectToRes
3263
- };
3264
- }
3265
- async authorize(query) {
3266
- if (typeof query.redirect_to !== "string") {
3267
- return {
3268
- status: 400,
3269
- json: {
3270
- message: "Missing redirect_to query param"
3271
- }
3272
- };
3560
+ const url = new URL(`/auth/${options.project}/authorize`, options.valBuildUrl);
3561
+ url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRe}/callback`));
3562
+ url.searchParams.set("state", token);
3563
+ return url.toString();
3564
+ };
3565
+ const getAppErrorUrl = error => {
3566
+ if (!options.project) {
3567
+ throw new Error("Project is not set");
3273
3568
  }
3274
- const token = crypto.randomUUID();
3275
- const redirectUrl = new URL(query.redirect_to);
3276
- const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
3277
- await this.callbacks.onEnable(true);
3278
- return {
3279
- cookies: {
3280
- [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3281
- [VAL_STATE_COOKIE$1]: {
3282
- value: createStateCookie({
3283
- redirect_to: query.redirect_to,
3284
- token
3285
- }),
3286
- options: {
3287
- httpOnly: true,
3288
- sameSite: "lax",
3289
- expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
3290
- }
3291
- }
3292
- },
3293
- status: 302,
3294
- redirectTo: appAuthorizeUrl
3295
- };
3296
- }
3297
- async callback(query, cookies) {
3298
- if (!this.options.project) {
3299
- return {
3300
- status: 302,
3301
- cookies: {
3302
- [VAL_STATE_COOKIE$1]: {
3303
- value: null
3304
- }
3305
- },
3306
- redirectTo: this.getAppErrorUrl("Project is not set")
3307
- };
3308
- }
3309
- if (!this.options.valSecret) {
3310
- return {
3311
- status: 302,
3312
- cookies: {
3313
- [VAL_STATE_COOKIE$1]: {
3314
- value: null
3315
- }
3316
- },
3317
- redirectTo: this.getAppErrorUrl("Secret is not set")
3318
- };
3319
- }
3320
- const {
3321
- success: callbackReqSuccess,
3322
- error: callbackReqError
3323
- } = verifyCallbackReq(cookies[VAL_STATE_COOKIE$1], query);
3324
- if (callbackReqError !== null) {
3325
- return {
3326
- status: 302,
3327
- cookies: {
3328
- [VAL_STATE_COOKIE$1]: {
3329
- value: null
3330
- }
3331
- },
3332
- redirectTo: this.getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`)
3333
- };
3334
- }
3335
- const data = await this.consumeCode(callbackReqSuccess.code);
3336
- if (data === null) {
3337
- return {
3338
- status: 302,
3339
- cookies: {
3340
- [VAL_STATE_COOKIE$1]: {
3341
- value: null
3342
- }
3343
- },
3344
- redirectTo: this.getAppErrorUrl("Failed to exchange code for user")
3345
- };
3346
- }
3347
- const exp = getExpire();
3348
- const valSecret = this.options.valSecret;
3349
- if (!valSecret) {
3350
- return {
3351
- status: 302,
3352
- cookies: {
3353
- [VAL_STATE_COOKIE$1]: {
3354
- value: null
3355
- }
3356
- },
3357
- redirectTo: this.getAppErrorUrl("Setup is not correct: secret is missing")
3358
- };
3359
- }
3360
- const cookie = encodeJwt({
3361
- ...data,
3362
- exp // this is the client side exp
3363
- }, valSecret);
3364
- return {
3365
- status: 302,
3366
- cookies: {
3367
- [VAL_STATE_COOKIE$1]: {
3368
- value: null
3369
- },
3370
- [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3371
- [VAL_SESSION_COOKIE$1]: {
3372
- value: cookie,
3373
- options: {
3374
- httpOnly: true,
3375
- sameSite: "strict",
3376
- path: "/",
3377
- secure: true,
3378
- expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
3379
- }
3380
- }
3381
- },
3382
- redirectTo: callbackReqSuccess.redirect_uri || "/"
3383
- };
3384
- }
3385
- async session(cookies) {
3386
- if (this.serverOps instanceof ValOpsFS) {
3387
- return {
3388
- status: 200,
3389
- json: {
3390
- mode: "local",
3391
- enabled: await this.callbacks.isEnabled()
3392
- }
3393
- };
3394
- }
3395
- if (!this.options.project) {
3396
- return {
3397
- status: 500,
3398
- json: {
3399
- message: "Project is not set"
3400
- }
3401
- };
3402
- }
3403
- if (!this.options.valSecret) {
3404
- return {
3405
- status: 500,
3406
- json: {
3407
- message: "Secret is not set"
3408
- }
3409
- };
3569
+ if (!options.valBuildUrl) {
3570
+ throw new Error("Val build url is not set");
3410
3571
  }
3411
- return withAuth(this.options.valSecret, cookies, "session", async data => {
3412
- if (!this.options.valBuildUrl) {
3413
- return {
3414
- status: 500,
3415
- json: {
3416
- message: "Val is not correctly setup. Build url is missing"
3417
- }
3418
- };
3419
- }
3420
- const url = new URL(`/api/val/${this.options.project}/auth/session`, this.options.valBuildUrl);
3421
- const fetchRes = await fetch(url, {
3422
- headers: getAuthHeaders(data.token, "application/json")
3423
- });
3424
- if (fetchRes.status === 200) {
3425
- return {
3426
- status: fetchRes.status,
3427
- json: {
3428
- mode: "proxy",
3429
- enabled: await this.callbacks.isEnabled(),
3430
- ...(await fetchRes.json())
3431
- }
3432
- };
3433
- } else {
3434
- return {
3435
- status: fetchRes.status,
3436
- json: {
3437
- message: "Failed to authorize",
3438
- ...(await fetchRes.json())
3439
- }
3440
- };
3441
- }
3442
- });
3443
- }
3444
- async consumeCode(code) {
3445
- if (!this.options.project) {
3572
+ const url = new URL(`/auth/${options.project}/authorize`, options.valBuildUrl);
3573
+ url.searchParams.set("error", encodeURIComponent(error));
3574
+ return url.toString();
3575
+ };
3576
+ const consumeCode = async code => {
3577
+ if (!options.project) {
3446
3578
  throw new Error("Project is not set");
3447
3579
  }
3448
- if (!this.options.valBuildUrl) {
3580
+ if (!options.valBuildUrl) {
3449
3581
  throw new Error("Val build url is not set");
3450
3582
  }
3451
- const url = new URL(`/api/val/${this.options.project}/auth/token`, this.options.valBuildUrl);
3583
+ const url = new URL(`/api/val/${options.project}/auth/token`, options.valBuildUrl);
3452
3584
  url.searchParams.set("code", encodeURIComponent(code));
3453
- if (!this.options.apiKey) {
3585
+ if (!options.apiKey) {
3454
3586
  return null;
3455
3587
  }
3456
3588
  return fetch(url, {
3457
3589
  method: "POST",
3458
- headers: getAuthHeaders(this.options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
3590
+ headers: getAuthHeaders(options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
3459
3591
  }).then(async res => {
3460
3592
  if (res.status === 200) {
3461
3593
  const token = await res.text();
@@ -3475,34 +3607,11 @@ class ValServer {
3475
3607
  console.debug("Failed to get user from code: ", err);
3476
3608
  return null;
3477
3609
  });
3478
- }
3479
- getAuthorizeUrl(publicValApiRe, token) {
3480
- if (!this.options.project) {
3481
- throw new Error("Project is not set");
3482
- }
3483
- if (!this.options.valBuildUrl) {
3484
- throw new Error("Val build url is not set");
3485
- }
3486
- const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
3487
- url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRe}/callback`));
3488
- url.searchParams.set("state", token);
3489
- return url.toString();
3490
- }
3491
- getAppErrorUrl(error) {
3492
- if (!this.options.project) {
3493
- throw new Error("Project is not set");
3494
- }
3495
- if (!this.options.valBuildUrl) {
3496
- throw new Error("Val build url is not set");
3497
- }
3498
- const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
3499
- url.searchParams.set("error", encodeURIComponent(error));
3500
- return url.toString();
3501
- }
3502
- getAuth(cookies) {
3503
- const cookie = cookies[VAL_SESSION_COOKIE$1];
3504
- if (!this.options.valSecret) {
3505
- if (this.serverOps instanceof ValOpsFS) {
3610
+ };
3611
+ const getAuth = cookies => {
3612
+ const cookie = cookies[VAL_SESSION_COOKIE];
3613
+ if (!options.valSecret) {
3614
+ if (serverOps instanceof ValOpsFS) {
3506
3615
  return {
3507
3616
  error: null,
3508
3617
  id: null
@@ -3514,9 +3623,9 @@ class ValServer {
3514
3623
  }
3515
3624
  }
3516
3625
  if (typeof cookie === "string") {
3517
- const decodedToken = decodeJwt(cookie, this.options.valSecret);
3626
+ const decodedToken = decodeJwt(cookie, options.valSecret);
3518
3627
  if (!decodedToken) {
3519
- if (this.serverOps instanceof ValOpsFS) {
3628
+ if (serverOps instanceof ValOpsFS) {
3520
3629
  return {
3521
3630
  error: null,
3522
3631
  id: null
@@ -3528,7 +3637,7 @@ class ValServer {
3528
3637
  }
3529
3638
  const verification = IntegratedServerJwtPayload.safeParse(decodedToken);
3530
3639
  if (!verification.success) {
3531
- if (this.serverOps instanceof ValOpsFS) {
3640
+ if (serverOps instanceof ValOpsFS) {
3532
3641
  return {
3533
3642
  error: null,
3534
3643
  id: null
@@ -3542,7 +3651,7 @@ class ValServer {
3542
3651
  id: verification.data.sub
3543
3652
  };
3544
3653
  } else {
3545
- if (this.serverOps instanceof ValOpsFS) {
3654
+ if (serverOps instanceof ValOpsFS) {
3546
3655
  return {
3547
3656
  error: null,
3548
3657
  id: null
@@ -3552,417 +3661,880 @@ class ValServer {
3552
3661
  error: "Login required: cookie not found"
3553
3662
  };
3554
3663
  }
3555
- }
3556
- async logout() {
3664
+ };
3665
+ const authorize = async redirectTo => {
3666
+ const token = crypto.randomUUID();
3667
+ const redirectUrl = new URL(redirectTo);
3668
+ const appAuthorizeUrl = getAuthorizeUrl(`${redirectUrl.origin}/${options.route}`, token);
3669
+ await callbacks.onEnable(true);
3557
3670
  return {
3558
- status: 200,
3559
3671
  cookies: {
3560
- [VAL_SESSION_COOKIE$1]: {
3561
- value: null
3562
- },
3563
- [VAL_STATE_COOKIE$1]: {
3564
- value: null
3672
+ [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3673
+ [VAL_STATE_COOKIE]: {
3674
+ value: createStateCookie({
3675
+ redirect_to: redirectTo,
3676
+ token
3677
+ }),
3678
+ options: {
3679
+ httpOnly: true,
3680
+ sameSite: "lax",
3681
+ expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
3682
+ }
3565
3683
  }
3566
- }
3684
+ },
3685
+ status: 302,
3686
+ redirectTo: appAuthorizeUrl
3567
3687
  };
3568
- }
3569
-
3570
- //#region patches
3571
- async getPatches(query, cookies) {
3572
- const auth = this.getAuth(cookies);
3573
- if (auth.error) {
3574
- return {
3575
- status: 401,
3576
- json: {
3577
- message: auth.error
3688
+ };
3689
+ return {
3690
+ "/draft/enable": {
3691
+ GET: async req => {
3692
+ const cookies = req.cookies;
3693
+ const auth = getAuth(cookies);
3694
+ if (auth.error) {
3695
+ return {
3696
+ status: 401,
3697
+ json: {
3698
+ message: auth.error
3699
+ }
3700
+ };
3578
3701
  }
3579
- };
3580
- }
3581
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3582
- return {
3583
- status: 401,
3584
- json: {
3585
- message: "Unauthorized"
3702
+ const query = req.query;
3703
+ const redirectToRes = getRedirectUrl(query, options.valEnableRedirectUrl);
3704
+ if (typeof redirectToRes !== "string") {
3705
+ return redirectToRes;
3586
3706
  }
3587
- };
3588
- }
3589
- const authors = query.authors;
3590
- const patches = await this.serverOps.fetchPatches({
3591
- authors,
3592
- patchIds: query.patchIds,
3593
- omitPatch: query.omitPatch === "true"
3594
- });
3595
- if (patches.errors && Object.keys(patches.errors).length > 0) {
3596
- console.error("Val: Failed to get patches", patches.errors);
3597
- return {
3598
- status: 500,
3599
- json: {
3600
- message: "Failed to get patches",
3601
- details: patches.errors
3707
+ await callbacks.onEnable(true);
3708
+ return {
3709
+ status: 302,
3710
+ redirectTo: redirectToRes
3711
+ };
3712
+ }
3713
+ },
3714
+ "/draft/disable": {
3715
+ GET: async req => {
3716
+ const cookies = req.cookies;
3717
+ const auth = getAuth(cookies);
3718
+ if (auth.error) {
3719
+ return {
3720
+ status: 401,
3721
+ json: {
3722
+ message: auth.error
3723
+ }
3724
+ };
3602
3725
  }
3603
- };
3604
- }
3605
- return {
3606
- status: 200,
3607
- json: patches
3608
- };
3609
- }
3610
- async deletePatches(query, cookies) {
3611
- const auth = this.getAuth(cookies);
3612
- if (auth.error) {
3613
- return {
3614
- status: 401,
3615
- json: {
3616
- message: auth.error
3726
+ const query = req.query;
3727
+ const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
3728
+ if (typeof redirectToRes !== "string") {
3729
+ return redirectToRes;
3617
3730
  }
3618
- };
3619
- }
3620
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3621
- return {
3622
- status: 401,
3623
- json: {
3624
- message: "Unauthorized"
3731
+ await callbacks.onDisable(true);
3732
+ return {
3733
+ status: 302,
3734
+ redirectTo: redirectToRes
3735
+ };
3736
+ }
3737
+ },
3738
+ "/draft/stat": {
3739
+ GET: async req => {
3740
+ const cookies = req.cookies;
3741
+ const auth = getAuth(cookies);
3742
+ if (auth.error) {
3743
+ return {
3744
+ status: 401,
3745
+ json: {
3746
+ message: auth.error
3747
+ }
3748
+ };
3625
3749
  }
3626
- };
3627
- }
3628
- const ids = query.id;
3629
- const deleteRes = await this.serverOps.deletePatches(ids);
3630
- if (deleteRes.errors && Object.keys(deleteRes.errors).length > 0) {
3631
- console.error("Val: Failed to delete patches", deleteRes.errors);
3632
- return {
3633
- status: 500,
3634
- json: {
3635
- message: "Failed to delete patches",
3636
- details: deleteRes.errors
3750
+ return {
3751
+ status: 200,
3752
+ json: {
3753
+ draftMode: await callbacks.isEnabled()
3754
+ }
3755
+ };
3756
+ }
3757
+ },
3758
+ "/enable": {
3759
+ GET: async req => {
3760
+ const cookies = req.cookies;
3761
+ const auth = getAuth(cookies);
3762
+ const query = req.query;
3763
+ const redirectToRes = getRedirectUrl(query, options.valEnableRedirectUrl);
3764
+ if (auth.error) {
3765
+ if (typeof redirectToRes === "string") {
3766
+ return authorize(redirectToRes);
3767
+ }
3768
+ return {
3769
+ status: 401,
3770
+ json: {
3771
+ message: auth.error
3772
+ }
3773
+ };
3637
3774
  }
3638
- };
3639
- }
3640
- return {
3641
- status: 200,
3642
- json: ids
3643
- };
3644
- }
3645
-
3646
- //#region tree ops
3647
- async getSchema(cookies) {
3648
- const auth = this.getAuth(cookies);
3649
- if (auth.error) {
3650
- return {
3651
- status: 401,
3652
- json: {
3653
- message: auth.error
3775
+ if (typeof redirectToRes !== "string") {
3776
+ return redirectToRes;
3654
3777
  }
3655
- };
3656
- }
3657
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3658
- return {
3659
- status: 401,
3660
- json: {
3661
- message: "Unauthorized"
3778
+ await callbacks.onEnable(true);
3779
+ return {
3780
+ cookies: {
3781
+ [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
3782
+ },
3783
+ status: 302,
3784
+ redirectTo: redirectToRes
3785
+ };
3786
+ }
3787
+ },
3788
+ "/disable": {
3789
+ GET: async req => {
3790
+ const cookies = req.cookies;
3791
+ const auth = getAuth(cookies);
3792
+ if (auth.error) {
3793
+ return {
3794
+ status: 401,
3795
+ json: {
3796
+ message: auth.error
3797
+ }
3798
+ };
3662
3799
  }
3663
- };
3664
- }
3665
- const moduleErrors = await this.serverOps.getModuleErrors();
3666
- if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
3667
- console.error("Val: Module errors", moduleErrors);
3668
- return {
3669
- status: 500,
3670
- json: {
3671
- message: "Val is not correctly setup. Check the val.modules file",
3672
- details: moduleErrors
3800
+ const query = req.query;
3801
+ const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
3802
+ if (typeof redirectToRes !== "string") {
3803
+ return redirectToRes;
3673
3804
  }
3674
- };
3675
- }
3676
- const schemaSha = await this.serverOps.getSchemaSha();
3677
- const schemas = await this.serverOps.getSchemas();
3678
- const serializedSchemas = {};
3679
- for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
3680
- const moduleFilePath = moduleFilePathS;
3681
- serializedSchemas[moduleFilePath] = schema.serialize();
3682
- }
3683
- return {
3684
- status: 200,
3685
- json: {
3686
- schemaSha,
3687
- schemas: serializedSchemas
3805
+ await callbacks.onDisable(true);
3806
+ return {
3807
+ cookies: {
3808
+ [VAL_ENABLE_COOKIE_NAME]: {
3809
+ value: "false"
3810
+ }
3811
+ },
3812
+ status: 302,
3813
+ redirectTo: redirectToRes
3814
+ };
3688
3815
  }
3689
- };
3690
- }
3691
- async putTree(body, treePath, query, cookies) {
3692
- var _bodyRes$data, _bodyRes$data2, _bodyRes$data3;
3693
- const auth = this.getAuth(cookies);
3694
- if (auth.error) {
3695
- return {
3696
- status: 401,
3697
- json: {
3698
- message: auth.error
3816
+ },
3817
+ //#region auth
3818
+ "/authorize": {
3819
+ GET: async req => {
3820
+ const query = req.query;
3821
+ if (typeof query.redirect_to !== "string") {
3822
+ return {
3823
+ status: 400,
3824
+ json: {
3825
+ message: "Missing redirect_to query param"
3826
+ }
3827
+ };
3699
3828
  }
3700
- };
3701
- }
3702
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3703
- return {
3704
- status: 401,
3705
- json: {
3706
- message: "Unauthorized"
3829
+ const redirectTo = query.redirect_to;
3830
+ return authorize(redirectTo);
3831
+ }
3832
+ },
3833
+ "/callback": {
3834
+ GET: async req => {
3835
+ const cookies = req.cookies;
3836
+ const query = req.query;
3837
+ if (!options.project) {
3838
+ return {
3839
+ status: 302,
3840
+ cookies: {
3841
+ [VAL_STATE_COOKIE]: {
3842
+ value: null
3843
+ }
3844
+ },
3845
+ redirectTo: getAppErrorUrl("Project is not set")
3846
+ };
3707
3847
  }
3708
- };
3709
- }
3710
- // TODO: move
3711
- const PutTreeBody = z.object({
3712
- patchIds: z.array(z.string().refine(id => true // TODO:
3713
- )).optional(),
3714
- addPatch: z.object({
3715
- path: z.string().refine(path => true // TODO:
3716
- ),
3717
- patch: Patch
3718
- }).optional()
3719
- }).optional();
3720
- const moduleErrors = await this.serverOps.getModuleErrors();
3721
- if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
3722
- console.error("Val: Module errors", moduleErrors);
3723
- return {
3724
- status: 500,
3725
- json: {
3726
- message: "Val is not correctly setup. Check the val.modules file",
3727
- details: moduleErrors
3848
+ if (!options.valSecret) {
3849
+ return {
3850
+ status: 302,
3851
+ cookies: {
3852
+ [VAL_STATE_COOKIE]: {
3853
+ value: null
3854
+ }
3855
+ },
3856
+ redirectTo: getAppErrorUrl("Secret is not set")
3857
+ };
3728
3858
  }
3729
- };
3730
- }
3731
- const bodyRes = PutTreeBody.safeParse(body);
3732
- if (!bodyRes.success) {
3733
- return {
3734
- status: 400,
3735
- json: {
3736
- message: "Invalid body: " + fromError(bodyRes.error).toString(),
3737
- details: bodyRes.error.errors
3859
+ const {
3860
+ success: callbackReqSuccess,
3861
+ error: callbackReqError
3862
+ } = verifyCallbackReq(cookies[VAL_STATE_COOKIE], query);
3863
+ if (callbackReqError !== null) {
3864
+ return {
3865
+ status: 302,
3866
+ cookies: {
3867
+ [VAL_STATE_COOKIE]: {
3868
+ value: null
3869
+ }
3870
+ },
3871
+ redirectTo: getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`)
3872
+ };
3873
+ }
3874
+ const data = await consumeCode(callbackReqSuccess.code);
3875
+ if (data === null) {
3876
+ return {
3877
+ status: 302,
3878
+ cookies: {
3879
+ [VAL_STATE_COOKIE]: {
3880
+ value: null
3881
+ }
3882
+ },
3883
+ redirectTo: getAppErrorUrl("Failed to exchange code for user")
3884
+ };
3885
+ }
3886
+ const exp = getExpire();
3887
+ const valSecret = options.valSecret;
3888
+ if (!valSecret) {
3889
+ return {
3890
+ status: 302,
3891
+ cookies: {
3892
+ [VAL_STATE_COOKIE]: {
3893
+ value: null
3894
+ }
3895
+ },
3896
+ redirectTo: getAppErrorUrl("Setup is not correct: secret is missing")
3897
+ };
3898
+ }
3899
+ const cookie = encodeJwt({
3900
+ ...data,
3901
+ exp // this is the client side exp
3902
+ }, valSecret);
3903
+ return {
3904
+ status: 302,
3905
+ cookies: {
3906
+ [VAL_STATE_COOKIE]: {
3907
+ value: null
3908
+ },
3909
+ [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3910
+ [VAL_SESSION_COOKIE]: {
3911
+ value: cookie,
3912
+ options: {
3913
+ httpOnly: true,
3914
+ sameSite: "strict",
3915
+ path: "/",
3916
+ secure: true,
3917
+ expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
3918
+ }
3919
+ }
3920
+ },
3921
+ redirectTo: callbackReqSuccess.redirect_uri || "/"
3922
+ };
3923
+ }
3924
+ },
3925
+ "/session": {
3926
+ GET: async req => {
3927
+ const cookies = req.cookies;
3928
+ if (serverOps instanceof ValOpsFS) {
3929
+ return {
3930
+ status: 200,
3931
+ json: {
3932
+ mode: "local",
3933
+ enabled: await callbacks.isEnabled()
3934
+ }
3935
+ };
3936
+ }
3937
+ if (!options.project) {
3938
+ return {
3939
+ status: 500,
3940
+ json: {
3941
+ message: "Project is not set"
3942
+ }
3943
+ };
3944
+ }
3945
+ if (!options.valSecret) {
3946
+ return {
3947
+ status: 500,
3948
+ json: {
3949
+ message: "Secret is not set"
3950
+ }
3951
+ };
3952
+ }
3953
+ return withAuth(options.valSecret, cookies, "session", async data => {
3954
+ if (!options.valBuildUrl) {
3955
+ return {
3956
+ status: 500,
3957
+ json: {
3958
+ message: "Val is not correctly setup. Build url is missing"
3959
+ }
3960
+ };
3961
+ }
3962
+ const url = new URL(`/api/val/${options.project}/auth/session`, options.valBuildUrl);
3963
+ const fetchRes = await fetch(url, {
3964
+ headers: getAuthHeaders(data.token, "application/json")
3965
+ });
3966
+ if (fetchRes.status === 200) {
3967
+ return {
3968
+ status: fetchRes.status,
3969
+ json: {
3970
+ mode: "proxy",
3971
+ enabled: await callbacks.isEnabled(),
3972
+ ...(await fetchRes.json())
3973
+ }
3974
+ };
3975
+ } else {
3976
+ return {
3977
+ status: fetchRes.status,
3978
+ json: {
3979
+ message: "Failed to authorize",
3980
+ ...(await fetchRes.json())
3981
+ }
3982
+ };
3983
+ }
3984
+ });
3985
+ }
3986
+ },
3987
+ "/logout": {
3988
+ GET: async () => {
3989
+ return {
3990
+ status: 200,
3991
+ cookies: {
3992
+ [VAL_SESSION_COOKIE]: {
3993
+ value: null
3994
+ },
3995
+ [VAL_STATE_COOKIE]: {
3996
+ value: null
3997
+ }
3998
+ }
3999
+ };
4000
+ }
4001
+ },
4002
+ //#region stat
4003
+ "/stat": {
4004
+ POST: async req => {
4005
+ const cookies = req.cookies;
4006
+ const auth = getAuth(cookies);
4007
+ if (auth.error) {
4008
+ return {
4009
+ status: 401,
4010
+ json: {
4011
+ message: auth.error
4012
+ }
4013
+ };
4014
+ }
4015
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
4016
+ return {
4017
+ status: 401,
4018
+ json: {
4019
+ message: "Unauthorized"
4020
+ }
4021
+ };
4022
+ }
4023
+ const currentStat = await serverOps.getStat({
4024
+ ...req.body,
4025
+ profileId: "id" in auth ? auth.id : undefined
4026
+ });
4027
+ if (currentStat.type === "error") {
4028
+ return {
4029
+ status: 500,
4030
+ json: currentStat.error
4031
+ };
4032
+ }
4033
+ return {
4034
+ status: 200,
4035
+ json: {
4036
+ ...currentStat,
4037
+ config: options.config
4038
+ }
4039
+ };
4040
+ }
4041
+ },
4042
+ //#region patches
4043
+ "/patches/~": {
4044
+ GET: async req => {
4045
+ const query = req.query;
4046
+ const cookies = req.cookies;
4047
+ const auth = getAuth(cookies);
4048
+ if (auth.error) {
4049
+ return {
4050
+ status: 401,
4051
+ json: {
4052
+ message: auth.error
4053
+ }
4054
+ };
4055
+ }
4056
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
4057
+ return {
4058
+ status: 401,
4059
+ json: {
4060
+ message: "Unauthorized"
4061
+ }
4062
+ };
4063
+ }
4064
+ const authors = query.author;
4065
+ const patches = await serverOps.fetchPatches({
4066
+ authors,
4067
+ patchIds: query.patch_id,
4068
+ omitPatch: query.omit_patch === true,
4069
+ moduleFilePaths: query.module_file_path
4070
+ });
4071
+ if (patches.error) {
4072
+ // Error is singular
4073
+ console.error("Val: Failed to get patches", patches.errors);
4074
+ return {
4075
+ status: 500,
4076
+ json: {
4077
+ message: patches.error.message,
4078
+ details: patches.error
4079
+ }
4080
+ };
4081
+ }
4082
+ if (patches.errors && Object.keys(patches.errors).length > 0) {
4083
+ // Errors is plural. Different property than above.
4084
+ console.error("Val: Failed to get patches", patches.errors);
4085
+ return {
4086
+ status: 500,
4087
+ json: {
4088
+ message: "Failed to get patches",
4089
+ details: patches.errors
4090
+ }
4091
+ };
4092
+ }
4093
+ return {
4094
+ status: 200,
4095
+ json: patches
4096
+ };
4097
+ },
4098
+ DELETE: async req => {
4099
+ const query = req.query;
4100
+ const cookies = req.cookies;
4101
+ const auth = getAuth(cookies);
4102
+ if (auth.error) {
4103
+ return {
4104
+ status: 401,
4105
+ json: {
4106
+ message: auth.error
4107
+ }
4108
+ };
4109
+ }
4110
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
4111
+ return {
4112
+ status: 401,
4113
+ json: {
4114
+ message: "Unauthorized"
4115
+ }
4116
+ };
4117
+ }
4118
+ const ids = query.id;
4119
+ const deleteRes = await serverOps.deletePatches(ids);
4120
+ if (deleteRes.errors && Object.keys(deleteRes.errors).length > 0) {
4121
+ console.error("Val: Failed to delete patches", deleteRes.errors);
4122
+ return {
4123
+ status: 500,
4124
+ json: {
4125
+ message: "Failed to delete patches",
4126
+ details: deleteRes.errors
4127
+ }
4128
+ };
4129
+ }
4130
+ return {
4131
+ status: 200,
4132
+ json: ids
4133
+ };
4134
+ }
4135
+ },
4136
+ //#region schema
4137
+ "/schema": {
4138
+ GET: async req => {
4139
+ const cookies = req.cookies;
4140
+ const auth = getAuth(cookies);
4141
+ if (auth.error) {
4142
+ return {
4143
+ status: 401,
4144
+ json: {
4145
+ message: auth.error
4146
+ }
4147
+ };
4148
+ }
4149
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
4150
+ return {
4151
+ status: 401,
4152
+ json: {
4153
+ message: "Unauthorized"
4154
+ }
4155
+ };
4156
+ }
4157
+ const moduleErrors = await serverOps.getModuleErrors();
4158
+ if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
4159
+ console.error("Val: Module errors", moduleErrors);
4160
+ return {
4161
+ status: 500,
4162
+ json: {
4163
+ message: `Got errors while fetching modules: ${moduleErrors.filter(error => error).map(error => error.message).join(", ")}`,
4164
+ details: moduleErrors
4165
+ }
4166
+ };
4167
+ }
4168
+ const schemaSha = await serverOps.getSchemaSha();
4169
+ const schemas = await serverOps.getSchemas();
4170
+ const serializedSchemas = {};
4171
+ try {
4172
+ for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
4173
+ const moduleFilePath = moduleFilePathS;
4174
+ serializedSchemas[moduleFilePath] = schema.serialize();
4175
+ }
4176
+ } catch (e) {
4177
+ console.error("Val: Failed to serialize schemas", e);
4178
+ return {
4179
+ status: 500,
4180
+ json: {
4181
+ message: "Failed to serialize schemas",
4182
+ details: [{
4183
+ message: e instanceof Error ? e.message : JSON.stringify(e)
4184
+ }]
4185
+ }
4186
+ };
4187
+ }
4188
+ return {
4189
+ status: 200,
4190
+ json: {
4191
+ schemaSha,
4192
+ schemas: serializedSchemas
4193
+ }
4194
+ };
4195
+ }
4196
+ },
4197
+ // #region sources
4198
+ "/sources": {
4199
+ PUT: async req => {
4200
+ var _body$patchIds;
4201
+ const query = req.query;
4202
+ const cookies = req.cookies;
4203
+ const body = req.body;
4204
+ const treePath = req.path || "";
4205
+ const auth = getAuth(cookies);
4206
+ if (auth.error) {
4207
+ return {
4208
+ status: 401,
4209
+ json: {
4210
+ message: auth.error
4211
+ }
4212
+ };
4213
+ }
4214
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
4215
+ return {
4216
+ status: 401,
4217
+ json: {
4218
+ message: "Unauthorized"
4219
+ }
4220
+ };
4221
+ }
4222
+ const moduleErrors = await serverOps.getModuleErrors();
4223
+ if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
4224
+ console.error("Val: Module errors", moduleErrors);
4225
+ return {
4226
+ status: 500,
4227
+ json: {
4228
+ message: "Val is not correctly setup. Check the val.modules file",
4229
+ details: moduleErrors
4230
+ }
4231
+ };
4232
+ }
4233
+ let tree;
4234
+ let patchAnalysis = null;
4235
+ let newPatchIds = undefined;
4236
+ if (body !== null && body !== void 0 && body.patchIds && (body === null || body === void 0 || (_body$patchIds = body.patchIds) === null || _body$patchIds === void 0 ? void 0 : _body$patchIds.length) > 0 || body !== null && body !== void 0 && body.addPatches) {
4237
+ // TODO: validate patches_sha
4238
+ const patchIds = body === null || body === void 0 ? void 0 : body.patchIds;
4239
+ const patchOps = patchIds && patchIds.length > 0 ? await serverOps.fetchPatches({
4240
+ patchIds,
4241
+ omitPatch: false
4242
+ }) : {
4243
+ patches: {}
4244
+ };
4245
+ if (patchOps.error) {
4246
+ return {
4247
+ status: 400,
4248
+ json: {
4249
+ message: "Failed to fetch patches: " + patchOps.error.message,
4250
+ details: []
4251
+ }
4252
+ };
4253
+ }
4254
+ let patchErrors = undefined;
4255
+ for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
4256
+ const patchId = patchIdS;
4257
+ if (!patchErrors) {
4258
+ patchErrors = {};
4259
+ }
4260
+ patchErrors[patchId] = {
4261
+ message: error.message
4262
+ };
4263
+ }
4264
+ // TODO: errors
4265
+ patchAnalysis = serverOps.analyzePatches(patchOps.patches);
4266
+ if (body !== null && body !== void 0 && body.addPatches) {
4267
+ for (const addPatch of body.addPatches) {
4268
+ const newPatchModuleFilePath = addPatch.path;
4269
+ const newPatchOps = addPatch.patch;
4270
+ const authorId = "id" in auth ? auth.id : null;
4271
+ const createPatchRes = await serverOps.createPatch(newPatchModuleFilePath, {
4272
+ ...patchAnalysis,
4273
+ ...patchOps
4274
+ }, newPatchOps, authorId);
4275
+ if (createPatchRes.error) {
4276
+ return {
4277
+ status: 500,
4278
+ json: {
4279
+ message: "Failed to create patch: " + createPatchRes.error.message,
4280
+ details: createPatchRes.error
4281
+ }
4282
+ };
4283
+ }
4284
+ if (!newPatchIds) {
4285
+ newPatchIds = [createPatchRes.patchId];
4286
+ } else {
4287
+ newPatchIds.push(createPatchRes.patchId);
4288
+ }
4289
+ patchOps.patches[createPatchRes.patchId] = {
4290
+ path: newPatchModuleFilePath,
4291
+ patch: newPatchOps,
4292
+ authorId,
4293
+ createdAt: createPatchRes.createdAt,
4294
+ appliedAt: null
4295
+ };
4296
+ patchAnalysis.patchesByModule[newPatchModuleFilePath] = [...(patchAnalysis.patchesByModule[newPatchModuleFilePath] || []), {
4297
+ patchId: createPatchRes.patchId
4298
+ }];
4299
+ }
4300
+ }
4301
+ tree = {
4302
+ ...(await serverOps.getTree({
4303
+ ...patchAnalysis,
4304
+ ...patchOps
4305
+ }))
4306
+ };
4307
+ if (query.validate_all) {
4308
+ const allTree = await serverOps.getTree();
4309
+ tree = {
4310
+ sources: {
4311
+ ...allTree.sources,
4312
+ ...tree.sources
4313
+ },
4314
+ errors: {
4315
+ ...allTree.errors,
4316
+ ...tree.errors
4317
+ }
4318
+ };
4319
+ }
4320
+ } else {
4321
+ tree = await serverOps.getTree();
3738
4322
  }
3739
- };
3740
- }
3741
- let tree;
3742
- let patchAnalysis = null;
3743
- let newPatchId = undefined;
3744
- 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) {
3745
- var _bodyRes$data4, _bodyRes$data5;
3746
- // TODO: validate patches_sha
3747
- const patchIds = (_bodyRes$data4 = bodyRes.data) === null || _bodyRes$data4 === void 0 ? void 0 : _bodyRes$data4.patchIds;
3748
- const patchOps = patchIds && patchIds.length > 0 ? await this.serverOps.fetchPatches({
3749
- patchIds,
3750
- omitPatch: false
3751
- }) : {
3752
- patches: {}
3753
- };
3754
- let patchErrors = undefined;
3755
- for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
3756
- const patchId = patchIdS;
3757
- if (!patchErrors) {
3758
- patchErrors = {};
3759
- }
3760
- patchErrors[patchId] = {
3761
- message: error.message
4323
+ let sourcesValidation = {
4324
+ errors: {},
4325
+ files: {}
3762
4326
  };
3763
- }
3764
- if ((_bodyRes$data5 = bodyRes.data) !== null && _bodyRes$data5 !== void 0 && _bodyRes$data5.addPatch) {
3765
- const newPatchModuleFilePath = bodyRes.data.addPatch.path;
3766
- const newPatchOps = bodyRes.data.addPatch.patch;
3767
- const authorId = "id" in auth ? auth.id : null;
3768
- const createPatchRes = await this.serverOps.createPatch(newPatchModuleFilePath, newPatchOps, authorId);
3769
- if (createPatchRes.error) {
3770
- return {
3771
- status: 500,
4327
+ if (query.validate_sources || query.validate_binary_files) {
4328
+ const schemas = await serverOps.getSchemas();
4329
+ sourcesValidation = await serverOps.validateSources(schemas, tree.sources);
4330
+
4331
+ // TODO: send validation errors
4332
+ if (query.validate_binary_files) {
4333
+ await serverOps.validateFiles(schemas, tree.sources, sourcesValidation.files);
4334
+ }
4335
+ }
4336
+ const schemaSha = await serverOps.getSchemaSha();
4337
+ const modules = {};
4338
+ for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
4339
+ const moduleFilePath = moduleFilePathS;
4340
+ if (moduleFilePath.startsWith(treePath)) {
4341
+ var _sourcesValidation$er;
4342
+ modules[moduleFilePath] = {
4343
+ source: module,
4344
+ patches: patchAnalysis && patchAnalysis.patchesByModule[moduleFilePath] ? {
4345
+ applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
4346
+ } : undefined,
4347
+ validationErrors: (_sourcesValidation$er = sourcesValidation.errors[moduleFilePath]) === null || _sourcesValidation$er === void 0 ? void 0 : _sourcesValidation$er.validations
4348
+ };
4349
+ }
4350
+ }
4351
+ if (tree.errors && Object.keys(tree.errors).length > 0) {
4352
+ const res = {
4353
+ status: 400,
3772
4354
  json: {
3773
- message: "Failed to create patch: " + createPatchRes.error.message,
3774
- details: createPatchRes.error
4355
+ type: "patch-error",
4356
+ schemaSha,
4357
+ modules,
4358
+ errors: Object.fromEntries(Object.entries(tree.errors).map(([key, value]) => [key, value.map(error => ({
4359
+ patchId: error.patchId,
4360
+ skipped: error.skipped,
4361
+ error: {
4362
+ message: error.error.message
4363
+ }
4364
+ }))])),
4365
+ message: "One or more patches failed to be applied"
3775
4366
  }
3776
4367
  };
4368
+ return res;
3777
4369
  }
3778
- // TODO: evaluate if we need this: seems wrong to delete patches that are not applied
3779
- // for (const fileRes of createPatchRes.files) {
3780
- // if (fileRes.error) {
3781
- // // clean up broken patch:
3782
- // await this.serverOps.deletePatches([createPatchRes.patchId]);
3783
- // return {
3784
- // status: 500,
3785
- // json: {
3786
- // message: "Failed to create patch",
3787
- // details: fileRes.error,
3788
- // },
3789
- // };
3790
- // }
3791
- // }
3792
- newPatchId = createPatchRes.patchId;
3793
- patchOps.patches[createPatchRes.patchId] = {
3794
- path: newPatchModuleFilePath,
3795
- patch: newPatchOps,
3796
- authorId,
3797
- createdAt: createPatchRes.createdAt,
3798
- appliedAt: null
3799
- };
3800
- }
3801
- // TODO: errors
3802
- patchAnalysis = this.serverOps.analyzePatches(patchOps.patches);
3803
- tree = {
3804
- ...(await this.serverOps.getTree({
3805
- ...patchAnalysis,
3806
- ...patchOps
3807
- }))
3808
- };
3809
- if (query.validate_all === "true") {
3810
- const allTree = await this.serverOps.getTree();
3811
- tree = {
3812
- sources: {
3813
- ...allTree.sources,
3814
- ...tree.sources
3815
- },
3816
- errors: {
3817
- ...allTree.errors,
3818
- ...tree.errors
4370
+ const res = {
4371
+ status: 200,
4372
+ json: {
4373
+ schemaSha,
4374
+ modules,
4375
+ newPatchIds
3819
4376
  }
3820
4377
  };
4378
+ return res;
3821
4379
  }
3822
- } else {
3823
- tree = await this.serverOps.getTree();
3824
- }
3825
- if (tree.errors && Object.keys(tree.errors).length > 0) {
3826
- console.error("Val: Failed to get tree", JSON.stringify(tree.errors));
3827
- }
3828
- if (query.validate_sources === "true" || query.validate_binary_files === "true") {
3829
- const schemas = await this.serverOps.getSchemas();
3830
- const sourcesValidation = await this.serverOps.validateSources(schemas, tree.sources);
3831
-
3832
- // TODO: send validation errors
3833
- if (query.validate_binary_files === "true") {
3834
- await this.serverOps.validateFiles(schemas, tree.sources, sourcesValidation.files);
3835
- }
3836
- }
3837
- const schemaSha = await this.serverOps.getSchemaSha();
3838
- const modules = {};
3839
- for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
3840
- const moduleFilePath = moduleFilePathS;
3841
- if (moduleFilePath.startsWith(treePath)) {
3842
- modules[moduleFilePath] = {
3843
- source: module,
3844
- patches: patchAnalysis ? {
3845
- applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
3846
- } : undefined
3847
- };
3848
- }
3849
- }
3850
- return {
3851
- status: 200,
3852
- json: {
3853
- schemaSha,
3854
- modules,
3855
- newPatchId
3856
- }
3857
- };
3858
- }
3859
- async postSave(body, cookies) {
3860
- const auth = this.getAuth(cookies);
3861
- if (auth.error) {
3862
- return {
3863
- status: 401,
3864
- json: {
3865
- message: auth.error
4380
+ },
4381
+ "/save": {
4382
+ POST: async req => {
4383
+ const cookies = req.cookies;
4384
+ const body = req.body;
4385
+ const auth = getAuth(cookies);
4386
+ if (auth.error) {
4387
+ return {
4388
+ status: 401,
4389
+ json: {
4390
+ message: auth.error
4391
+ }
4392
+ };
3866
4393
  }
3867
- };
3868
- }
3869
- const PostSaveBody = z.object({
3870
- patchIds: z.array(z.string().refine(id => true // TODO:
3871
- ))
3872
- });
3873
- const bodyRes = PostSaveBody.safeParse(body);
3874
- if (!bodyRes.success) {
3875
- return {
3876
- status: 400,
3877
- json: {
3878
- message: "Invalid body: " + fromError(bodyRes.error).toString(),
3879
- details: bodyRes.error.errors
4394
+ const PostSaveBody = z.object({
4395
+ patchIds: z.array(z.string().refine(id => true // TODO:
4396
+ ))
4397
+ });
4398
+ const bodyRes = PostSaveBody.safeParse(body);
4399
+ if (!bodyRes.success) {
4400
+ return {
4401
+ status: 400,
4402
+ json: {
4403
+ message: "Invalid body: " + fromError(bodyRes.error).toString(),
4404
+ details: bodyRes.error.errors
4405
+ }
4406
+ };
3880
4407
  }
3881
- };
3882
- }
3883
- const {
3884
- patchIds
3885
- } = bodyRes.data;
3886
- const patches = await this.serverOps.fetchPatches({
3887
- patchIds,
3888
- omitPatch: false
3889
- });
3890
- const analysis = this.serverOps.analyzePatches(patches.patches);
3891
- const preparedCommit = await this.serverOps.prepare({
3892
- ...analysis,
3893
- ...patches
3894
- });
3895
- if (preparedCommit.hasErrors) {
3896
- console.error("Failed to create commit", {
3897
- sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
3898
- binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
3899
- });
3900
- return {
3901
- status: 400,
3902
- json: {
3903
- message: "Failed to create commit",
3904
- details: {
4408
+ const {
4409
+ patchIds
4410
+ } = bodyRes.data;
4411
+ const patches = await serverOps.fetchPatches({
4412
+ patchIds,
4413
+ omitPatch: false
4414
+ });
4415
+ const analysis = serverOps.analyzePatches(patches.patches);
4416
+ const preparedCommit = await serverOps.prepare({
4417
+ ...analysis,
4418
+ ...patches
4419
+ });
4420
+ if (preparedCommit.hasErrors) {
4421
+ console.error("Failed to create commit", JSON.stringify({
3905
4422
  sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
3906
4423
  binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
4424
+ }, null, 2));
4425
+ return {
4426
+ status: 400,
4427
+ json: {
4428
+ message: "Failed to create commit",
4429
+ details: {
4430
+ sourceFilePatchErrors: Object.fromEntries(Object.entries(preparedCommit.sourceFilePatchErrors).map(([key, errors]) => [key, errors.map(e => ({
4431
+ message: formatPatchSourceError(e)
4432
+ }))])),
4433
+ binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
4434
+ }
4435
+ }
4436
+ };
4437
+ }
4438
+ if (serverOps instanceof ValOpsFS) {
4439
+ await serverOps.saveFiles(preparedCommit);
4440
+ await serverOps.deletePatches(patchIds);
4441
+ return {
4442
+ status: 200,
4443
+ json: {} // TODO:
4444
+ };
4445
+ } else if (serverOps instanceof ValOpsHttp) {
4446
+ if (auth.error === undefined && auth.id) {
4447
+ const commitRes = await serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
4448
+ if (commitRes.error) {
4449
+ console.error("Failed to commit", commitRes.error);
4450
+ if ("isNotFastForward" in commitRes && commitRes.isNotFastForward) {
4451
+ return {
4452
+ status: 409,
4453
+ json: {
4454
+ isNotFastForward: true,
4455
+ message: "Cannot commit: this is not the latest version of this branch"
4456
+ }
4457
+ };
4458
+ }
4459
+ return {
4460
+ status: 400,
4461
+ json: {
4462
+ message: commitRes.error.message,
4463
+ details: []
4464
+ }
4465
+ };
4466
+ }
4467
+ // TODO: serverOps.markApplied(patchIds);
4468
+ return {
4469
+ status: 200,
4470
+ json: {} // TODO:
4471
+ };
3907
4472
  }
4473
+ return {
4474
+ status: 401,
4475
+ json: {
4476
+ message: "Unauthorized"
4477
+ }
4478
+ };
4479
+ } else {
4480
+ throw new Error("Invalid server ops");
3908
4481
  }
3909
- };
3910
- }
3911
- if (this.serverOps instanceof ValOpsFS) {
3912
- await this.serverOps.saveFiles(preparedCommit);
3913
- return {
3914
- status: 200,
3915
- json: {} // TODO:
3916
- };
3917
- } else if (this.serverOps instanceof ValOpsHttp) {
3918
- if (auth.error === undefined && auth.id) {
3919
- await this.serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
3920
- return {
3921
- status: 200,
3922
- json: {} // TODO:
3923
- };
3924
4482
  }
3925
- return {
3926
- status: 401,
3927
- json: {
3928
- message: "Unauthorized"
4483
+ },
4484
+ //#region files
4485
+ "/files": {
4486
+ GET: async req => {
4487
+ const query = req.query;
4488
+ const filePath = req.path;
4489
+ // NOTE: no auth here since you would need the patch_id to get something that is not published.
4490
+ // For everything that is published, well they are already public so no auth required there...
4491
+ // We could imagine adding auth just to be a 200% certain,
4492
+ // 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, ...).
4493
+ // So: 1) patch ids are not possible to guess (but possible to brute force)
4494
+ // 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)
4495
+ // 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
4496
+ // Thus: attack surface + ease of attack + benefit = low probability of attack
4497
+ // If we couldn't argue that patch ids are secret enough, then this would be a problem.
4498
+ let cacheControl;
4499
+ let fileBuffer;
4500
+ let mimeType;
4501
+ if (query.patch_id) {
4502
+ fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
4503
+ mimeType = Internal.filenameToMimeType(filePath);
4504
+ cacheControl = "public, max-age=20000, immutable";
4505
+ } else {
4506
+ fileBuffer = await serverOps.getBinaryFile(filePath);
3929
4507
  }
3930
- };
3931
- } else {
3932
- throw new Error("Invalid server ops");
3933
- }
3934
- }
3935
-
3936
- //#region files
3937
- async getFiles(filePath, query) {
3938
- // NOTE: no auth here since you would need the patch_id to get something that is not published.
3939
- // For everything that is published, well they are already public so no auth required there...
3940
- // We could imagine adding auth just to be a 200% certain,
3941
- // 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, ...).
3942
- // So: 1) patch ids are not possible to guess (but possible to brute force)
3943
- // 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)
3944
- // 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
3945
- // Thus: attack surface + ease of attack + benefit = low probability of attack
3946
- // If we couldn't argue that patch ids are secret enough, then this would be a problem.
3947
- let fileBuffer;
3948
- if (query.patch_id) {
3949
- fileBuffer = await this.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
3950
- } else {
3951
- fileBuffer = await this.serverOps.getBinaryFile(filePath);
3952
- }
3953
- if (fileBuffer) {
3954
- return {
3955
- status: 200,
3956
- body: bufferToReadableStream(fileBuffer)
3957
- };
3958
- } else {
3959
- return {
3960
- status: 404,
3961
- json: {
3962
- message: "File not found"
4508
+ if (fileBuffer) {
4509
+ return {
4510
+ status: 200,
4511
+ headers: {
4512
+ // TODO: we could use ETag and return 304 instead
4513
+ "Content-Type": mimeType || "application/octet-stream",
4514
+ "Cache-Control": cacheControl || "public, max-age=0, must-revalidate"
4515
+ },
4516
+ body: bufferToReadableStream(fileBuffer)
4517
+ };
4518
+ } else {
4519
+ return {
4520
+ status: 404,
4521
+ json: {
4522
+ message: "File not found"
4523
+ }
4524
+ };
3963
4525
  }
3964
- };
4526
+ }
3965
4527
  }
4528
+ };
4529
+ };
4530
+ function formatPatchSourceError(error) {
4531
+ if ("message" in error) {
4532
+ return error.message;
4533
+ } else if (Array.isArray(error)) {
4534
+ return error.map(formatPatchSourceError).join("\n");
4535
+ } else {
4536
+ const _exhaustiveCheck = error;
4537
+ return "Unknown patch source error: " + JSON.stringify(_exhaustiveCheck);
3966
4538
  }
3967
4539
  }
3968
4540
  function verifyCallbackReq(stateCookie, queryParams) {
@@ -4082,7 +4654,7 @@ const IntegratedServerJwtPayload = z.object({
4082
4654
  project: z.string()
4083
4655
  });
4084
4656
  async function withAuth(secret, cookies, errorMessageType, handler) {
4085
- const cookie = cookies[VAL_SESSION_COOKIE$1];
4657
+ const cookie = cookies[VAL_SESSION_COOKIE];
4086
4658
  if (typeof cookie === "string") {
4087
4659
  const decodedToken = decodeJwt(cookie, secret);
4088
4660
  if (!decodedToken) {
@@ -4100,7 +4672,7 @@ async function withAuth(secret, cookies, errorMessageType, handler) {
4100
4672
  status: 401,
4101
4673
  json: {
4102
4674
  message: "Session invalid or, most likely, expired. You will need to login again.",
4103
- details: verification.error
4675
+ details: fromError(verification.error).toString()
4104
4676
  }
4105
4677
  };
4106
4678
  }
@@ -4260,14 +4832,14 @@ function guessMimeTypeFromPath(filePath) {
4260
4832
  return null;
4261
4833
  }
4262
4834
 
4263
- async function createValServer(valModules, route, opts, callbacks, formatter) {
4264
- const valServerConfig = await initHandlerOptions(route, opts);
4265
- return new ValServer(valModules, {
4835
+ async function createValServer(valModules, route, opts, config, callbacks, formatter) {
4836
+ const valServerConfig = await initHandlerOptions(route, opts, config);
4837
+ return ValServer(valModules, {
4266
4838
  formatter,
4267
4839
  ...valServerConfig
4268
4840
  }, callbacks);
4269
4841
  }
4270
- async function initHandlerOptions(route, opts) {
4842
+ async function initHandlerOptions(route, opts, config) {
4271
4843
  const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
4272
4844
  const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
4273
4845
  const isProxyMode = opts.mode === "proxy" || opts.mode === undefined && (maybeApiKey || maybeValSecret);
@@ -4312,7 +4884,8 @@ async function initHandlerOptions(route, opts) {
4312
4884
  valEnableRedirectUrl,
4313
4885
  valDisableRedirectUrl,
4314
4886
  valContentUrl,
4315
- valBuildUrl
4887
+ valBuildUrl,
4888
+ config
4316
4889
  };
4317
4890
  } else {
4318
4891
  const cwd = process.cwd();
@@ -4326,7 +4899,8 @@ async function initHandlerOptions(route, opts) {
4326
4899
  valBuildUrl,
4327
4900
  apiKey: maybeApiKey,
4328
4901
  valSecret: maybeValSecret,
4329
- project: maybeValProject
4902
+ project: maybeValProject,
4903
+ config
4330
4904
  };
4331
4905
  }
4332
4906
  }
@@ -4387,17 +4961,9 @@ async function readCommit(gitDir, branchName) {
4387
4961
  return undefined;
4388
4962
  }
4389
4963
  }
4390
- const {
4391
- VAL_SESSION_COOKIE,
4392
- VAL_STATE_COOKIE
4393
- } = Internal;
4394
- const TREE_PATH_PREFIX = "/tree/~";
4395
- const PATCHES_PATH_PREFIX = "/patches/~";
4396
- const FILES_PATH_PREFIX = "/files";
4397
4964
  function createValApiRouter(route, valServerPromise, convert) {
4398
4965
  const uiRequestHandler = createUIRequestHandler();
4399
4966
  return async req => {
4400
- var _req$method;
4401
4967
  const valServer = await valServerPromise;
4402
4968
  const url = new URL(req.url);
4403
4969
  if (!url.pathname.startsWith(route)) {
@@ -4411,115 +4977,235 @@ function createValApiRouter(route, valServerPromise, convert) {
4411
4977
  json: error
4412
4978
  });
4413
4979
  }
4414
- const method = (_req$method = req.method) === null || _req$method === void 0 ? void 0 : _req$method.toUpperCase();
4415
- function withTreePath(path, prefix) {
4416
- return async useTreePath => {
4417
- const pathIndex = path.indexOf("~");
4418
- if (path.startsWith(prefix) && pathIndex !== -1) {
4419
- return useTreePath(path.slice(pathIndex + 1));
4420
- } else {
4421
- if (prefix.indexOf("/~") === -1) {
4422
- return convert({
4423
- status: 500,
4424
- json: {
4425
- message: `Route is incorrectly formed: ${prefix}!`
4980
+ const path = url.pathname.slice(route.length);
4981
+ const groupQueryParams = arr => {
4982
+ const map = {};
4983
+ for (const [key, value] of arr) {
4984
+ const list = map[key] || [];
4985
+ list.push(value);
4986
+ map[key] = list;
4987
+ }
4988
+ return map;
4989
+ };
4990
+ async function getValServerResponse(reqApiRoutePath, req) {
4991
+ var _req$method, _anyApi$route, _anyValServer$route;
4992
+ const anyApi =
4993
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4994
+ Api;
4995
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4996
+ const anyValServer = valServer;
4997
+ const method = (_req$method = req.method) === null || _req$method === void 0 ? void 0 : _req$method.toUpperCase();
4998
+ let route = null;
4999
+ let path = undefined;
5000
+ for (const routeDef of Object.keys(Api)) {
5001
+ if (routeDef === reqApiRoutePath) {
5002
+ route = routeDef;
5003
+ break;
5004
+ }
5005
+ if (reqApiRoutePath.startsWith(routeDef)) {
5006
+ var _anyApi$routeDef;
5007
+ const reqDefinition = anyApi === null || anyApi === void 0 || (_anyApi$routeDef = anyApi[routeDef]) === null || _anyApi$routeDef === void 0 || (_anyApi$routeDef = _anyApi$routeDef[method]) === null || _anyApi$routeDef === void 0 ? void 0 : _anyApi$routeDef.req;
5008
+ if (reqDefinition) {
5009
+ route = routeDef;
5010
+ if (reqDefinition.path) {
5011
+ const subPath = reqApiRoutePath.slice(routeDef.length);
5012
+ const pathRes = reqDefinition.path.safeParse(subPath);
5013
+ if (!pathRes.success) {
5014
+ return zodErrorResult(pathRes.error, `invalid path: '${subPath}' endpoint: '${routeDef}'`);
5015
+ } else {
5016
+ path = pathRes.data;
4426
5017
  }
4427
- });
5018
+ }
5019
+ break;
4428
5020
  }
4429
- return convert({
4430
- status: 404,
4431
- json: {
4432
- message: `Malformed ${prefix} path! Expected: '${prefix}'`
5021
+ }
5022
+ }
5023
+ if (!route) {
5024
+ return {
5025
+ status: 404,
5026
+ json: {
5027
+ message: "Route not found. Valid routes are: " + Object.keys(Api),
5028
+ details: {
5029
+ route,
5030
+ method
4433
5031
  }
4434
- });
5032
+ }
5033
+ };
5034
+ }
5035
+ const apiEndpoint = anyApi === null || anyApi === void 0 || (_anyApi$route = anyApi[route]) === null || _anyApi$route === void 0 ? void 0 : _anyApi$route[method];
5036
+ const reqDefinition = apiEndpoint === null || apiEndpoint === void 0 ? void 0 : apiEndpoint.req;
5037
+ if (!reqDefinition) {
5038
+ return {
5039
+ status: 404,
5040
+ json: {
5041
+ message: `Requested method ${method} on route ${route} is not valid. Valid methods are: ${Object.keys(anyApi[route]).join(", ")}`,
5042
+ details: {
5043
+ route,
5044
+ method
5045
+ }
5046
+ }
5047
+ };
5048
+ }
5049
+ const endpointImpl = anyValServer === null || anyValServer === void 0 || (_anyValServer$route = anyValServer[route]) === null || _anyValServer$route === void 0 ? void 0 : _anyValServer$route[method];
5050
+ if (!endpointImpl) {
5051
+ return {
5052
+ status: 500,
5053
+ json: {
5054
+ message: "Missing server implementation of route with method. This might be caused by a mismatch between Val package versions.",
5055
+ details: {
5056
+ valid: {
5057
+ route: {
5058
+ server: Object.keys(anyValServer || {}),
5059
+ api: Object.keys(anyApi || {})
5060
+ },
5061
+ method: {
5062
+ server: Object.keys((anyValServer === null || anyValServer === void 0 ? void 0 : anyValServer[route]) || {}),
5063
+ api: Object.keys((anyApi === null || anyApi === void 0 ? void 0 : anyApi[route]) || {})
5064
+ }
5065
+ },
5066
+ route,
5067
+ method
5068
+ }
5069
+ }
5070
+ };
5071
+ }
5072
+ let bodyRes;
5073
+ try {
5074
+ bodyRes = reqDefinition.body ? reqDefinition.body.safeParse(await req.json()) : {
5075
+ success: true,
5076
+ data: {}
5077
+ };
5078
+ if (!bodyRes.success) {
5079
+ return zodErrorResult(bodyRes.error, "invalid body data");
4435
5080
  }
5081
+ } catch (e) {
5082
+ return {
5083
+ status: 400,
5084
+ json: {
5085
+ message: "Could not parse request body",
5086
+ details: {
5087
+ error: JSON.stringify(e)
5088
+ }
5089
+ }
5090
+ };
5091
+ }
5092
+ const cookiesRes = reqDefinition.cookies ? getCookies(req, reqDefinition.cookies) : {
5093
+ success: true,
5094
+ data: {}
4436
5095
  };
5096
+ if (!cookiesRes.success) {
5097
+ return zodErrorResult(cookiesRes.error, "invalid cookies");
5098
+ }
5099
+ const actualQueryParams = groupQueryParams(Array.from(url.searchParams.entries()));
5100
+ let query = {};
5101
+ if (reqDefinition.query) {
5102
+ // This is code is particularly heavy, however
5103
+ // @see ValidQueryParamTypes in ApiRouter.ts where we explain what we want to support
5104
+ // We prioritized a declarative ApiRouter, so this code is what we ended up with for better of worse
5105
+ const queryRules = {};
5106
+ for (const [key, zodRule] of Object.entries(reqDefinition.query)) {
5107
+ let innerType = zodRule;
5108
+ let isOptional = false;
5109
+ let isArray = false;
5110
+ // extract inner types:
5111
+ if (innerType instanceof z.ZodOptional) {
5112
+ isOptional = true;
5113
+ innerType = innerType.unwrap();
5114
+ }
5115
+ if (innerType instanceof z.ZodArray) {
5116
+ isArray = true;
5117
+ innerType = innerType.element;
5118
+ }
5119
+ // convert boolean to union of literals true and false so we can parse it as a string
5120
+ if (innerType instanceof z.ZodBoolean) {
5121
+ innerType = z.union([z.literal("true"), z.literal("false")]).transform(arg => arg === "true");
5122
+ }
5123
+ // re-build rules:
5124
+ let arrayCompatibleRule = innerType;
5125
+ arrayCompatibleRule = z.array(innerType); // we always want to parse an array because we group the query params by into an array
5126
+ if (isOptional) {
5127
+ arrayCompatibleRule = arrayCompatibleRule.optional();
5128
+ }
5129
+ if (!isArray) {
5130
+ arrayCompatibleRule = arrayCompatibleRule.transform(arg => arg && arg[0]);
5131
+ }
5132
+ queryRules[key] = arrayCompatibleRule;
5133
+ }
5134
+ const queryRes = z.object(queryRules).safeParse(actualQueryParams);
5135
+ if (!queryRes.success) {
5136
+ return zodErrorResult(queryRes.error, `invalid query params: (${JSON.stringify(actualQueryParams)})`);
5137
+ }
5138
+ query = queryRes.data;
5139
+ }
5140
+ const res = await endpointImpl({
5141
+ body: bodyRes.data,
5142
+ cookies: cookiesRes.data,
5143
+ query,
5144
+ path
5145
+ });
5146
+ if (res.status === 500) {
5147
+ var _res$json;
5148
+ return {
5149
+ status: 500,
5150
+ json: {
5151
+ message: ((_res$json = res.json) === null || _res$json === void 0 ? void 0 : _res$json.message) || "Internal Server Error"
5152
+ }
5153
+ };
5154
+ }
5155
+ const resDef = apiEndpoint.res;
5156
+ if (resDef) {
5157
+ const responseResult = resDef.safeParse(res);
5158
+ if (!responseResult.success) {
5159
+ return {
5160
+ status: 500,
5161
+ json: {
5162
+ message: "Could not validate response. This is likely a bug in Val server.",
5163
+ details: {
5164
+ response: res,
5165
+ errors: formatZodErrorString(responseResult.error)
5166
+ }
5167
+ }
5168
+ };
5169
+ }
5170
+ }
5171
+ return res;
4437
5172
  }
4438
- const path = url.pathname.slice(route.length);
4439
5173
  if (path.startsWith("/static")) {
4440
5174
  return convert(await uiRequestHandler(path.slice("/static".length), url.href));
4441
- } else if (path === "/session") {
4442
- return convert(await valServer.session(getCookies(req, [VAL_SESSION_COOKIE])));
4443
- } else if (path === "/authorize") {
4444
- return convert(await valServer.authorize({
4445
- redirect_to: url.searchParams.get("redirect_to") || undefined
4446
- }));
4447
- } else if (path === "/callback") {
4448
- return convert(await valServer.callback({
4449
- code: url.searchParams.get("code") || undefined,
4450
- state: url.searchParams.get("state") || undefined
4451
- }, getCookies(req, [VAL_STATE_COOKIE])));
4452
- } else if (path === "/logout") {
4453
- return convert(await valServer.logout());
4454
- } else if (path === "/enable") {
4455
- return convert(await valServer.enable({
4456
- redirect_to: url.searchParams.get("redirect_to") || undefined
4457
- }));
4458
- } else if (path === "/disable") {
4459
- return convert(await valServer.disable({
4460
- redirect_to: url.searchParams.get("redirect_to") || undefined
4461
- }));
4462
- } else if (method === "POST" && path === "/save") {
4463
- const body = await req.json();
4464
- return convert(await valServer.postSave(body, getCookies(req, [VAL_SESSION_COOKIE])));
4465
- // } else if (method === "POST" && path === "/validate") {
4466
- // const body = (await req.json()) as unknown;
4467
- // return convert(
4468
- // await valServer.postValidate(
4469
- // body,
4470
- // getCookies(req, [VAL_SESSION_COOKIE]),
4471
- // requestHeaders
4472
- // )
4473
- // );
4474
- } else if (method === "GET" && path === "/schema") {
4475
- return convert(await valServer.getSchema(getCookies(req, [VAL_SESSION_COOKIE])));
4476
- } else if (method === "PUT" && path.startsWith(TREE_PATH_PREFIX)) {
4477
- return withTreePath(path, TREE_PATH_PREFIX)(async treePath => convert(await valServer.putTree(await req.json(), treePath, {
4478
- patches_sha: url.searchParams.get("patches_sha") || undefined,
4479
- validate_all: url.searchParams.get("validate_all") || undefined,
4480
- validate_binary_files: url.searchParams.get("validate_binary_files") || undefined,
4481
- validate_sources: url.searchParams.get("validate_sources") || undefined
4482
- }, getCookies(req, [VAL_SESSION_COOKIE]))));
4483
- } else if (method === "GET" && path.startsWith(PATCHES_PATH_PREFIX)) {
4484
- return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.getPatches({
4485
- authors: url.searchParams.getAll("author"),
4486
- patchIds: url.searchParams.getAll("patch_id"),
4487
- omitPatch: url.searchParams.get("omit_patch") || undefined
4488
- }, getCookies(req, [VAL_SESSION_COOKIE]))));
4489
- } else if (method === "DELETE" && path.startsWith(PATCHES_PATH_PREFIX)) {
4490
- return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.deletePatches({
4491
- id: url.searchParams.getAll("id")
4492
- }, getCookies(req, [VAL_SESSION_COOKIE]))));
4493
- } else if (method === "GET" && path.startsWith(FILES_PATH_PREFIX)) {
4494
- const treePath = path.slice(FILES_PATH_PREFIX.length);
4495
- return convert(await valServer.getFiles(treePath, {
4496
- patch_id: url.searchParams.get("patch_id") || undefined
4497
- }));
4498
5175
  } else {
4499
- return convert({
4500
- status: 404,
4501
- json: {
4502
- message: "Not Found",
4503
- details: {
4504
- method,
4505
- path
4506
- }
4507
- }
4508
- });
5176
+ return convert(await getValServerResponse(path, req));
5177
+ }
5178
+ };
5179
+ }
5180
+ function formatZodErrorString(error) {
5181
+ const errors = fromZodError(error).toString();
5182
+ return errors.length > 640 ? `${errors.slice(0, 640)}...` : errors;
5183
+ }
5184
+ function zodErrorResult(error, message) {
5185
+ return {
5186
+ status: 400,
5187
+ json: {
5188
+ message: "Bad Request: " + message,
5189
+ details: {
5190
+ errors: formatZodErrorString(error)
5191
+ }
4509
5192
  }
4510
5193
  };
4511
5194
  }
4512
5195
 
4513
5196
  // TODO: is this naive implementation is too naive?
4514
- function getCookies(req, names) {
4515
- var _req$headers$get;
4516
- return ((_req$headers$get = req.headers.get("Cookie")) === null || _req$headers$get === void 0 ? void 0 : _req$headers$get.split("; ").reduce((acc, cookie) => {
4517
- const [name, value] = cookie.split("=");
4518
- if (names.includes(name.trim())) {
4519
- acc[name.trim()] = decodeURIComponent(value.trim());
4520
- }
4521
- return acc;
4522
- }, {})) || {};
5197
+ function getCookies(req, cookiesDef) {
5198
+ var _req$headers;
5199
+ const input = {};
5200
+ const cookieParts = (_req$headers = req.headers) === null || _req$headers === void 0 || (_req$headers = _req$headers.get("Cookie")) === null || _req$headers === void 0 ? void 0 : _req$headers.split("; ");
5201
+ for (const name of Object.keys(cookiesDef)) {
5202
+ const cookie = cookieParts === null || cookieParts === void 0 ? void 0 : cookieParts.find(cookie => cookie.startsWith(`${name}=`));
5203
+ const value = cookie ? decodeURIComponent(cookie === null || cookie === void 0 ? void 0 : cookie.split("=")[1]) : undefined;
5204
+ if (value) {
5205
+ input[name.trim()] = value;
5206
+ }
5207
+ }
5208
+ return z.object(cookiesDef).safeParse(input);
4523
5209
  }
4524
5210
 
4525
5211
  /**
@@ -4562,6 +5248,49 @@ class ValFSHost {
4562
5248
  }
4563
5249
  }
4564
5250
 
5251
+ async function extractImageMetadata(filename, input) {
5252
+ const imageSize = sizeOf(input);
5253
+ let mimeType = null;
5254
+ if (imageSize.type) {
5255
+ const possibleMimeType = `image/${imageSize.type}`;
5256
+ if (Internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
5257
+ mimeType = possibleMimeType;
5258
+ }
5259
+ const filenameBasedLookup = Internal.filenameToMimeType(filename);
5260
+ if (filenameBasedLookup) {
5261
+ mimeType = filenameBasedLookup;
5262
+ }
5263
+ }
5264
+ if (!mimeType) {
5265
+ mimeType = "application/octet-stream";
5266
+ }
5267
+ let {
5268
+ width,
5269
+ height
5270
+ } = imageSize;
5271
+ if (!width || !height) {
5272
+ width = 0;
5273
+ height = 0;
5274
+ }
5275
+ return {
5276
+ width,
5277
+ height,
5278
+ mimeType
5279
+ };
5280
+ }
5281
+ async function extractFileMetadata(filename,
5282
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5283
+ _input // TODO: use buffer to determine mimetype
5284
+ ) {
5285
+ let mimeType = Internal.filenameToMimeType(filename);
5286
+ if (!mimeType) {
5287
+ mimeType = "application/octet-stream";
5288
+ }
5289
+ return {
5290
+ mimeType
5291
+ };
5292
+ }
5293
+
4565
5294
  function getValidationErrorFileRef(validationError) {
4566
5295
  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;
4567
5296
  if (!maybeRef) {
@@ -4589,15 +5318,15 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4589
5318
  throw Error("Cannot fix file without a file reference");
4590
5319
  }
4591
5320
  const filename = fsPath__default.join(config.projectRoot, fileRef);
4592
- const buffer = fs.readFileSync(filename);
4593
- return extractFileMetadata(fileRef, buffer);
5321
+ fs.readFileSync(filename);
5322
+ return extractFileMetadata(fileRef);
4594
5323
  }
4595
5324
  const remainingErrors = [];
4596
5325
  const patch = [];
4597
5326
  for (const fix of validationError.fixes || []) {
4598
5327
  if (fix === "image:replace-metadata" || fix === "image:add-metadata") {
4599
5328
  const imageMetadata = await getImageMetadata();
4600
- if (imageMetadata.width === undefined || imageMetadata.height === undefined || imageMetadata.sha256 === undefined) {
5329
+ if (imageMetadata.width === undefined || imageMetadata.height === undefined) {
4601
5330
  remainingErrors.push({
4602
5331
  ...validationError,
4603
5332
  message: "Failed to get image metadata",
@@ -4608,8 +5337,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4608
5337
  const metadataIsCorrect =
4609
5338
  // metadata is a prop that is an object
4610
5339
  typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
4611
- // sha256 is correct
4612
- "sha256" in currentValue.metadata && currentValue.metadata.sha256 === imageMetadata.sha256 &&
4613
5340
  // width is correct
4614
5341
  "width" in currentValue.metadata && currentValue.metadata.width === imageMetadata.width &&
4615
5342
  // height is correct
@@ -4626,18 +5353,11 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4626
5353
  value: {
4627
5354
  width: imageMetadata.width,
4628
5355
  height: imageMetadata.height,
4629
- sha256: imageMetadata.sha256,
4630
5356
  mimeType: imageMetadata.mimeType
4631
5357
  }
4632
5358
  });
4633
5359
  } else {
4634
5360
  if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
4635
- if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== imageMetadata.sha256) {
4636
- remainingErrors.push({
4637
- message: "Image metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + imageMetadata.sha256 + ".",
4638
- fixes: undefined
4639
- });
4640
- }
4641
5361
  if (!("width" in currentValue.metadata) || currentValue.metadata.width !== imageMetadata.width) {
4642
5362
  remainingErrors.push({
4643
5363
  message: "Image metadata width is incorrect! Found: " + ("width" in currentValue.metadata ? currentValue.metadata.width : "<empty>") + ". Expected: " + imageMetadata.width,
@@ -4672,14 +5392,13 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4672
5392
  value: {
4673
5393
  width: imageMetadata.width,
4674
5394
  height: imageMetadata.height,
4675
- sha256: imageMetadata.sha256,
4676
5395
  mimeType: imageMetadata.mimeType
4677
5396
  }
4678
5397
  });
4679
5398
  }
4680
5399
  } else if (fix === "file:add-metadata" || fix === "file:check-metadata") {
4681
5400
  const fileMetadata = await getFileMetadata();
4682
- if (fileMetadata.sha256 === undefined) {
5401
+ if (fileMetadata === undefined) {
4683
5402
  remainingErrors.push({
4684
5403
  ...validationError,
4685
5404
  message: "Failed to get image metadata",
@@ -4690,8 +5409,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4690
5409
  const metadataIsCorrect =
4691
5410
  // metadata is a prop that is an object
4692
5411
  typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
4693
- // sha256 is correct
4694
- "sha256" in currentValue.metadata && currentValue.metadata.sha256 === fileMetadata.sha256 &&
4695
5412
  // mimeType is correct
4696
5413
  "mimeType" in currentValue.metadata && currentValue.metadata.mimeType === fileMetadata.mimeType;
4697
5414
 
@@ -4702,7 +5419,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4702
5419
  op: "replace",
4703
5420
  path: sourceToPatchPath(sourcePath).concat("metadata"),
4704
5421
  value: {
4705
- sha256: fileMetadata.sha256,
4706
5422
  ...(fileMetadata.mimeType ? {
4707
5423
  mimeType: fileMetadata.mimeType
4708
5424
  } : {})
@@ -4710,12 +5426,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4710
5426
  });
4711
5427
  } else {
4712
5428
  if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
4713
- if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== fileMetadata.sha256) {
4714
- remainingErrors.push({
4715
- message: "File metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + fileMetadata.sha256 + ".",
4716
- fixes: undefined
4717
- });
4718
- }
4719
5429
  if (!("mimeType" in currentValue.metadata) || currentValue.metadata.mimeType !== fileMetadata.mimeType) {
4720
5430
  remainingErrors.push({
4721
5431
  message: "File metadata mimeType is incorrect! Found: " + ("mimeType" in currentValue.metadata ? currentValue.metadata.mimeType : "<empty>") + ". Expected: " + fileMetadata.mimeType,
@@ -4736,7 +5446,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4736
5446
  op: "add",
4737
5447
  path: sourceToPatchPath(sourcePath).concat("metadata"),
4738
5448
  value: {
4739
- sha256: fileMetadata.sha256,
4740
5449
  ...(fileMetadata.mimeType ? {
4741
5450
  mimeType: fileMetadata.mimeType
4742
5451
  } : {})