@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.
@@ -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 (Internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
1253
- mimeType = possibleMimeType;
1254
- }
1255
- const filenameBasedLookup = Internal.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 = Internal.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;
@@ -1592,25 +1555,27 @@ class ValOps {
1592
1555
  }
1593
1556
  for (const [sourcePathS, validationErrors] of Object.entries(res)) {
1594
1557
  const sourcePath = sourcePathS;
1595
- for (const validationError of validationErrors) {
1596
- if (isOnlyFileCheckValidationError(validationError)) {
1597
- if (files[sourcePath]) {
1598
- throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
1599
- }
1600
- const value = validationError.value;
1601
- if (isFileSource(value)) {
1602
- files[sourcePath] = value;
1603
- }
1604
- } else {
1605
- if (!errors[path]) {
1606
- errors[path] = {
1607
- validations: {}
1608
- };
1609
- }
1610
- if (!errors[path].validations[sourcePath]) {
1611
- 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);
1612
1578
  }
1613
- errors[path].validations[sourcePath].push(validationError);
1614
1579
  }
1615
1580
  }
1616
1581
  }
@@ -1798,8 +1763,22 @@ class ValOps {
1798
1763
  const patchRes = applyPatch(tsSourceFile, tsOps, sourceFileOps);
1799
1764
  if (result.isErr(patchRes)) {
1800
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
+ }
1801
1774
  errors.push(...patchRes.error);
1802
1775
  } else {
1776
+ console.error("Could not patch", JSON.stringify({
1777
+ path,
1778
+ patchId,
1779
+ error: patchRes.error,
1780
+ sourceFileOps
1781
+ }, null, 2));
1803
1782
  errors.push(patchRes.error);
1804
1783
  }
1805
1784
  triedPatches.push(patchId);
@@ -1893,16 +1872,23 @@ class ValOps {
1893
1872
  }
1894
1873
 
1895
1874
  // #region createPatch
1896
- async createPatch(path, patch, authorId) {
1897
- const {
1898
- sources,
1899
- schemas,
1900
- moduleErrors
1901
- } = 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
+ }
1902
1887
  const source = sources[path];
1903
1888
  const schema = schemas[path];
1904
1889
  const moduleError = moduleErrors.find(e => e.path === path);
1905
1890
  if (moduleError) {
1891
+ console.error(`Cannot patch. Module at path: '${path}' has fatal errors: "${moduleError.message}"`);
1906
1892
  return {
1907
1893
  error: {
1908
1894
  message: `Cannot patch. Module at path: '${path}' has fatal errors: ` + moduleErrors.map(m => `"${m.message}"`).join(" and ")
@@ -1910,6 +1896,7 @@ class ValOps {
1910
1896
  };
1911
1897
  }
1912
1898
  if (!source) {
1899
+ console.error(`Cannot patch. Module source at path: '${path}' does not exist`);
1913
1900
  return {
1914
1901
  error: {
1915
1902
  message: `Cannot patch. Module source at path: '${path}' does not exist`
@@ -1917,6 +1904,7 @@ class ValOps {
1917
1904
  };
1918
1905
  }
1919
1906
  if (!schema) {
1907
+ console.error(`Cannot patch. Module schema at path: '${path}' does not exist`);
1920
1908
  return {
1921
1909
  error: {
1922
1910
  message: `Cannot patch. Module schema at path: '${path}' does not exist`
@@ -1934,10 +1922,12 @@ class ValOps {
1934
1922
  filePath
1935
1923
  } = op;
1936
1924
  if (files[filePath]) {
1925
+ console.error(`Cannot have multiple files with same path in same patch. Path: ${filePath}`);
1937
1926
  files[filePath] = {
1938
1927
  error: new PatchError("Cannot have multiple files with same path in same patch")
1939
1928
  };
1940
1929
  } else if (typeof value !== "string") {
1930
+ console.error(`Value is not a string. Path: ${filePath}. Value: ${value}`);
1941
1931
  files[filePath] = {
1942
1932
  error: new PatchError("Value is not a string")
1943
1933
  };
@@ -1953,15 +1943,14 @@ class ValOps {
1953
1943
  path: op.path,
1954
1944
  filePath,
1955
1945
  nestedFilePath: op.nestedFilePath,
1956
- value: {
1957
- sha256
1958
- }
1946
+ value: sha256
1959
1947
  });
1960
1948
  }
1961
1949
  }
1962
1950
  }
1963
1951
  const saveRes = await this.saveSourceFilePatch(path, sourceFileOps, authorId);
1964
1952
  if (saveRes.error) {
1953
+ console.error(`Could not save source file patch at path: '${path}'. Error: ${saveRes.error.message}`);
1965
1954
  return {
1966
1955
  error: saveRes.error
1967
1956
  };
@@ -1985,17 +1974,20 @@ class ValOps {
1985
1974
  ? "image" : schemaAtPath instanceof FileSchema ? "file" : schemaAtPath.serialize().type;
1986
1975
  } catch (e) {
1987
1976
  if (e instanceof Error) {
1977
+ console.error(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`);
1988
1978
  return {
1989
1979
  filePath,
1990
1980
  error: new PatchError(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`)
1991
1981
  };
1992
1982
  }
1983
+ console.error(`Could not resolve file type at: ${modulePath}. Unknown error.`);
1993
1984
  return {
1994
1985
  filePath,
1995
1986
  error: new PatchError(`Could not resolve file type at: ${modulePath}. Unknown error.`)
1996
1987
  };
1997
1988
  }
1998
1989
  if (type !== "image" && type !== "file") {
1990
+ console.error("Unknown file type (resolved from schema): " + type);
1999
1991
  return {
2000
1992
  filePath,
2001
1993
  error: new PatchError("Unknown file type (resolved from schema): " + type)
@@ -2003,6 +1995,7 @@ class ValOps {
2003
1995
  }
2004
1996
  const mimeType = getMimeTypeFromBase64(data.value);
2005
1997
  if (!mimeType) {
1998
+ console.error("Could not get mimeType from base 64 encoded value");
2006
1999
  return {
2007
2000
  filePath,
2008
2001
  error: new PatchError("Could not get mimeType from base 64 encoded value. First chars were: " + data.value.slice(0, 20))
@@ -2010,6 +2003,7 @@ class ValOps {
2010
2003
  }
2011
2004
  const buffer = bufferFromDataUrl(data.value);
2012
2005
  if (!buffer) {
2006
+ console.error("Could not create buffer from base 64 encoded value");
2013
2007
  return {
2014
2008
  filePath,
2015
2009
  error: new PatchError("Could not create buffer from base 64 encoded value")
@@ -2017,6 +2011,7 @@ class ValOps {
2017
2011
  }
2018
2012
  const metadataOps = createMetadataFromBuffer(type, mimeType, buffer);
2019
2013
  if (metadataOps.errors) {
2014
+ console.error(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`);
2020
2015
  return {
2021
2016
  filePath,
2022
2017
  error: new PatchError(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`)
@@ -2038,6 +2033,14 @@ class ValOps {
2038
2033
  };
2039
2034
  }
2040
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
+ }
2041
2044
  return {
2042
2045
  patchId,
2043
2046
  files: saveFileRes,
@@ -2062,14 +2065,13 @@ function isFileSource(value) {
2062
2065
  }
2063
2066
  function getFieldsForType(type) {
2064
2067
  if (type === "file") {
2065
- return ["sha256", "mimeType"];
2068
+ return ["mimeType"];
2066
2069
  } else if (type === "image") {
2067
- return ["sha256", "mimeType", "height", "width"];
2070
+ return ["mimeType", "height", "width"];
2068
2071
  }
2069
2072
  throw new Error("Unknown type: " + type);
2070
2073
  }
2071
2074
  function createMetadataFromBuffer(type, mimeType, buffer) {
2072
- const sha256 = getSha256(mimeType, buffer);
2073
2075
  const errors = [];
2074
2076
  let availableMetadata;
2075
2077
  if (type === "image") {
@@ -2078,7 +2080,7 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
2078
2080
  height,
2079
2081
  type
2080
2082
  } = sizeOf(buffer);
2081
- const normalizedType = type === "jpg" ? "jpeg" : type;
2083
+ const normalizedType = type === "jpg" ? "jpeg" : type === "svg" ? "svg+xml" : type;
2082
2084
  if (type !== undefined && `image/${normalizedType}` !== mimeType) {
2083
2085
  return {
2084
2086
  errors: [{
@@ -2087,14 +2089,12 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
2087
2089
  };
2088
2090
  }
2089
2091
  availableMetadata = {
2090
- sha256: sha256,
2091
2092
  mimeType,
2092
2093
  height,
2093
2094
  width
2094
2095
  };
2095
2096
  } else {
2096
2097
  availableMetadata = {
2097
- sha256: sha256,
2098
2098
  mimeType
2099
2099
  };
2100
2100
  }
@@ -2214,9 +2214,7 @@ const OperationT = z$1.discriminatedUnion("op", [z$1.object({
2214
2214
  path: z$1.array(z$1.string()),
2215
2215
  filePath: z$1.string(),
2216
2216
  nestedFilePath: z$1.array(z$1.string()).optional(),
2217
- value: z$1.union([z$1.string(), z$1.object({
2218
- sha256: z$1.string()
2219
- })])
2217
+ value: z$1.string()
2220
2218
  }).strict()]);
2221
2219
  const Patch = z$1.array(OperationT);
2222
2220
 
@@ -2230,6 +2228,161 @@ class ValOpsFS extends ValOps {
2230
2228
  async onInit() {
2231
2229
  // do nothing
2232
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
+ }
2233
2386
  async readPatches(includes) {
2234
2387
  const patchesCacheDir = this.getPatchesDir();
2235
2388
  let patchJsonFiles = [];
@@ -2238,8 +2391,8 @@ class ValOpsFS extends ValOps {
2238
2391
  }
2239
2392
  const patches = {};
2240
2393
  const errors = {};
2241
- const sortedPatchIds = patchJsonFiles.map(file => parseInt(fsPath__default.basename(fsPath__default.dirname(file)), 10)).sort();
2242
- 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) {
2243
2396
  if (Number.isNaN(patchIdNum)) {
2244
2397
  throw new Error("Could not parse patch id from file name. Files found: " + patchJsonFiles.join(", "));
2245
2398
  }
@@ -2280,6 +2433,12 @@ class ValOpsFS extends ValOps {
2280
2433
  errors: allErrors,
2281
2434
  patches: allPatches
2282
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
+ }
2283
2442
  for (const [patchIdS, patch] of Object.entries(allPatches)) {
2284
2443
  const patchId = patchIdS;
2285
2444
  if (filters.authors && !(patch.authorId === null || filters.authors.includes(patch.authorId))) {
@@ -2295,10 +2454,6 @@ class ValOpsFS extends ValOps {
2295
2454
  authorId: patch.authorId,
2296
2455
  appliedAt: patch.appliedAt
2297
2456
  };
2298
- const error = allErrors && allErrors[patchId];
2299
- if (error) {
2300
- errors[patchId] = error;
2301
- }
2302
2457
  }
2303
2458
  if (errors && Object.keys(errors).length > 0) {
2304
2459
  return {
@@ -2642,11 +2797,11 @@ class ValOpsFS extends ValOps {
2642
2797
  getPatchDir(patchId) {
2643
2798
  return fsPath__default.join(this.getPatchesDir(), patchId);
2644
2799
  }
2645
- getBinaryFilePath(filename, patchId) {
2646
- 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));
2647
2802
  }
2648
- getBinaryFileMetadataPath(filename, patchId) {
2649
- 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");
2650
2805
  }
2651
2806
  getPatchFilePath(patchId) {
2652
2807
  return fsPath__default.join(this.getPatchDir(patchId), "patch.json");
@@ -2719,12 +2874,10 @@ const BaseSha = z.string().refine(s => !!s); // TODO: validate
2719
2874
  const AuthorId = z.string().refine(s => !!s); // TODO: validate
2720
2875
  const ModuleFilePath = z.string().refine(s => !!s); // TODO: validate
2721
2876
  const Metadata = z.union([z.object({
2722
- sha256: z.string(),
2723
2877
  mimeType: z.string(),
2724
2878
  width: z.number(),
2725
2879
  height: z.number()
2726
2880
  }), z.object({
2727
- sha256: z.string(),
2728
2881
  mimeType: z.string()
2729
2882
  })]);
2730
2883
  const MetadataRes = z.object({
@@ -2796,7 +2949,9 @@ const CommitResponse = z.object({
2796
2949
  branch: z.string()
2797
2950
  });
2798
2951
  class ValOpsHttp extends ValOps {
2799
- constructor(hostUrl, project, commitSha, branch, apiKey, valModules, options) {
2952
+ constructor(hostUrl, project, commitSha,
2953
+ // TODO: CommitSha
2954
+ branch, apiKey, valModules, options) {
2800
2955
  super(valModules, options);
2801
2956
  this.hostUrl = hostUrl;
2802
2957
  this.project = project;
@@ -2810,7 +2965,150 @@ class ValOpsHttp extends ValOps {
2810
2965
  async onInit() {
2811
2966
  // TODO: unused for now. Implement or remove
2812
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
+ }
2813
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) {
2814
3112
  const params = [];
2815
3113
  params.push(["branch", this.branch]);
2816
3114
  if (filters.patchIds) {
@@ -3059,7 +3357,7 @@ class ValOpsHttp extends ValOps {
3059
3357
  if (!file) {
3060
3358
  return null;
3061
3359
  }
3062
- return Buffer.from(file.value, "base64");
3360
+ return bufferFromDataUrl(file.value) ?? null;
3063
3361
  }
3064
3362
  async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
3065
3363
  const params = new URLSearchParams();
@@ -3167,6 +3465,7 @@ class ValOpsHttp extends ValOps {
3167
3465
  }
3168
3466
  async commit(prepared, message, committer, newBranch) {
3169
3467
  try {
3468
+ var _res$headers$get;
3170
3469
  const existingBranch = this.branch;
3171
3470
  const res = await fetch(`${this.hostUrl}/v1/${this.project}/commit`, {
3172
3471
  method: "POST",
@@ -3202,6 +3501,22 @@ class ValOpsHttp extends ValOps {
3202
3501
  }
3203
3502
  };
3204
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
+ }
3205
3520
  return {
3206
3521
  error: {
3207
3522
  message: "Could not commit. HTTP error: " + res.status + " " + res.statusText
@@ -3222,12 +3537,14 @@ const ValServer = (valModules, options, callbacks) => {
3222
3537
  let serverOps;
3223
3538
  if (options.mode === "fs") {
3224
3539
  serverOps = new ValOpsFS(options.cwd, valModules, {
3225
- formatter: options.formatter
3540
+ formatter: options.formatter,
3541
+ config: options.config
3226
3542
  });
3227
3543
  } else if (options.mode === "http") {
3228
3544
  serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
3229
3545
  formatter: options.formatter,
3230
- root: options.root
3546
+ root: options.root,
3547
+ config: options.config
3231
3548
  });
3232
3549
  } else {
3233
3550
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -3345,12 +3662,116 @@ const ValServer = (valModules, options, callbacks) => {
3345
3662
  };
3346
3663
  }
3347
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);
3670
+ return {
3671
+ cookies: {
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
+ }
3683
+ }
3684
+ },
3685
+ status: 302,
3686
+ redirectTo: appAuthorizeUrl
3687
+ };
3688
+ };
3348
3689
  return {
3349
- //#region auth
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
+ };
3701
+ }
3702
+ const query = req.query;
3703
+ const redirectToRes = getRedirectUrl(query, options.valEnableRedirectUrl);
3704
+ if (typeof redirectToRes !== "string") {
3705
+ return redirectToRes;
3706
+ }
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
+ };
3725
+ }
3726
+ const query = req.query;
3727
+ const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
3728
+ if (typeof redirectToRes !== "string") {
3729
+ return redirectToRes;
3730
+ }
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
+ };
3749
+ }
3750
+ return {
3751
+ status: 200,
3752
+ json: {
3753
+ draftMode: await callbacks.isEnabled()
3754
+ }
3755
+ };
3756
+ }
3757
+ },
3350
3758
  "/enable": {
3351
3759
  GET: async req => {
3760
+ const cookies = req.cookies;
3761
+ const auth = getAuth(cookies);
3352
3762
  const query = req.query;
3353
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
+ };
3774
+ }
3354
3775
  if (typeof redirectToRes !== "string") {
3355
3776
  return redirectToRes;
3356
3777
  }
@@ -3366,6 +3787,16 @@ const ValServer = (valModules, options, callbacks) => {
3366
3787
  },
3367
3788
  "/disable": {
3368
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
+ };
3799
+ }
3369
3800
  const query = req.query;
3370
3801
  const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
3371
3802
  if (typeof redirectToRes !== "string") {
@@ -3383,6 +3814,7 @@ const ValServer = (valModules, options, callbacks) => {
3383
3814
  };
3384
3815
  }
3385
3816
  },
3817
+ //#region auth
3386
3818
  "/authorize": {
3387
3819
  GET: async req => {
3388
3820
  const query = req.query;
@@ -3394,28 +3826,8 @@ const ValServer = (valModules, options, callbacks) => {
3394
3826
  }
3395
3827
  };
3396
3828
  }
3397
- const token = crypto.randomUUID();
3398
- const redirectUrl = new URL(query.redirect_to);
3399
- const appAuthorizeUrl = getAuthorizeUrl(`${redirectUrl.origin}/${options.route}`, token);
3400
- await callbacks.onEnable(true);
3401
- return {
3402
- cookies: {
3403
- [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3404
- [VAL_STATE_COOKIE]: {
3405
- value: createStateCookie({
3406
- redirect_to: query.redirect_to,
3407
- token
3408
- }),
3409
- options: {
3410
- httpOnly: true,
3411
- sameSite: "lax",
3412
- expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
3413
- }
3414
- }
3415
- },
3416
- status: 302,
3417
- redirectTo: appAuthorizeUrl
3418
- };
3829
+ const redirectTo = query.redirect_to;
3830
+ return authorize(redirectTo);
3419
3831
  }
3420
3832
  },
3421
3833
  "/callback": {
@@ -3587,6 +3999,46 @@ const ValServer = (valModules, options, callbacks) => {
3587
3999
  };
3588
4000
  }
3589
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
+ },
3590
4042
  //#region patches
3591
4043
  "/patches/~": {
3592
4044
  GET: async req => {
@@ -3681,7 +4133,7 @@ const ValServer = (valModules, options, callbacks) => {
3681
4133
  };
3682
4134
  }
3683
4135
  },
3684
- //#region tree ops
4136
+ //#region schema
3685
4137
  "/schema": {
3686
4138
  GET: async req => {
3687
4139
  const cookies = req.cookies;
@@ -3708,7 +4160,7 @@ const ValServer = (valModules, options, callbacks) => {
3708
4160
  return {
3709
4161
  status: 500,
3710
4162
  json: {
3711
- message: "Val is not correctly setup. Check the val.modules file",
4163
+ message: `Got errors while fetching modules: ${moduleErrors.filter(error => error).map(error => error.message).join(", ")}`,
3712
4164
  details: moduleErrors
3713
4165
  }
3714
4166
  };
@@ -3716,9 +4168,22 @@ const ValServer = (valModules, options, callbacks) => {
3716
4168
  const schemaSha = await serverOps.getSchemaSha();
3717
4169
  const schemas = await serverOps.getSchemas();
3718
4170
  const serializedSchemas = {};
3719
- for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
3720
- const moduleFilePath = moduleFilePathS;
3721
- serializedSchemas[moduleFilePath] = schema.serialize();
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
+ };
3722
4187
  }
3723
4188
  return {
3724
4189
  status: 200,
@@ -3729,7 +4194,8 @@ const ValServer = (valModules, options, callbacks) => {
3729
4194
  };
3730
4195
  }
3731
4196
  },
3732
- "/tree/~": {
4197
+ // #region sources
4198
+ "/sources": {
3733
4199
  PUT: async req => {
3734
4200
  var _body$patchIds;
3735
4201
  const query = req.query;
@@ -3766,8 +4232,8 @@ const ValServer = (valModules, options, callbacks) => {
3766
4232
  }
3767
4233
  let tree;
3768
4234
  let patchAnalysis = null;
3769
- let newPatchId = undefined;
3770
- 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) {
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) {
3771
4237
  // TODO: validate patches_sha
3772
4238
  const patchIds = body === null || body === void 0 ? void 0 : body.patchIds;
3773
4239
  const patchOps = patchIds && patchIds.length > 0 ? await serverOps.fetchPatches({
@@ -3776,6 +4242,15 @@ const ValServer = (valModules, options, callbacks) => {
3776
4242
  }) : {
3777
4243
  patches: {}
3778
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
+ }
3779
4254
  let patchErrors = undefined;
3780
4255
  for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
3781
4256
  const patchId = patchIdS;
@@ -3786,45 +4261,43 @@ const ValServer = (valModules, options, callbacks) => {
3786
4261
  message: error.message
3787
4262
  };
3788
4263
  }
3789
- if (body !== null && body !== void 0 && body.addPatch) {
3790
- const newPatchModuleFilePath = body.addPatch.path;
3791
- const newPatchOps = body.addPatch.patch;
3792
- const authorId = "id" in auth ? auth.id : null;
3793
- const createPatchRes = await serverOps.createPatch(newPatchModuleFilePath, newPatchOps, authorId);
3794
- if (createPatchRes.error) {
3795
- return {
3796
- status: 500,
3797
- json: {
3798
- message: "Failed to create patch: " + createPatchRes.error.message,
3799
- details: createPatchRes.error
3800
- }
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
3801
4295
  };
4296
+ patchAnalysis.patchesByModule[newPatchModuleFilePath] = [...(patchAnalysis.patchesByModule[newPatchModuleFilePath] || []), {
4297
+ patchId: createPatchRes.patchId
4298
+ }];
3802
4299
  }
3803
- // TODO: evaluate if we need this: seems wrong to delete patches that are not applied
3804
- // for (const fileRes of createPatchRes.files) {
3805
- // if (fileRes.error) {
3806
- // // clean up broken patch:
3807
- // await this.serverOps.deletePatches([createPatchRes.patchId]);
3808
- // return {
3809
- // status: 500,
3810
- // json: {
3811
- // message: "Failed to create patch",
3812
- // details: fileRes.error,
3813
- // },
3814
- // };
3815
- // }
3816
- // }
3817
- newPatchId = createPatchRes.patchId;
3818
- patchOps.patches[createPatchRes.patchId] = {
3819
- path: newPatchModuleFilePath,
3820
- patch: newPatchOps,
3821
- authorId,
3822
- createdAt: createPatchRes.createdAt,
3823
- appliedAt: null
3824
- };
3825
4300
  }
3826
- // TODO: errors
3827
- patchAnalysis = serverOps.analyzePatches(patchOps.patches);
3828
4301
  tree = {
3829
4302
  ...(await serverOps.getTree({
3830
4303
  ...patchAnalysis,
@@ -3847,27 +4320,13 @@ const ValServer = (valModules, options, callbacks) => {
3847
4320
  } else {
3848
4321
  tree = await serverOps.getTree();
3849
4322
  }
3850
- if (tree.errors && Object.keys(tree.errors).length > 0) {
3851
- console.error("Val: Failed to get tree", JSON.stringify(tree.errors));
3852
- const res = {
3853
- status: 400,
3854
- json: {
3855
- type: "patch-error",
3856
- errors: Object.fromEntries(Object.entries(tree.errors).map(([key, value]) => [key, value.map(error => ({
3857
- patchId: error.patchId,
3858
- skipped: error.skipped,
3859
- error: {
3860
- message: error.error.message
3861
- }
3862
- }))])),
3863
- message: "One or more patches failed to be applied"
3864
- }
3865
- };
3866
- return res;
3867
- }
4323
+ let sourcesValidation = {
4324
+ errors: {},
4325
+ files: {}
4326
+ };
3868
4327
  if (query.validate_sources || query.validate_binary_files) {
3869
4328
  const schemas = await serverOps.getSchemas();
3870
- const sourcesValidation = await serverOps.validateSources(schemas, tree.sources);
4329
+ sourcesValidation = await serverOps.validateSources(schemas, tree.sources);
3871
4330
 
3872
4331
  // TODO: send validation errors
3873
4332
  if (query.validate_binary_files) {
@@ -3879,20 +4338,41 @@ const ValServer = (valModules, options, callbacks) => {
3879
4338
  for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
3880
4339
  const moduleFilePath = moduleFilePathS;
3881
4340
  if (moduleFilePath.startsWith(treePath)) {
4341
+ var _sourcesValidation$er;
3882
4342
  modules[moduleFilePath] = {
3883
4343
  source: module,
3884
4344
  patches: patchAnalysis && patchAnalysis.patchesByModule[moduleFilePath] ? {
3885
4345
  applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
3886
- } : undefined
4346
+ } : undefined,
4347
+ validationErrors: (_sourcesValidation$er = sourcesValidation.errors[moduleFilePath]) === null || _sourcesValidation$er === void 0 ? void 0 : _sourcesValidation$er.validations
3887
4348
  };
3888
4349
  }
3889
4350
  }
4351
+ if (tree.errors && Object.keys(tree.errors).length > 0) {
4352
+ const res = {
4353
+ status: 400,
4354
+ json: {
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"
4366
+ }
4367
+ };
4368
+ return res;
4369
+ }
3890
4370
  const res = {
3891
4371
  status: 200,
3892
4372
  json: {
3893
4373
  schemaSha,
3894
4374
  modules,
3895
- newPatchId
4375
+ newPatchIds
3896
4376
  }
3897
4377
  };
3898
4378
  return res;
@@ -3938,10 +4418,10 @@ const ValServer = (valModules, options, callbacks) => {
3938
4418
  ...patches
3939
4419
  });
3940
4420
  if (preparedCommit.hasErrors) {
3941
- console.error("Failed to create commit", {
4421
+ console.error("Failed to create commit", JSON.stringify({
3942
4422
  sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
3943
4423
  binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
3944
- });
4424
+ }, null, 2));
3945
4425
  return {
3946
4426
  status: 400,
3947
4427
  json: {
@@ -3957,13 +4437,34 @@ const ValServer = (valModules, options, callbacks) => {
3957
4437
  }
3958
4438
  if (serverOps instanceof ValOpsFS) {
3959
4439
  await serverOps.saveFiles(preparedCommit);
4440
+ await serverOps.deletePatches(patchIds);
3960
4441
  return {
3961
4442
  status: 200,
3962
4443
  json: {} // TODO:
3963
4444
  };
3964
4445
  } else if (serverOps instanceof ValOpsHttp) {
3965
4446
  if (auth.error === undefined && auth.id) {
3966
- await serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", 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);
3967
4468
  return {
3968
4469
  status: 200,
3969
4470
  json: {} // TODO:
@@ -3994,15 +4495,24 @@ const ValServer = (valModules, options, callbacks) => {
3994
4495
  // 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
3995
4496
  // Thus: attack surface + ease of attack + benefit = low probability of attack
3996
4497
  // If we couldn't argue that patch ids are secret enough, then this would be a problem.
4498
+ let cacheControl;
3997
4499
  let fileBuffer;
4500
+ let mimeType;
3998
4501
  if (query.patch_id) {
3999
4502
  fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
4503
+ mimeType = Internal.filenameToMimeType(filePath);
4504
+ cacheControl = "public, max-age=20000, immutable";
4000
4505
  } else {
4001
4506
  fileBuffer = await serverOps.getBinaryFile(filePath);
4002
4507
  }
4003
4508
  if (fileBuffer) {
4004
4509
  return {
4005
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
+ },
4006
4516
  body: bufferToReadableStream(fileBuffer)
4007
4517
  };
4008
4518
  } else {
@@ -4322,14 +4832,14 @@ function guessMimeTypeFromPath(filePath) {
4322
4832
  return null;
4323
4833
  }
4324
4834
 
4325
- async function createValServer(valModules, route, opts, callbacks, formatter) {
4326
- const valServerConfig = await initHandlerOptions(route, opts);
4835
+ async function createValServer(valModules, route, opts, config, callbacks, formatter) {
4836
+ const valServerConfig = await initHandlerOptions(route, opts, config);
4327
4837
  return ValServer(valModules, {
4328
4838
  formatter,
4329
4839
  ...valServerConfig
4330
4840
  }, callbacks);
4331
4841
  }
4332
- async function initHandlerOptions(route, opts) {
4842
+ async function initHandlerOptions(route, opts, config) {
4333
4843
  const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
4334
4844
  const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
4335
4845
  const isProxyMode = opts.mode === "proxy" || opts.mode === undefined && (maybeApiKey || maybeValSecret);
@@ -4374,7 +4884,8 @@ async function initHandlerOptions(route, opts) {
4374
4884
  valEnableRedirectUrl,
4375
4885
  valDisableRedirectUrl,
4376
4886
  valContentUrl,
4377
- valBuildUrl
4887
+ valBuildUrl,
4888
+ config
4378
4889
  };
4379
4890
  } else {
4380
4891
  const cwd = process.cwd();
@@ -4388,7 +4899,8 @@ async function initHandlerOptions(route, opts) {
4388
4899
  valBuildUrl,
4389
4900
  apiKey: maybeApiKey,
4390
4901
  valSecret: maybeValSecret,
4391
- project: maybeValProject
4902
+ project: maybeValProject,
4903
+ config
4392
4904
  };
4393
4905
  }
4394
4906
  }
@@ -4557,12 +5069,25 @@ function createValApiRouter(route, valServerPromise, convert) {
4557
5069
  }
4558
5070
  };
4559
5071
  }
4560
- const bodyRes = reqDefinition.body ? reqDefinition.body.safeParse(await req.json()) : {
4561
- success: true,
4562
- data: {}
4563
- };
4564
- if (!bodyRes.success) {
4565
- return zodErrorResult(bodyRes.error, "invalid body data");
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");
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
+ };
4566
5091
  }
4567
5092
  const cookiesRes = reqDefinition.cookies ? getCookies(req, reqDefinition.cookies) : {
4568
5093
  success: true,
@@ -4593,7 +5118,7 @@ function createValApiRouter(route, valServerPromise, convert) {
4593
5118
  }
4594
5119
  // convert boolean to union of literals true and false so we can parse it as a string
4595
5120
  if (innerType instanceof z.ZodBoolean) {
4596
- innerType = z.union([z.literal("true"), z.literal("false")]).transform(arg => Boolean(arg));
5121
+ innerType = z.union([z.literal("true"), z.literal("false")]).transform(arg => arg === "true");
4597
5122
  }
4598
5123
  // re-build rules:
4599
5124
  let arrayCompatibleRule = innerType;
@@ -4618,6 +5143,15 @@ function createValApiRouter(route, valServerPromise, convert) {
4618
5143
  query,
4619
5144
  path
4620
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
+ }
4621
5155
  const resDef = apiEndpoint.res;
4622
5156
  if (resDef) {
4623
5157
  const responseResult = resDef.safeParse(res);
@@ -4714,6 +5248,49 @@ class ValFSHost {
4714
5248
  }
4715
5249
  }
4716
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
+
4717
5294
  function getValidationErrorFileRef(validationError) {
4718
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;
4719
5296
  if (!maybeRef) {
@@ -4741,15 +5318,15 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4741
5318
  throw Error("Cannot fix file without a file reference");
4742
5319
  }
4743
5320
  const filename = fsPath__default.join(config.projectRoot, fileRef);
4744
- const buffer = fs.readFileSync(filename);
4745
- return extractFileMetadata(fileRef, buffer);
5321
+ fs.readFileSync(filename);
5322
+ return extractFileMetadata(fileRef);
4746
5323
  }
4747
5324
  const remainingErrors = [];
4748
5325
  const patch = [];
4749
5326
  for (const fix of validationError.fixes || []) {
4750
5327
  if (fix === "image:replace-metadata" || fix === "image:add-metadata") {
4751
5328
  const imageMetadata = await getImageMetadata();
4752
- if (imageMetadata.width === undefined || imageMetadata.height === undefined || imageMetadata.sha256 === undefined) {
5329
+ if (imageMetadata.width === undefined || imageMetadata.height === undefined) {
4753
5330
  remainingErrors.push({
4754
5331
  ...validationError,
4755
5332
  message: "Failed to get image metadata",
@@ -4760,8 +5337,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4760
5337
  const metadataIsCorrect =
4761
5338
  // metadata is a prop that is an object
4762
5339
  typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
4763
- // sha256 is correct
4764
- "sha256" in currentValue.metadata && currentValue.metadata.sha256 === imageMetadata.sha256 &&
4765
5340
  // width is correct
4766
5341
  "width" in currentValue.metadata && currentValue.metadata.width === imageMetadata.width &&
4767
5342
  // height is correct
@@ -4778,18 +5353,11 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4778
5353
  value: {
4779
5354
  width: imageMetadata.width,
4780
5355
  height: imageMetadata.height,
4781
- sha256: imageMetadata.sha256,
4782
5356
  mimeType: imageMetadata.mimeType
4783
5357
  }
4784
5358
  });
4785
5359
  } else {
4786
5360
  if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
4787
- if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== imageMetadata.sha256) {
4788
- remainingErrors.push({
4789
- message: "Image metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + imageMetadata.sha256 + ".",
4790
- fixes: undefined
4791
- });
4792
- }
4793
5361
  if (!("width" in currentValue.metadata) || currentValue.metadata.width !== imageMetadata.width) {
4794
5362
  remainingErrors.push({
4795
5363
  message: "Image metadata width is incorrect! Found: " + ("width" in currentValue.metadata ? currentValue.metadata.width : "<empty>") + ". Expected: " + imageMetadata.width,
@@ -4824,14 +5392,13 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4824
5392
  value: {
4825
5393
  width: imageMetadata.width,
4826
5394
  height: imageMetadata.height,
4827
- sha256: imageMetadata.sha256,
4828
5395
  mimeType: imageMetadata.mimeType
4829
5396
  }
4830
5397
  });
4831
5398
  }
4832
5399
  } else if (fix === "file:add-metadata" || fix === "file:check-metadata") {
4833
5400
  const fileMetadata = await getFileMetadata();
4834
- if (fileMetadata.sha256 === undefined) {
5401
+ if (fileMetadata === undefined) {
4835
5402
  remainingErrors.push({
4836
5403
  ...validationError,
4837
5404
  message: "Failed to get image metadata",
@@ -4842,8 +5409,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4842
5409
  const metadataIsCorrect =
4843
5410
  // metadata is a prop that is an object
4844
5411
  typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
4845
- // sha256 is correct
4846
- "sha256" in currentValue.metadata && currentValue.metadata.sha256 === fileMetadata.sha256 &&
4847
5412
  // mimeType is correct
4848
5413
  "mimeType" in currentValue.metadata && currentValue.metadata.mimeType === fileMetadata.mimeType;
4849
5414
 
@@ -4854,7 +5419,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4854
5419
  op: "replace",
4855
5420
  path: sourceToPatchPath(sourcePath).concat("metadata"),
4856
5421
  value: {
4857
- sha256: fileMetadata.sha256,
4858
5422
  ...(fileMetadata.mimeType ? {
4859
5423
  mimeType: fileMetadata.mimeType
4860
5424
  } : {})
@@ -4862,12 +5426,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4862
5426
  });
4863
5427
  } else {
4864
5428
  if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
4865
- if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== fileMetadata.sha256) {
4866
- remainingErrors.push({
4867
- message: "File metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + fileMetadata.sha256 + ".",
4868
- fixes: undefined
4869
- });
4870
- }
4871
5429
  if (!("mimeType" in currentValue.metadata) || currentValue.metadata.mimeType !== fileMetadata.mimeType) {
4872
5430
  remainingErrors.push({
4873
5431
  message: "File metadata mimeType is incorrect! Found: " + ("mimeType" in currentValue.metadata ? currentValue.metadata.mimeType : "<empty>") + ". Expected: " + fileMetadata.mimeType,
@@ -4888,7 +5446,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
4888
5446
  op: "add",
4889
5447
  path: sourceToPatchPath(sourcePath).concat("metadata"),
4890
5448
  value: {
4891
- sha256: fileMetadata.sha256,
4892
5449
  ...(fileMetadata.mimeType ? {
4893
5450
  mimeType: fileMetadata.mimeType
4894
5451
  } : {})