@valbuild/server 0.64.0 → 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.
@@ -1273,56 +1273,6 @@ function encodeJwt(payload, sessionKey) {
1273
1273
  return `${jwtHeaderBase64}.${payloadBase64}.${crypto__default["default"].createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
1274
1274
  }
1275
1275
 
1276
- const textEncoder$2 = new TextEncoder();
1277
- async function extractImageMetadata(filename, input) {
1278
- const imageSize = sizeOf__default["default"](input);
1279
- let mimeType = null;
1280
- if (imageSize.type) {
1281
- const possibleMimeType = `image/${imageSize.type}`;
1282
- if (core.Internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
1283
- mimeType = possibleMimeType;
1284
- }
1285
- const filenameBasedLookup = core.Internal.filenameToMimeType(filename);
1286
- if (filenameBasedLookup) {
1287
- mimeType = filenameBasedLookup;
1288
- }
1289
- }
1290
- if (!mimeType) {
1291
- mimeType = "application/octet-stream";
1292
- }
1293
- let {
1294
- width,
1295
- height
1296
- } = imageSize;
1297
- if (!width || !height) {
1298
- width = 0;
1299
- height = 0;
1300
- }
1301
- const sha256 = getSha256(mimeType, input);
1302
- return {
1303
- width,
1304
- height,
1305
- sha256,
1306
- mimeType
1307
- };
1308
- }
1309
- function getSha256(mimeType, input) {
1310
- return core.Internal.getSHA256Hash(textEncoder$2.encode(
1311
- // TODO: we should probably store the mimetype in the metadata and reuse it here
1312
- `data:${mimeType};base64,${input.toString("base64")}`));
1313
- }
1314
- async function extractFileMetadata(filename, input) {
1315
- let mimeType = core.Internal.filenameToMimeType(filename);
1316
- if (!mimeType) {
1317
- mimeType = "application/octet-stream";
1318
- }
1319
- const sha256 = getSha256(mimeType, input);
1320
- return {
1321
- sha256,
1322
- mimeType
1323
- };
1324
- }
1325
-
1326
1276
  /* eslint-disable @typescript-eslint/no-unused-vars */
1327
1277
  const textEncoder$1 = new TextEncoder();
1328
1278
  const jsonOps = new patch.JSONOps();
@@ -1351,15 +1301,20 @@ class ValOps {
1351
1301
  this.modulesErrors = null;
1352
1302
  }
1353
1303
  hash(input) {
1354
- let str;
1355
- if (typeof input === "string") {
1356
- str = input;
1357
- } else {
1358
- str = JSON.stringify(input);
1359
- }
1360
- return core.Internal.getSHA256Hash(textEncoder$1.encode(str));
1304
+ return core.Internal.getSHA256Hash(textEncoder$1.encode(input));
1361
1305
  }
1362
1306
 
1307
+ // #region stat
1308
+ /**
1309
+ * Get the status from Val
1310
+ *
1311
+ * This works differently in ValOpsFS and ValOpsHttp:
1312
+ * - 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).
1313
+ * - In ValOpsHttp (in production) it returns a WebSocket URL so that the client can connect directly.
1314
+ *
1315
+ * 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.
1316
+ */
1317
+
1363
1318
  // #region initTree
1364
1319
  async initTree() {
1365
1320
  if (this.baseSha === null || this.schemaSha === null || this.sources === null || this.schemas === null || this.modulesErrors === null) {
@@ -1416,17 +1371,25 @@ class ValOps {
1416
1371
  addModuleError(`source in ${path} is undefined`, moduleIdx, path);
1417
1372
  return;
1418
1373
  }
1374
+ let serializedSchema;
1375
+ try {
1376
+ serializedSchema = schema.serialize();
1377
+ } catch (e) {
1378
+ const message = e instanceof Error ? e.message : JSON.stringify(e);
1379
+ addModuleError(`Could not serialize module: '${path}'. Error: ${message}`, moduleIdx, path);
1380
+ return;
1381
+ }
1419
1382
  const pathM = path;
1420
1383
  currentSources[pathM] = source;
1421
1384
  currentSchemas[pathM] = schema;
1422
1385
  // make sure the checks above is enough that this does not fail - even if val modules are not set up correctly
1423
- baseSha += this.hash({
1386
+ baseSha = this.hash(baseSha + JSON.stringify({
1424
1387
  path,
1425
- schema: schema.serialize(),
1388
+ schema: serializedSchema,
1426
1389
  source,
1427
1390
  modulesErrors: currentModulesErrors
1428
- });
1429
- schemaSha += this.hash(schema.serialize());
1391
+ }));
1392
+ schemaSha = this.hash(schemaSha + JSON.stringify(serializedSchema));
1430
1393
  });
1431
1394
  }
1432
1395
  this.sources = currentSources;
@@ -1622,25 +1585,27 @@ class ValOps {
1622
1585
  }
1623
1586
  for (const [sourcePathS, validationErrors] of Object.entries(res)) {
1624
1587
  const sourcePath = sourcePathS;
1625
- for (const validationError of validationErrors) {
1626
- if (isOnlyFileCheckValidationError(validationError)) {
1627
- if (files[sourcePath]) {
1628
- throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
1629
- }
1630
- const value = validationError.value;
1631
- if (isFileSource(value)) {
1632
- files[sourcePath] = value;
1633
- }
1634
- } else {
1635
- if (!errors[path]) {
1636
- errors[path] = {
1637
- validations: {}
1638
- };
1639
- }
1640
- if (!errors[path].validations[sourcePath]) {
1641
- errors[path].validations[sourcePath] = [];
1588
+ if (validationErrors) {
1589
+ for (const validationError of validationErrors) {
1590
+ if (isOnlyFileCheckValidationError(validationError)) {
1591
+ if (files[sourcePath]) {
1592
+ throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
1593
+ }
1594
+ const value = validationError.value;
1595
+ if (isFileSource(value)) {
1596
+ files[sourcePath] = value;
1597
+ }
1598
+ } else {
1599
+ if (!errors[path]) {
1600
+ errors[path] = {
1601
+ validations: {}
1602
+ };
1603
+ }
1604
+ if (!errors[path].validations[sourcePath]) {
1605
+ errors[path].validations[sourcePath] = [];
1606
+ }
1607
+ errors[path].validations[sourcePath].push(validationError);
1642
1608
  }
1643
- errors[path].validations[sourcePath].push(validationError);
1644
1609
  }
1645
1610
  }
1646
1611
  }
@@ -1828,8 +1793,22 @@ class ValOps {
1828
1793
  const patchRes = patch.applyPatch(tsSourceFile, tsOps, sourceFileOps);
1829
1794
  if (fp.result.isErr(patchRes)) {
1830
1795
  if (Array.isArray(patchRes.error)) {
1796
+ for (const error of patchRes.error) {
1797
+ console.error("Could not patch", JSON.stringify({
1798
+ path,
1799
+ patchId,
1800
+ error,
1801
+ sourceFileOps
1802
+ }, null, 2));
1803
+ }
1831
1804
  errors.push(...patchRes.error);
1832
1805
  } else {
1806
+ console.error("Could not patch", JSON.stringify({
1807
+ path,
1808
+ patchId,
1809
+ error: patchRes.error,
1810
+ sourceFileOps
1811
+ }, null, 2));
1833
1812
  errors.push(patchRes.error);
1834
1813
  }
1835
1814
  triedPatches.push(patchId);
@@ -1923,16 +1902,23 @@ class ValOps {
1923
1902
  }
1924
1903
 
1925
1904
  // #region createPatch
1926
- async createPatch(path, patch$1, authorId) {
1927
- const {
1928
- sources,
1929
- schemas,
1930
- moduleErrors
1931
- } = await this.initTree();
1905
+ async createPatch(path, patchAnalysis, patch$1, authorId) {
1906
+ const initTree = await this.initTree();
1907
+ const schemas = initTree.schemas;
1908
+ const moduleErrors = initTree.moduleErrors;
1909
+ let sources = initTree.sources;
1910
+ if (patchAnalysis) {
1911
+ const tree = await this.getTree(patchAnalysis);
1912
+ sources = {
1913
+ ...sources,
1914
+ ...tree.sources
1915
+ };
1916
+ }
1932
1917
  const source = sources[path];
1933
1918
  const schema = schemas[path];
1934
1919
  const moduleError = moduleErrors.find(e => e.path === path);
1935
1920
  if (moduleError) {
1921
+ console.error(`Cannot patch. Module at path: '${path}' has fatal errors: "${moduleError.message}"`);
1936
1922
  return {
1937
1923
  error: {
1938
1924
  message: `Cannot patch. Module at path: '${path}' has fatal errors: ` + moduleErrors.map(m => `"${m.message}"`).join(" and ")
@@ -1940,6 +1926,7 @@ class ValOps {
1940
1926
  };
1941
1927
  }
1942
1928
  if (!source) {
1929
+ console.error(`Cannot patch. Module source at path: '${path}' does not exist`);
1943
1930
  return {
1944
1931
  error: {
1945
1932
  message: `Cannot patch. Module source at path: '${path}' does not exist`
@@ -1947,6 +1934,7 @@ class ValOps {
1947
1934
  };
1948
1935
  }
1949
1936
  if (!schema) {
1937
+ console.error(`Cannot patch. Module schema at path: '${path}' does not exist`);
1950
1938
  return {
1951
1939
  error: {
1952
1940
  message: `Cannot patch. Module schema at path: '${path}' does not exist`
@@ -1964,10 +1952,12 @@ class ValOps {
1964
1952
  filePath
1965
1953
  } = op;
1966
1954
  if (files[filePath]) {
1955
+ console.error(`Cannot have multiple files with same path in same patch. Path: ${filePath}`);
1967
1956
  files[filePath] = {
1968
1957
  error: new patch.PatchError("Cannot have multiple files with same path in same patch")
1969
1958
  };
1970
1959
  } else if (typeof value !== "string") {
1960
+ console.error(`Value is not a string. Path: ${filePath}. Value: ${value}`);
1971
1961
  files[filePath] = {
1972
1962
  error: new patch.PatchError("Value is not a string")
1973
1963
  };
@@ -1983,15 +1973,14 @@ class ValOps {
1983
1973
  path: op.path,
1984
1974
  filePath,
1985
1975
  nestedFilePath: op.nestedFilePath,
1986
- value: {
1987
- sha256
1988
- }
1976
+ value: sha256
1989
1977
  });
1990
1978
  }
1991
1979
  }
1992
1980
  }
1993
1981
  const saveRes = await this.saveSourceFilePatch(path, sourceFileOps, authorId);
1994
1982
  if (saveRes.error) {
1983
+ console.error(`Could not save source file patch at path: '${path}'. Error: ${saveRes.error.message}`);
1995
1984
  return {
1996
1985
  error: saveRes.error
1997
1986
  };
@@ -2015,17 +2004,20 @@ class ValOps {
2015
2004
  ? "image" : schemaAtPath instanceof core.FileSchema ? "file" : schemaAtPath.serialize().type;
2016
2005
  } catch (e) {
2017
2006
  if (e instanceof Error) {
2007
+ console.error(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`);
2018
2008
  return {
2019
2009
  filePath,
2020
2010
  error: new patch.PatchError(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`)
2021
2011
  };
2022
2012
  }
2013
+ console.error(`Could not resolve file type at: ${modulePath}. Unknown error.`);
2023
2014
  return {
2024
2015
  filePath,
2025
2016
  error: new patch.PatchError(`Could not resolve file type at: ${modulePath}. Unknown error.`)
2026
2017
  };
2027
2018
  }
2028
2019
  if (type !== "image" && type !== "file") {
2020
+ console.error("Unknown file type (resolved from schema): " + type);
2029
2021
  return {
2030
2022
  filePath,
2031
2023
  error: new patch.PatchError("Unknown file type (resolved from schema): " + type)
@@ -2033,6 +2025,7 @@ class ValOps {
2033
2025
  }
2034
2026
  const mimeType = getMimeTypeFromBase64(data.value);
2035
2027
  if (!mimeType) {
2028
+ console.error("Could not get mimeType from base 64 encoded value");
2036
2029
  return {
2037
2030
  filePath,
2038
2031
  error: new patch.PatchError("Could not get mimeType from base 64 encoded value. First chars were: " + data.value.slice(0, 20))
@@ -2040,6 +2033,7 @@ class ValOps {
2040
2033
  }
2041
2034
  const buffer = bufferFromDataUrl(data.value);
2042
2035
  if (!buffer) {
2036
+ console.error("Could not create buffer from base 64 encoded value");
2043
2037
  return {
2044
2038
  filePath,
2045
2039
  error: new patch.PatchError("Could not create buffer from base 64 encoded value")
@@ -2047,6 +2041,7 @@ class ValOps {
2047
2041
  }
2048
2042
  const metadataOps = createMetadataFromBuffer(type, mimeType, buffer);
2049
2043
  if (metadataOps.errors) {
2044
+ console.error(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`);
2050
2045
  return {
2051
2046
  filePath,
2052
2047
  error: new patch.PatchError(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`)
@@ -2068,6 +2063,14 @@ class ValOps {
2068
2063
  };
2069
2064
  }
2070
2065
  }));
2066
+ const errors = saveFileRes.filter(f => !!f.error);
2067
+ if (errors.length > 0) {
2068
+ return {
2069
+ error: {
2070
+ message: "Could not save patch: " + errors.map(e => e.error.message).join(", ")
2071
+ }
2072
+ };
2073
+ }
2071
2074
  return {
2072
2075
  patchId,
2073
2076
  files: saveFileRes,
@@ -2092,14 +2095,13 @@ function isFileSource(value) {
2092
2095
  }
2093
2096
  function getFieldsForType(type) {
2094
2097
  if (type === "file") {
2095
- return ["sha256", "mimeType"];
2098
+ return ["mimeType"];
2096
2099
  } else if (type === "image") {
2097
- return ["sha256", "mimeType", "height", "width"];
2100
+ return ["mimeType", "height", "width"];
2098
2101
  }
2099
2102
  throw new Error("Unknown type: " + type);
2100
2103
  }
2101
2104
  function createMetadataFromBuffer(type, mimeType, buffer) {
2102
- const sha256 = getSha256(mimeType, buffer);
2103
2105
  const errors = [];
2104
2106
  let availableMetadata;
2105
2107
  if (type === "image") {
@@ -2108,7 +2110,7 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
2108
2110
  height,
2109
2111
  type
2110
2112
  } = sizeOf__default["default"](buffer);
2111
- const normalizedType = type === "jpg" ? "jpeg" : type;
2113
+ const normalizedType = type === "jpg" ? "jpeg" : type === "svg" ? "svg+xml" : type;
2112
2114
  if (type !== undefined && `image/${normalizedType}` !== mimeType) {
2113
2115
  return {
2114
2116
  errors: [{
@@ -2117,14 +2119,12 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
2117
2119
  };
2118
2120
  }
2119
2121
  availableMetadata = {
2120
- sha256: sha256,
2121
2122
  mimeType,
2122
2123
  height,
2123
2124
  width
2124
2125
  };
2125
2126
  } else {
2126
2127
  availableMetadata = {
2127
- sha256: sha256,
2128
2128
  mimeType
2129
2129
  };
2130
2130
  }
@@ -2244,9 +2244,7 @@ const OperationT = z__default["default"].discriminatedUnion("op", [z__default["d
2244
2244
  path: z__default["default"].array(z__default["default"].string()),
2245
2245
  filePath: z__default["default"].string(),
2246
2246
  nestedFilePath: z__default["default"].array(z__default["default"].string()).optional(),
2247
- value: z__default["default"].union([z__default["default"].string(), z__default["default"].object({
2248
- sha256: z__default["default"].string()
2249
- })])
2247
+ value: z__default["default"].string()
2250
2248
  }).strict()]);
2251
2249
  const Patch = z__default["default"].array(OperationT);
2252
2250
 
@@ -2260,6 +2258,161 @@ class ValOpsFS extends ValOps {
2260
2258
  async onInit() {
2261
2259
  // do nothing
2262
2260
  }
2261
+ async getStat(params) {
2262
+ // In ValOpsFS, we don't have a websocket server to listen to file changes so we use long-polling.
2263
+ // If a file that Val depends on changes, we break the connection and tell the client to request again to get the latest values.
2264
+ try {
2265
+ var _this$options, _this$options2;
2266
+ const currentBaseSha = await this.getBaseSha();
2267
+ const currentSchemaSha = await this.getSchemaSha();
2268
+ const moduleFilePaths = Object.keys(await this.getSchemas());
2269
+ const patchData = await this.readPatches();
2270
+ const patches = [];
2271
+ // TODO: use proper patch sequences when available:
2272
+ for (const [patchId] of Object.entries(patchData.patches).sort(([, a], [, b]) => {
2273
+ return a.createdAt.localeCompare(b.createdAt, undefined);
2274
+ })) {
2275
+ patches.push(patchId);
2276
+ }
2277
+ // something changed: return immediately
2278
+ const didChange = !params || currentBaseSha !== params.baseSha || currentSchemaSha !== params.schemaSha || patches.length !== params.patches.length || patches.some((p, i) => p !== params.patches[i]);
2279
+ if (didChange) {
2280
+ return {
2281
+ type: "did-change",
2282
+ baseSha: currentBaseSha,
2283
+ schemaSha: currentSchemaSha,
2284
+ patches
2285
+ };
2286
+ }
2287
+ let fsWatcher = null;
2288
+ let stopPolling = false;
2289
+ const didDirectoryChangeUsingPolling = (dir, interval, setHandle) => {
2290
+ const mtimeInDir = {};
2291
+ if (fs__default["default"].existsSync(dir)) {
2292
+ for (const file of fs__default["default"].readdirSync(dir)) {
2293
+ mtimeInDir[file] = fs__default["default"].statSync(fsPath__namespace["default"].join(dir, file)).mtime.getTime();
2294
+ }
2295
+ }
2296
+ return new Promise(resolve => {
2297
+ const go = resolve => {
2298
+ const start = Date.now();
2299
+ if (fs__default["default"].existsSync(dir)) {
2300
+ const subDirs = fs__default["default"].readdirSync(dir);
2301
+ // amount of files changed
2302
+ if (subDirs.length !== Object.keys(mtimeInDir).length) {
2303
+ resolve("request-again");
2304
+ }
2305
+ for (const file of fs__default["default"].readdirSync(dir)) {
2306
+ const mtime = fs__default["default"].statSync(fsPath__namespace["default"].join(dir, file)).mtime.getTime();
2307
+ if (mtime !== mtimeInDir[file]) {
2308
+ resolve("request-again");
2309
+ }
2310
+ }
2311
+ } else {
2312
+ // dir had files, but now is deleted
2313
+ if (Object.keys(mtimeInDir).length > 0) {
2314
+ resolve("request-again");
2315
+ }
2316
+ }
2317
+ if (Date.now() - start > interval) {
2318
+ console.warn("Val: polling interval of patches exceeded");
2319
+ }
2320
+ if (stopPolling) {
2321
+ return;
2322
+ }
2323
+ setHandle(setTimeout(() => go(resolve), interval));
2324
+ };
2325
+ setHandle(setTimeout(() => go(resolve), interval));
2326
+ });
2327
+ };
2328
+ const didFilesChangeUsingPolling = (files, interval, setHandle) => {
2329
+ const mtimes = {};
2330
+ for (const file of files) {
2331
+ if (fs__default["default"].existsSync(file)) {
2332
+ mtimes[file] = fs__default["default"].statSync(file).mtime.getTime();
2333
+ } else {
2334
+ mtimes[file] = -1;
2335
+ }
2336
+ }
2337
+ return new Promise(resolve => {
2338
+ const go = resolve => {
2339
+ const start = Date.now();
2340
+ for (const file of files) {
2341
+ const mtime = fs__default["default"].existsSync(file) ? fs__default["default"].statSync(file).mtime.getTime() : -1;
2342
+ if (mtime !== mtimes[file]) {
2343
+ resolve("request-again");
2344
+ }
2345
+ }
2346
+ if (Date.now() - start > interval) {
2347
+ console.warn("Val: polling interval of files exceeded");
2348
+ }
2349
+ setHandle(setTimeout(() => go(resolve), interval));
2350
+ };
2351
+ if (stopPolling) {
2352
+ return;
2353
+ }
2354
+ setHandle(setTimeout(() => go(resolve), interval));
2355
+ });
2356
+ };
2357
+ 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
2358
+ const disableFilePolling = ((_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.disableFilePolling) || false;
2359
+ let patchesDirHandle;
2360
+ let valFilesIntervalHandle;
2361
+ const type = await Promise.race([
2362
+ // 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
2363
+ disableFilePolling ? new Promise(() => {}) : didDirectoryChangeUsingPolling(this.getPatchesDir(), statFilePollingInterval, handle => {
2364
+ patchesDirHandle = handle;
2365
+ }),
2366
+ // we poll the files that Val depends on for changes
2367
+ disableFilePolling ? new Promise(() => {}) : didFilesChangeUsingPolling([fsPath__namespace["default"].join(this.rootDir, "val.config.ts"), fsPath__namespace["default"].join(this.rootDir, "val.modules.ts"), fsPath__namespace["default"].join(this.rootDir, "val.config.js"), fsPath__namespace["default"].join(this.rootDir, "val.modules.js"), ...moduleFilePaths.map(p => fsPath__namespace["default"].join(this.rootDir, p))], statFilePollingInterval, handle => {
2368
+ valFilesIntervalHandle = handle;
2369
+ }), new Promise(resolve => {
2370
+ fsWatcher = fs__default["default"].watch(this.rootDir, {
2371
+ recursive: true
2372
+ }, (eventType, filename) => {
2373
+ if (!filename) {
2374
+ return;
2375
+ }
2376
+ 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");
2377
+ if (isChange) {
2378
+ // a file that Val depends on just changed or a patch was created, break connection and request stat again to get the new values
2379
+ resolve("request-again");
2380
+ }
2381
+ });
2382
+ }), new Promise(resolve => {
2383
+ var _this$options3;
2384
+ return setTimeout(() => resolve("no-change"), ((_this$options3 = this.options) === null || _this$options3 === void 0 ? void 0 : _this$options3.statPollingInterval) || 20000);
2385
+ })]).finally(() => {
2386
+ if (fsWatcher) {
2387
+ fsWatcher.close();
2388
+ }
2389
+ stopPolling = true;
2390
+ clearInterval(patchesDirHandle);
2391
+ clearInterval(valFilesIntervalHandle);
2392
+ });
2393
+ return {
2394
+ type,
2395
+ baseSha: currentBaseSha,
2396
+ schemaSha: currentSchemaSha,
2397
+ patches
2398
+ };
2399
+ } catch (err) {
2400
+ if (err instanceof Error) {
2401
+ return {
2402
+ type: "error",
2403
+ error: {
2404
+ message: err.message
2405
+ }
2406
+ };
2407
+ }
2408
+ return {
2409
+ type: "error",
2410
+ error: {
2411
+ message: "Unknown error (getStat)"
2412
+ }
2413
+ };
2414
+ }
2415
+ }
2263
2416
  async readPatches(includes) {
2264
2417
  const patchesCacheDir = this.getPatchesDir();
2265
2418
  let patchJsonFiles = [];
@@ -2268,8 +2421,8 @@ class ValOpsFS extends ValOps {
2268
2421
  }
2269
2422
  const patches = {};
2270
2423
  const errors = {};
2271
- const sortedPatchIds = patchJsonFiles.map(file => parseInt(fsPath__namespace["default"].basename(fsPath__namespace["default"].dirname(file)), 10)).sort();
2272
- for (const patchIdNum of sortedPatchIds) {
2424
+ const parsedPatchIds = patchJsonFiles.map(file => parseInt(fsPath__namespace["default"].basename(fsPath__namespace["default"].dirname(file)), 10)).sort();
2425
+ for (const patchIdNum of parsedPatchIds) {
2273
2426
  if (Number.isNaN(patchIdNum)) {
2274
2427
  throw new Error("Could not parse patch id from file name. Files found: " + patchJsonFiles.join(", "));
2275
2428
  }
@@ -2310,6 +2463,12 @@ class ValOpsFS extends ValOps {
2310
2463
  errors: allErrors,
2311
2464
  patches: allPatches
2312
2465
  } = await this.readPatches(filters.patchIds);
2466
+ if (allErrors && Object.keys(allErrors).length > 0) {
2467
+ for (const [patchId, error] of Object.entries(allErrors)) {
2468
+ console.error("Error reading patch", patchId, error);
2469
+ errors[patchId] = error;
2470
+ }
2471
+ }
2313
2472
  for (const [patchIdS, patch] of Object.entries(allPatches)) {
2314
2473
  const patchId = patchIdS;
2315
2474
  if (filters.authors && !(patch.authorId === null || filters.authors.includes(patch.authorId))) {
@@ -2325,10 +2484,6 @@ class ValOpsFS extends ValOps {
2325
2484
  authorId: patch.authorId,
2326
2485
  appliedAt: patch.appliedAt
2327
2486
  };
2328
- const error = allErrors && allErrors[patchId];
2329
- if (error) {
2330
- errors[patchId] = error;
2331
- }
2332
2487
  }
2333
2488
  if (errors && Object.keys(errors).length > 0) {
2334
2489
  return {
@@ -2672,11 +2827,11 @@ class ValOpsFS extends ValOps {
2672
2827
  getPatchDir(patchId) {
2673
2828
  return fsPath__namespace["default"].join(this.getPatchesDir(), patchId);
2674
2829
  }
2675
- getBinaryFilePath(filename, patchId) {
2676
- return fsPath__namespace["default"].join(this.getPatchDir(patchId), "files", filename, fsPath__namespace["default"].basename(filename));
2830
+ getBinaryFilePath(filePath, patchId) {
2831
+ return fsPath__namespace["default"].join(this.getPatchDir(patchId), "files", filePath, fsPath__namespace["default"].basename(filePath));
2677
2832
  }
2678
- getBinaryFileMetadataPath(filename, patchId) {
2679
- return fsPath__namespace["default"].join(this.getPatchDir(patchId), "files", filename, "metadata.json");
2833
+ getBinaryFileMetadataPath(filePath, patchId) {
2834
+ return fsPath__namespace["default"].join(this.getPatchDir(patchId), "files", filePath, "metadata.json");
2680
2835
  }
2681
2836
  getPatchFilePath(patchId) {
2682
2837
  return fsPath__namespace["default"].join(this.getPatchDir(patchId), "patch.json");
@@ -2749,12 +2904,10 @@ const BaseSha = z.z.string().refine(s => !!s); // TODO: validate
2749
2904
  const AuthorId = z.z.string().refine(s => !!s); // TODO: validate
2750
2905
  const ModuleFilePath = z.z.string().refine(s => !!s); // TODO: validate
2751
2906
  const Metadata = z.z.union([z.z.object({
2752
- sha256: z.z.string(),
2753
2907
  mimeType: z.z.string(),
2754
2908
  width: z.z.number(),
2755
2909
  height: z.z.number()
2756
2910
  }), z.z.object({
2757
- sha256: z.z.string(),
2758
2911
  mimeType: z.z.string()
2759
2912
  })]);
2760
2913
  const MetadataRes = z.z.object({
@@ -2826,7 +2979,9 @@ const CommitResponse = z.z.object({
2826
2979
  branch: z.z.string()
2827
2980
  });
2828
2981
  class ValOpsHttp extends ValOps {
2829
- constructor(hostUrl, project, commitSha, branch, apiKey, valModules, options) {
2982
+ constructor(hostUrl, project, commitSha,
2983
+ // TODO: CommitSha
2984
+ branch, apiKey, valModules, options) {
2830
2985
  super(valModules, options);
2831
2986
  this.hostUrl = hostUrl;
2832
2987
  this.project = project;
@@ -2840,7 +2995,150 @@ class ValOpsHttp extends ValOps {
2840
2995
  async onInit() {
2841
2996
  // TODO: unused for now. Implement or remove
2842
2997
  }
2998
+ async getStat(params) {
2999
+ if (!(params !== null && params !== void 0 && params.profileId)) {
3000
+ return {
3001
+ type: "error",
3002
+ error: {
3003
+ message: "No profileId provided"
3004
+ }
3005
+ };
3006
+ }
3007
+ const currentBaseSha = await this.getBaseSha();
3008
+ const currentSchemaSha = await this.getSchemaSha();
3009
+ const patchData = await this.fetchPatches({
3010
+ omitPatch: true,
3011
+ authors: undefined,
3012
+ patchIds: undefined,
3013
+ moduleFilePaths: undefined
3014
+ });
3015
+ const patches = [];
3016
+ // TODO: use proper patch sequences when available:
3017
+ for (const [patchId] of Object.entries(patchData.patches).sort(([, a], [, b]) => {
3018
+ return a.createdAt.localeCompare(b.createdAt, undefined);
3019
+ })) {
3020
+ patches.push(patchId);
3021
+ }
3022
+ const webSocketNonceRes = await this.getWebSocketNonce(params.profileId);
3023
+ if (webSocketNonceRes.status === "error") {
3024
+ return {
3025
+ type: "error",
3026
+ error: webSocketNonceRes.error
3027
+ };
3028
+ }
3029
+ const {
3030
+ nonce,
3031
+ url
3032
+ } = webSocketNonceRes.data;
3033
+ return {
3034
+ type: "use-websocket",
3035
+ url,
3036
+ nonce,
3037
+ baseSha: currentBaseSha,
3038
+ schemaSha: currentSchemaSha,
3039
+ patches,
3040
+ commitSha: this.commitSha
3041
+ };
3042
+ }
3043
+ async getWebSocketNonce(profileId) {
3044
+ return fetch(`${this.hostUrl}/v1/${this.project}/websocket/nonces`, {
3045
+ method: "POST",
3046
+ body: JSON.stringify({
3047
+ branch: this.branch,
3048
+ profileId
3049
+ }),
3050
+ headers: {
3051
+ ...this.authHeaders,
3052
+ "Content-Type": "application/json"
3053
+ }
3054
+ }).then(async res => {
3055
+ if (res.ok) {
3056
+ const json = await res.json();
3057
+ if (typeof json.nonce !== "string" || typeof json.url !== "string") {
3058
+ return {
3059
+ status: "error",
3060
+ error: {
3061
+ message: "Invalid nonce response: " + JSON.stringify(json)
3062
+ }
3063
+ };
3064
+ }
3065
+ if (!json.url.startsWith("ws://") && !json.url.startsWith("wss://")) {
3066
+ return {
3067
+ status: "error",
3068
+ error: {
3069
+ message: "Invalid websocket url: " + json.url
3070
+ }
3071
+ };
3072
+ }
3073
+ return {
3074
+ status: "success",
3075
+ data: {
3076
+ nonce: json.nonce,
3077
+ url: json.url
3078
+ }
3079
+ };
3080
+ }
3081
+ return {
3082
+ status: "error",
3083
+ error: {
3084
+ message: "Could not get nonce. HTTP error: " + res.status + " " + res.statusText
3085
+ }
3086
+ };
3087
+ }).catch(e => {
3088
+ console.error("Could not get nonce (connection error?):", e instanceof Error ? e.message : e.toString());
3089
+ return {
3090
+ status: "error",
3091
+ error: {
3092
+ message: "Could not get nonce. Error: " + (e instanceof Error ? e.message : e.toString())
3093
+ }
3094
+ };
3095
+ });
3096
+ }
2843
3097
  async fetchPatches(filters) {
3098
+ // Split patchIds into chunks to avoid too long query strings
3099
+ // NOTE: fetching patches results are cached, so this should reduce the pressure on the server
3100
+ const chunkSize = 100;
3101
+ const patchIds = filters.patchIds || [];
3102
+ const patchIdChunks = [];
3103
+ for (let i = 0; i < patchIds.length; i += chunkSize) {
3104
+ patchIdChunks.push(patchIds.slice(i, i + chunkSize));
3105
+ }
3106
+ let allPatches = {};
3107
+ let allErrors = {};
3108
+ if (patchIds === undefined || patchIds.length === 0) {
3109
+ return this.fetchPatchesInternal({
3110
+ patchIds: patchIds,
3111
+ authors: filters.authors,
3112
+ moduleFilePaths: filters.moduleFilePaths,
3113
+ omitPatch: filters.omitPatch
3114
+ });
3115
+ }
3116
+ for (const res of await Promise.all(patchIdChunks.map(patchIdChunk => this.fetchPatchesInternal({
3117
+ patchIds: patchIdChunk,
3118
+ authors: filters.authors,
3119
+ moduleFilePaths: filters.moduleFilePaths,
3120
+ omitPatch: filters.omitPatch
3121
+ })))) {
3122
+ if ("error" in res) {
3123
+ return res;
3124
+ }
3125
+ allPatches = {
3126
+ ...allPatches,
3127
+ ...res.patches
3128
+ };
3129
+ if (res.errors) {
3130
+ allErrors = {
3131
+ ...allErrors,
3132
+ ...res.errors
3133
+ };
3134
+ }
3135
+ }
3136
+ return {
3137
+ patches: allPatches,
3138
+ errors: Object.keys(allErrors).length > 0 ? allErrors : undefined
3139
+ };
3140
+ }
3141
+ async fetchPatchesInternal(filters) {
2844
3142
  const params = [];
2845
3143
  params.push(["branch", this.branch]);
2846
3144
  if (filters.patchIds) {
@@ -3089,7 +3387,7 @@ class ValOpsHttp extends ValOps {
3089
3387
  if (!file) {
3090
3388
  return null;
3091
3389
  }
3092
- return Buffer.from(file.value, "base64");
3390
+ return bufferFromDataUrl(file.value) ?? null;
3093
3391
  }
3094
3392
  async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
3095
3393
  const params = new URLSearchParams();
@@ -3197,6 +3495,7 @@ class ValOpsHttp extends ValOps {
3197
3495
  }
3198
3496
  async commit(prepared, message, committer, newBranch) {
3199
3497
  try {
3498
+ var _res$headers$get;
3200
3499
  const existingBranch = this.branch;
3201
3500
  const res = await fetch(`${this.hostUrl}/v1/${this.project}/commit`, {
3202
3501
  method: "POST",
@@ -3232,6 +3531,22 @@ class ValOpsHttp extends ValOps {
3232
3531
  }
3233
3532
  };
3234
3533
  }
3534
+ if ((_res$headers$get = res.headers.get("Content-Type")) !== null && _res$headers$get !== void 0 && _res$headers$get.includes("application/json")) {
3535
+ const json = await res.json();
3536
+ if (json.isNotFastForward) {
3537
+ return {
3538
+ isNotFastForward: true,
3539
+ error: {
3540
+ message: "Could not commit. Not a fast-forward commit"
3541
+ }
3542
+ };
3543
+ }
3544
+ return {
3545
+ error: {
3546
+ message: json.message
3547
+ }
3548
+ };
3549
+ }
3235
3550
  return {
3236
3551
  error: {
3237
3552
  message: "Could not commit. HTTP error: " + res.status + " " + res.statusText
@@ -3252,12 +3567,14 @@ const ValServer = (valModules, options, callbacks) => {
3252
3567
  let serverOps;
3253
3568
  if (options.mode === "fs") {
3254
3569
  serverOps = new ValOpsFS(options.cwd, valModules, {
3255
- formatter: options.formatter
3570
+ formatter: options.formatter,
3571
+ config: options.config
3256
3572
  });
3257
3573
  } else if (options.mode === "http") {
3258
3574
  serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
3259
3575
  formatter: options.formatter,
3260
- root: options.root
3576
+ root: options.root,
3577
+ config: options.config
3261
3578
  });
3262
3579
  } else {
3263
3580
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -3375,12 +3692,116 @@ const ValServer = (valModules, options, callbacks) => {
3375
3692
  };
3376
3693
  }
3377
3694
  };
3695
+ const authorize = async redirectTo => {
3696
+ const token = crypto.randomUUID();
3697
+ const redirectUrl = new URL(redirectTo);
3698
+ const appAuthorizeUrl = getAuthorizeUrl(`${redirectUrl.origin}/${options.route}`, token);
3699
+ await callbacks.onEnable(true);
3700
+ return {
3701
+ cookies: {
3702
+ [internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3703
+ [internal.VAL_STATE_COOKIE]: {
3704
+ value: createStateCookie({
3705
+ redirect_to: redirectTo,
3706
+ token
3707
+ }),
3708
+ options: {
3709
+ httpOnly: true,
3710
+ sameSite: "lax",
3711
+ expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
3712
+ }
3713
+ }
3714
+ },
3715
+ status: 302,
3716
+ redirectTo: appAuthorizeUrl
3717
+ };
3718
+ };
3378
3719
  return {
3379
- //#region auth
3720
+ "/draft/enable": {
3721
+ GET: async req => {
3722
+ const cookies = req.cookies;
3723
+ const auth = getAuth(cookies);
3724
+ if (auth.error) {
3725
+ return {
3726
+ status: 401,
3727
+ json: {
3728
+ message: auth.error
3729
+ }
3730
+ };
3731
+ }
3732
+ const query = req.query;
3733
+ const redirectToRes = getRedirectUrl(query, options.valEnableRedirectUrl);
3734
+ if (typeof redirectToRes !== "string") {
3735
+ return redirectToRes;
3736
+ }
3737
+ await callbacks.onEnable(true);
3738
+ return {
3739
+ status: 302,
3740
+ redirectTo: redirectToRes
3741
+ };
3742
+ }
3743
+ },
3744
+ "/draft/disable": {
3745
+ GET: async req => {
3746
+ const cookies = req.cookies;
3747
+ const auth = getAuth(cookies);
3748
+ if (auth.error) {
3749
+ return {
3750
+ status: 401,
3751
+ json: {
3752
+ message: auth.error
3753
+ }
3754
+ };
3755
+ }
3756
+ const query = req.query;
3757
+ const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
3758
+ if (typeof redirectToRes !== "string") {
3759
+ return redirectToRes;
3760
+ }
3761
+ await callbacks.onDisable(true);
3762
+ return {
3763
+ status: 302,
3764
+ redirectTo: redirectToRes
3765
+ };
3766
+ }
3767
+ },
3768
+ "/draft/stat": {
3769
+ GET: async req => {
3770
+ const cookies = req.cookies;
3771
+ const auth = getAuth(cookies);
3772
+ if (auth.error) {
3773
+ return {
3774
+ status: 401,
3775
+ json: {
3776
+ message: auth.error
3777
+ }
3778
+ };
3779
+ }
3780
+ return {
3781
+ status: 200,
3782
+ json: {
3783
+ draftMode: await callbacks.isEnabled()
3784
+ }
3785
+ };
3786
+ }
3787
+ },
3380
3788
  "/enable": {
3381
3789
  GET: async req => {
3790
+ const cookies = req.cookies;
3791
+ const auth = getAuth(cookies);
3382
3792
  const query = req.query;
3383
3793
  const redirectToRes = getRedirectUrl(query, options.valEnableRedirectUrl);
3794
+ if (auth.error) {
3795
+ if (typeof redirectToRes === "string") {
3796
+ return authorize(redirectToRes);
3797
+ }
3798
+ return {
3799
+ status: 401,
3800
+ json: {
3801
+ message: auth.error
3802
+ }
3803
+ };
3804
+ }
3384
3805
  if (typeof redirectToRes !== "string") {
3385
3806
  return redirectToRes;
3386
3807
  }
@@ -3396,6 +3817,16 @@ const ValServer = (valModules, options, callbacks) => {
3396
3817
  },
3397
3818
  "/disable": {
3398
3819
  GET: async req => {
3820
+ const cookies = req.cookies;
3821
+ const auth = getAuth(cookies);
3822
+ if (auth.error) {
3823
+ return {
3824
+ status: 401,
3825
+ json: {
3826
+ message: auth.error
3827
+ }
3828
+ };
3829
+ }
3399
3830
  const query = req.query;
3400
3831
  const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
3401
3832
  if (typeof redirectToRes !== "string") {
@@ -3413,6 +3844,7 @@ const ValServer = (valModules, options, callbacks) => {
3413
3844
  };
3414
3845
  }
3415
3846
  },
3847
+ //#region auth
3416
3848
  "/authorize": {
3417
3849
  GET: async req => {
3418
3850
  const query = req.query;
@@ -3424,28 +3856,8 @@ const ValServer = (valModules, options, callbacks) => {
3424
3856
  }
3425
3857
  };
3426
3858
  }
3427
- const token = crypto.randomUUID();
3428
- const redirectUrl = new URL(query.redirect_to);
3429
- const appAuthorizeUrl = getAuthorizeUrl(`${redirectUrl.origin}/${options.route}`, token);
3430
- await callbacks.onEnable(true);
3431
- return {
3432
- cookies: {
3433
- [internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3434
- [internal.VAL_STATE_COOKIE]: {
3435
- value: createStateCookie({
3436
- redirect_to: query.redirect_to,
3437
- token
3438
- }),
3439
- options: {
3440
- httpOnly: true,
3441
- sameSite: "lax",
3442
- expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
3443
- }
3444
- }
3445
- },
3446
- status: 302,
3447
- redirectTo: appAuthorizeUrl
3448
- };
3859
+ const redirectTo = query.redirect_to;
3860
+ return authorize(redirectTo);
3449
3861
  }
3450
3862
  },
3451
3863
  "/callback": {
@@ -3617,6 +4029,46 @@ const ValServer = (valModules, options, callbacks) => {
3617
4029
  };
3618
4030
  }
3619
4031
  },
4032
+ //#region stat
4033
+ "/stat": {
4034
+ POST: async req => {
4035
+ const cookies = req.cookies;
4036
+ const auth = getAuth(cookies);
4037
+ if (auth.error) {
4038
+ return {
4039
+ status: 401,
4040
+ json: {
4041
+ message: auth.error
4042
+ }
4043
+ };
4044
+ }
4045
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
4046
+ return {
4047
+ status: 401,
4048
+ json: {
4049
+ message: "Unauthorized"
4050
+ }
4051
+ };
4052
+ }
4053
+ const currentStat = await serverOps.getStat({
4054
+ ...req.body,
4055
+ profileId: "id" in auth ? auth.id : undefined
4056
+ });
4057
+ if (currentStat.type === "error") {
4058
+ return {
4059
+ status: 500,
4060
+ json: currentStat.error
4061
+ };
4062
+ }
4063
+ return {
4064
+ status: 200,
4065
+ json: {
4066
+ ...currentStat,
4067
+ config: options.config
4068
+ }
4069
+ };
4070
+ }
4071
+ },
3620
4072
  //#region patches
3621
4073
  "/patches/~": {
3622
4074
  GET: async req => {
@@ -3711,7 +4163,7 @@ const ValServer = (valModules, options, callbacks) => {
3711
4163
  };
3712
4164
  }
3713
4165
  },
3714
- //#region tree ops
4166
+ //#region schema
3715
4167
  "/schema": {
3716
4168
  GET: async req => {
3717
4169
  const cookies = req.cookies;
@@ -3738,7 +4190,7 @@ const ValServer = (valModules, options, callbacks) => {
3738
4190
  return {
3739
4191
  status: 500,
3740
4192
  json: {
3741
- message: "Val is not correctly setup. Check the val.modules file",
4193
+ message: `Got errors while fetching modules: ${moduleErrors.filter(error => error).map(error => error.message).join(", ")}`,
3742
4194
  details: moduleErrors
3743
4195
  }
3744
4196
  };
@@ -3746,9 +4198,22 @@ const ValServer = (valModules, options, callbacks) => {
3746
4198
  const schemaSha = await serverOps.getSchemaSha();
3747
4199
  const schemas = await serverOps.getSchemas();
3748
4200
  const serializedSchemas = {};
3749
- for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
3750
- const moduleFilePath = moduleFilePathS;
3751
- serializedSchemas[moduleFilePath] = schema.serialize();
4201
+ try {
4202
+ for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
4203
+ const moduleFilePath = moduleFilePathS;
4204
+ serializedSchemas[moduleFilePath] = schema.serialize();
4205
+ }
4206
+ } catch (e) {
4207
+ console.error("Val: Failed to serialize schemas", e);
4208
+ return {
4209
+ status: 500,
4210
+ json: {
4211
+ message: "Failed to serialize schemas",
4212
+ details: [{
4213
+ message: e instanceof Error ? e.message : JSON.stringify(e)
4214
+ }]
4215
+ }
4216
+ };
3752
4217
  }
3753
4218
  return {
3754
4219
  status: 200,
@@ -3759,7 +4224,8 @@ const ValServer = (valModules, options, callbacks) => {
3759
4224
  };
3760
4225
  }
3761
4226
  },
3762
- "/tree/~": {
4227
+ // #region sources
4228
+ "/sources": {
3763
4229
  PUT: async req => {
3764
4230
  var _body$patchIds;
3765
4231
  const query = req.query;
@@ -3796,8 +4262,8 @@ const ValServer = (valModules, options, callbacks) => {
3796
4262
  }
3797
4263
  let tree;
3798
4264
  let patchAnalysis = null;
3799
- let newPatchId = undefined;
3800
- 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.addPatch) {
4265
+ let newPatchIds = undefined;
4266
+ 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) {
3801
4267
  // TODO: validate patches_sha
3802
4268
  const patchIds = body === null || body === void 0 ? void 0 : body.patchIds;
3803
4269
  const patchOps = patchIds && patchIds.length > 0 ? await serverOps.fetchPatches({
@@ -3806,6 +4272,15 @@ const ValServer = (valModules, options, callbacks) => {
3806
4272
  }) : {
3807
4273
  patches: {}
3808
4274
  };
4275
+ if (patchOps.error) {
4276
+ return {
4277
+ status: 400,
4278
+ json: {
4279
+ message: "Failed to fetch patches: " + patchOps.error.message,
4280
+ details: []
4281
+ }
4282
+ };
4283
+ }
3809
4284
  let patchErrors = undefined;
3810
4285
  for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
3811
4286
  const patchId = patchIdS;
@@ -3816,45 +4291,43 @@ const ValServer = (valModules, options, callbacks) => {
3816
4291
  message: error.message
3817
4292
  };
3818
4293
  }
3819
- if (body !== null && body !== void 0 && body.addPatch) {
3820
- const newPatchModuleFilePath = body.addPatch.path;
3821
- const newPatchOps = body.addPatch.patch;
3822
- const authorId = "id" in auth ? auth.id : null;
3823
- const createPatchRes = await serverOps.createPatch(newPatchModuleFilePath, newPatchOps, authorId);
3824
- if (createPatchRes.error) {
3825
- return {
3826
- status: 500,
3827
- json: {
3828
- message: "Failed to create patch: " + createPatchRes.error.message,
3829
- details: createPatchRes.error
3830
- }
4294
+ // TODO: errors
4295
+ patchAnalysis = serverOps.analyzePatches(patchOps.patches);
4296
+ if (body !== null && body !== void 0 && body.addPatches) {
4297
+ for (const addPatch of body.addPatches) {
4298
+ const newPatchModuleFilePath = addPatch.path;
4299
+ const newPatchOps = addPatch.patch;
4300
+ const authorId = "id" in auth ? auth.id : null;
4301
+ const createPatchRes = await serverOps.createPatch(newPatchModuleFilePath, {
4302
+ ...patchAnalysis,
4303
+ ...patchOps
4304
+ }, newPatchOps, authorId);
4305
+ if (createPatchRes.error) {
4306
+ return {
4307
+ status: 500,
4308
+ json: {
4309
+ message: "Failed to create patch: " + createPatchRes.error.message,
4310
+ details: createPatchRes.error
4311
+ }
4312
+ };
4313
+ }
4314
+ if (!newPatchIds) {
4315
+ newPatchIds = [createPatchRes.patchId];
4316
+ } else {
4317
+ newPatchIds.push(createPatchRes.patchId);
4318
+ }
4319
+ patchOps.patches[createPatchRes.patchId] = {
4320
+ path: newPatchModuleFilePath,
4321
+ patch: newPatchOps,
4322
+ authorId,
4323
+ createdAt: createPatchRes.createdAt,
4324
+ appliedAt: null
3831
4325
  };
4326
+ patchAnalysis.patchesByModule[newPatchModuleFilePath] = [...(patchAnalysis.patchesByModule[newPatchModuleFilePath] || []), {
4327
+ patchId: createPatchRes.patchId
4328
+ }];
3832
4329
  }
3833
- // TODO: evaluate if we need this: seems wrong to delete patches that are not applied
3834
- // for (const fileRes of createPatchRes.files) {
3835
- // if (fileRes.error) {
3836
- // // clean up broken patch:
3837
- // await this.serverOps.deletePatches([createPatchRes.patchId]);
3838
- // return {
3839
- // status: 500,
3840
- // json: {
3841
- // message: "Failed to create patch",
3842
- // details: fileRes.error,
3843
- // },
3844
- // };
3845
- // }
3846
- // }
3847
- newPatchId = createPatchRes.patchId;
3848
- patchOps.patches[createPatchRes.patchId] = {
3849
- path: newPatchModuleFilePath,
3850
- patch: newPatchOps,
3851
- authorId,
3852
- createdAt: createPatchRes.createdAt,
3853
- appliedAt: null
3854
- };
3855
4330
  }
3856
- // TODO: errors
3857
- patchAnalysis = serverOps.analyzePatches(patchOps.patches);
3858
4331
  tree = {
3859
4332
  ...(await serverOps.getTree({
3860
4333
  ...patchAnalysis,
@@ -3877,27 +4350,13 @@ const ValServer = (valModules, options, callbacks) => {
3877
4350
  } else {
3878
4351
  tree = await serverOps.getTree();
3879
4352
  }
3880
- if (tree.errors && Object.keys(tree.errors).length > 0) {
3881
- console.error("Val: Failed to get tree", JSON.stringify(tree.errors));
3882
- const res = {
3883
- status: 400,
3884
- json: {
3885
- type: "patch-error",
3886
- errors: Object.fromEntries(Object.entries(tree.errors).map(([key, value]) => [key, value.map(error => ({
3887
- patchId: error.patchId,
3888
- skipped: error.skipped,
3889
- error: {
3890
- message: error.error.message
3891
- }
3892
- }))])),
3893
- message: "One or more patches failed to be applied"
3894
- }
3895
- };
3896
- return res;
3897
- }
4353
+ let sourcesValidation = {
4354
+ errors: {},
4355
+ files: {}
4356
+ };
3898
4357
  if (query.validate_sources || query.validate_binary_files) {
3899
4358
  const schemas = await serverOps.getSchemas();
3900
- const sourcesValidation = await serverOps.validateSources(schemas, tree.sources);
4359
+ sourcesValidation = await serverOps.validateSources(schemas, tree.sources);
3901
4360
 
3902
4361
  // TODO: send validation errors
3903
4362
  if (query.validate_binary_files) {
@@ -3909,20 +4368,41 @@ const ValServer = (valModules, options, callbacks) => {
3909
4368
  for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
3910
4369
  const moduleFilePath = moduleFilePathS;
3911
4370
  if (moduleFilePath.startsWith(treePath)) {
4371
+ var _sourcesValidation$er;
3912
4372
  modules[moduleFilePath] = {
3913
4373
  source: module,
3914
4374
  patches: patchAnalysis && patchAnalysis.patchesByModule[moduleFilePath] ? {
3915
4375
  applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
3916
- } : undefined
4376
+ } : undefined,
4377
+ validationErrors: (_sourcesValidation$er = sourcesValidation.errors[moduleFilePath]) === null || _sourcesValidation$er === void 0 ? void 0 : _sourcesValidation$er.validations
3917
4378
  };
3918
4379
  }
3919
4380
  }
4381
+ if (tree.errors && Object.keys(tree.errors).length > 0) {
4382
+ const res = {
4383
+ status: 400,
4384
+ json: {
4385
+ type: "patch-error",
4386
+ schemaSha,
4387
+ modules,
4388
+ errors: Object.fromEntries(Object.entries(tree.errors).map(([key, value]) => [key, value.map(error => ({
4389
+ patchId: error.patchId,
4390
+ skipped: error.skipped,
4391
+ error: {
4392
+ message: error.error.message
4393
+ }
4394
+ }))])),
4395
+ message: "One or more patches failed to be applied"
4396
+ }
4397
+ };
4398
+ return res;
4399
+ }
3920
4400
  const res = {
3921
4401
  status: 200,
3922
4402
  json: {
3923
4403
  schemaSha,
3924
4404
  modules,
3925
- newPatchId
4405
+ newPatchIds
3926
4406
  }
3927
4407
  };
3928
4408
  return res;
@@ -3968,10 +4448,10 @@ const ValServer = (valModules, options, callbacks) => {
3968
4448
  ...patches
3969
4449
  });
3970
4450
  if (preparedCommit.hasErrors) {
3971
- console.error("Failed to create commit", {
4451
+ console.error("Failed to create commit", JSON.stringify({
3972
4452
  sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
3973
4453
  binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
3974
- });
4454
+ }, null, 2));
3975
4455
  return {
3976
4456
  status: 400,
3977
4457
  json: {
@@ -3987,13 +4467,34 @@ const ValServer = (valModules, options, callbacks) => {
3987
4467
  }
3988
4468
  if (serverOps instanceof ValOpsFS) {
3989
4469
  await serverOps.saveFiles(preparedCommit);
4470
+ await serverOps.deletePatches(patchIds);
3990
4471
  return {
3991
4472
  status: 200,
3992
4473
  json: {} // TODO:
3993
4474
  };
3994
4475
  } else if (serverOps instanceof ValOpsHttp) {
3995
4476
  if (auth.error === undefined && auth.id) {
3996
- await serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
4477
+ const commitRes = await serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
4478
+ if (commitRes.error) {
4479
+ console.error("Failed to commit", commitRes.error);
4480
+ if ("isNotFastForward" in commitRes && commitRes.isNotFastForward) {
4481
+ return {
4482
+ status: 409,
4483
+ json: {
4484
+ isNotFastForward: true,
4485
+ message: "Cannot commit: this is not the latest version of this branch"
4486
+ }
4487
+ };
4488
+ }
4489
+ return {
4490
+ status: 400,
4491
+ json: {
4492
+ message: commitRes.error.message,
4493
+ details: []
4494
+ }
4495
+ };
4496
+ }
4497
+ // TODO: serverOps.markApplied(patchIds);
3997
4498
  return {
3998
4499
  status: 200,
3999
4500
  json: {} // TODO:
@@ -4024,15 +4525,24 @@ const ValServer = (valModules, options, callbacks) => {
4024
4525
  // 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
4025
4526
  // Thus: attack surface + ease of attack + benefit = low probability of attack
4026
4527
  // If we couldn't argue that patch ids are secret enough, then this would be a problem.
4528
+ let cacheControl;
4027
4529
  let fileBuffer;
4530
+ let mimeType;
4028
4531
  if (query.patch_id) {
4029
4532
  fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
4533
+ mimeType = core.Internal.filenameToMimeType(filePath);
4534
+ cacheControl = "public, max-age=20000, immutable";
4030
4535
  } else {
4031
4536
  fileBuffer = await serverOps.getBinaryFile(filePath);
4032
4537
  }
4033
4538
  if (fileBuffer) {
4034
4539
  return {
4035
4540
  status: 200,
4541
+ headers: {
4542
+ // TODO: we could use ETag and return 304 instead
4543
+ "Content-Type": mimeType || "application/octet-stream",
4544
+ "Cache-Control": cacheControl || "public, max-age=0, must-revalidate"
4545
+ },
4036
4546
  body: bufferToReadableStream(fileBuffer)
4037
4547
  };
4038
4548
  } else {
@@ -4352,14 +4862,14 @@ function guessMimeTypeFromPath(filePath) {
4352
4862
  return null;
4353
4863
  }
4354
4864
 
4355
- async function createValServer(valModules, route, opts, callbacks, formatter) {
4356
- const valServerConfig = await initHandlerOptions(route, opts);
4865
+ async function createValServer(valModules, route, opts, config, callbacks, formatter) {
4866
+ const valServerConfig = await initHandlerOptions(route, opts, config);
4357
4867
  return ValServer(valModules, {
4358
4868
  formatter,
4359
4869
  ...valServerConfig
4360
4870
  }, callbacks);
4361
4871
  }
4362
- async function initHandlerOptions(route, opts) {
4872
+ async function initHandlerOptions(route, opts, config) {
4363
4873
  const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
4364
4874
  const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
4365
4875
  const isProxyMode = opts.mode === "proxy" || opts.mode === undefined && (maybeApiKey || maybeValSecret);
@@ -4404,7 +4914,8 @@ async function initHandlerOptions(route, opts) {
4404
4914
  valEnableRedirectUrl,
4405
4915
  valDisableRedirectUrl,
4406
4916
  valContentUrl,
4407
- valBuildUrl
4917
+ valBuildUrl,
4918
+ config
4408
4919
  };
4409
4920
  } else {
4410
4921
  const cwd = process.cwd();
@@ -4418,7 +4929,8 @@ async function initHandlerOptions(route, opts) {
4418
4929
  valBuildUrl,
4419
4930
  apiKey: maybeApiKey,
4420
4931
  valSecret: maybeValSecret,
4421
- project: maybeValProject
4932
+ project: maybeValProject,
4933
+ config
4422
4934
  };
4423
4935
  }
4424
4936
  }
@@ -4587,12 +5099,25 @@ function createValApiRouter(route, valServerPromise, convert) {
4587
5099
  }
4588
5100
  };
4589
5101
  }
4590
- const bodyRes = reqDefinition.body ? reqDefinition.body.safeParse(await req.json()) : {
4591
- success: true,
4592
- data: {}
4593
- };
4594
- if (!bodyRes.success) {
4595
- return zodErrorResult(bodyRes.error, "invalid body data");
5102
+ let bodyRes;
5103
+ try {
5104
+ bodyRes = reqDefinition.body ? reqDefinition.body.safeParse(await req.json()) : {
5105
+ success: true,
5106
+ data: {}
5107
+ };
5108
+ if (!bodyRes.success) {
5109
+ return zodErrorResult(bodyRes.error, "invalid body data");
5110
+ }
5111
+ } catch (e) {
5112
+ return {
5113
+ status: 400,
5114
+ json: {
5115
+ message: "Could not parse request body",
5116
+ details: {
5117
+ error: JSON.stringify(e)
5118
+ }
5119
+ }
5120
+ };
4596
5121
  }
4597
5122
  const cookiesRes = reqDefinition.cookies ? getCookies(req, reqDefinition.cookies) : {
4598
5123
  success: true,
@@ -4623,7 +5148,7 @@ function createValApiRouter(route, valServerPromise, convert) {
4623
5148
  }
4624
5149
  // convert boolean to union of literals true and false so we can parse it as a string
4625
5150
  if (innerType instanceof z.z.ZodBoolean) {
4626
- innerType = z.z.union([z.z.literal("true"), z.z.literal("false")]).transform(arg => Boolean(arg));
5151
+ innerType = z.z.union([z.z.literal("true"), z.z.literal("false")]).transform(arg => arg === "true");
4627
5152
  }
4628
5153
  // re-build rules:
4629
5154
  let arrayCompatibleRule = innerType;
@@ -4648,6 +5173,15 @@ function createValApiRouter(route, valServerPromise, convert) {
4648
5173
  query,
4649
5174
  path
4650
5175
  });
5176
+ if (res.status === 500) {
5177
+ var _res$json;
5178
+ return {
5179
+ status: 500,
5180
+ json: {
5181
+ message: ((_res$json = res.json) === null || _res$json === void 0 ? void 0 : _res$json.message) || "Internal Server Error"
5182
+ }
5183
+ };
5184
+ }
4651
5185
  const resDef = apiEndpoint.res;
4652
5186
  if (resDef) {
4653
5187
  const responseResult = resDef.safeParse(res);
@@ -4744,6 +5278,49 @@ class ValFSHost {
4744
5278
  }
4745
5279
  }
4746
5280
 
5281
+ async function extractImageMetadata(filename, input) {
5282
+ const imageSize = sizeOf__default["default"](input);
5283
+ let mimeType = null;
5284
+ if (imageSize.type) {
5285
+ const possibleMimeType = `image/${imageSize.type}`;
5286
+ if (core.Internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
5287
+ mimeType = possibleMimeType;
5288
+ }
5289
+ const filenameBasedLookup = core.Internal.filenameToMimeType(filename);
5290
+ if (filenameBasedLookup) {
5291
+ mimeType = filenameBasedLookup;
5292
+ }
5293
+ }
5294
+ if (!mimeType) {
5295
+ mimeType = "application/octet-stream";
5296
+ }
5297
+ let {
5298
+ width,
5299
+ height
5300
+ } = imageSize;
5301
+ if (!width || !height) {
5302
+ width = 0;
5303
+ height = 0;
5304
+ }
5305
+ return {
5306
+ width,
5307
+ height,
5308
+ mimeType
5309
+ };
5310
+ }
5311
+ async function extractFileMetadata(filename,
5312
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5313
+ _input // TODO: use buffer to determine mimetype
5314
+ ) {
5315
+ let mimeType = core.Internal.filenameToMimeType(filename);
5316
+ if (!mimeType) {
5317
+ mimeType = "application/octet-stream";
5318
+ }
5319
+ return {
5320
+ mimeType
5321
+ };
5322
+ }
5323
+
4747
5324
  function getValidationErrorFileRef(validationError) {
4748
5325
  const maybeRef = validationError.value && typeof validationError.value === "object" && core.FILE_REF_PROP in validationError.value && typeof validationError.value[core.FILE_REF_PROP] === "string" ? validationError.value[core.FILE_REF_PROP] : undefined;
4749
5326
  if (!maybeRef) {
@@ -4771,15 +5348,15 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4771
5348
  throw Error("Cannot fix file without a file reference");
4772
5349
  }
4773
5350
  const filename = fsPath__namespace["default"].join(config.projectRoot, fileRef);
4774
- const buffer = fs__default["default"].readFileSync(filename);
4775
- return extractFileMetadata(fileRef, buffer);
5351
+ fs__default["default"].readFileSync(filename);
5352
+ return extractFileMetadata(fileRef);
4776
5353
  }
4777
5354
  const remainingErrors = [];
4778
5355
  const patch$1 = [];
4779
5356
  for (const fix of validationError.fixes || []) {
4780
5357
  if (fix === "image:replace-metadata" || fix === "image:add-metadata") {
4781
5358
  const imageMetadata = await getImageMetadata();
4782
- if (imageMetadata.width === undefined || imageMetadata.height === undefined || imageMetadata.sha256 === undefined) {
5359
+ if (imageMetadata.width === undefined || imageMetadata.height === undefined) {
4783
5360
  remainingErrors.push({
4784
5361
  ...validationError,
4785
5362
  message: "Failed to get image metadata",
@@ -4790,8 +5367,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4790
5367
  const metadataIsCorrect =
4791
5368
  // metadata is a prop that is an object
4792
5369
  typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
4793
- // sha256 is correct
4794
- "sha256" in currentValue.metadata && currentValue.metadata.sha256 === imageMetadata.sha256 &&
4795
5370
  // width is correct
4796
5371
  "width" in currentValue.metadata && currentValue.metadata.width === imageMetadata.width &&
4797
5372
  // height is correct
@@ -4808,18 +5383,11 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4808
5383
  value: {
4809
5384
  width: imageMetadata.width,
4810
5385
  height: imageMetadata.height,
4811
- sha256: imageMetadata.sha256,
4812
5386
  mimeType: imageMetadata.mimeType
4813
5387
  }
4814
5388
  });
4815
5389
  } else {
4816
5390
  if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
4817
- if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== imageMetadata.sha256) {
4818
- remainingErrors.push({
4819
- message: "Image metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + imageMetadata.sha256 + ".",
4820
- fixes: undefined
4821
- });
4822
- }
4823
5391
  if (!("width" in currentValue.metadata) || currentValue.metadata.width !== imageMetadata.width) {
4824
5392
  remainingErrors.push({
4825
5393
  message: "Image metadata width is incorrect! Found: " + ("width" in currentValue.metadata ? currentValue.metadata.width : "<empty>") + ". Expected: " + imageMetadata.width,
@@ -4854,14 +5422,13 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4854
5422
  value: {
4855
5423
  width: imageMetadata.width,
4856
5424
  height: imageMetadata.height,
4857
- sha256: imageMetadata.sha256,
4858
5425
  mimeType: imageMetadata.mimeType
4859
5426
  }
4860
5427
  });
4861
5428
  }
4862
5429
  } else if (fix === "file:add-metadata" || fix === "file:check-metadata") {
4863
5430
  const fileMetadata = await getFileMetadata();
4864
- if (fileMetadata.sha256 === undefined) {
5431
+ if (fileMetadata === undefined) {
4865
5432
  remainingErrors.push({
4866
5433
  ...validationError,
4867
5434
  message: "Failed to get image metadata",
@@ -4872,8 +5439,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4872
5439
  const metadataIsCorrect =
4873
5440
  // metadata is a prop that is an object
4874
5441
  typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
4875
- // sha256 is correct
4876
- "sha256" in currentValue.metadata && currentValue.metadata.sha256 === fileMetadata.sha256 &&
4877
5442
  // mimeType is correct
4878
5443
  "mimeType" in currentValue.metadata && currentValue.metadata.mimeType === fileMetadata.mimeType;
4879
5444
 
@@ -4884,7 +5449,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4884
5449
  op: "replace",
4885
5450
  path: patch.sourceToPatchPath(sourcePath).concat("metadata"),
4886
5451
  value: {
4887
- sha256: fileMetadata.sha256,
4888
5452
  ...(fileMetadata.mimeType ? {
4889
5453
  mimeType: fileMetadata.mimeType
4890
5454
  } : {})
@@ -4892,12 +5456,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4892
5456
  });
4893
5457
  } else {
4894
5458
  if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
4895
- if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== fileMetadata.sha256) {
4896
- remainingErrors.push({
4897
- message: "File metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + fileMetadata.sha256 + ".",
4898
- fixes: undefined
4899
- });
4900
- }
4901
5459
  if (!("mimeType" in currentValue.metadata) || currentValue.metadata.mimeType !== fileMetadata.mimeType) {
4902
5460
  remainingErrors.push({
4903
5461
  message: "File metadata mimeType is incorrect! Found: " + ("mimeType" in currentValue.metadata ? currentValue.metadata.mimeType : "<empty>") + ". Expected: " + fileMetadata.mimeType,
@@ -4918,7 +5476,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4918
5476
  op: "add",
4919
5477
  path: patch.sourceToPatchPath(sourcePath).concat("metadata"),
4920
5478
  value: {
4921
- sha256: fileMetadata.sha256,
4922
5479
  ...(fileMetadata.mimeType ? {
4923
5480
  mimeType: fileMetadata.mimeType
4924
5481
  } : {})