@valbuild/server 0.93.0 → 0.95.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.
@@ -628,7 +628,7 @@ function removeFromNode(document, node, key) {
628
628
  } else if (ts.isObjectLiteralExpression(node)) {
629
629
  return pipe(findObjectPropertyAssignment(node, key), result.flatMap(assignment => {
630
630
  if (!assignment) {
631
- return result.err(new PatchError("Cannot replace object element which does not exist"));
631
+ return result.err(new PatchError("Cannot remove object element which does not exist"));
632
632
  }
633
633
  return result.ok(assignment);
634
634
  }), result.map(assignment => [removeAt(document, node.properties, node.properties.indexOf(assignment))[0], assignment.initializer]));
@@ -995,7 +995,14 @@ class ValSourceFileHandler {
995
995
  });
996
996
  fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
997
997
  },
998
- rmFile: fs.rmSync
998
+ rmFile: fs.rmSync,
999
+ readBuffer: fileName => {
1000
+ try {
1001
+ return fs.readFileSync(fileName);
1002
+ } catch {
1003
+ return undefined;
1004
+ }
1005
+ }
999
1006
  }) {
1000
1007
  this.projectRoot = projectRoot;
1001
1008
  this.compilerOptions = compilerOptions;
@@ -1043,7 +1050,14 @@ class ValModuleLoader {
1043
1050
  });
1044
1051
  fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1045
1052
  },
1046
- rmFile: fs.rmSync
1053
+ rmFile: fs.rmSync,
1054
+ readBuffer: fileName => {
1055
+ try {
1056
+ return fs.readFileSync(fileName);
1057
+ } catch {
1058
+ return undefined;
1059
+ }
1060
+ }
1047
1061
  }, disableCache = false) {
1048
1062
  this.projectRoot = projectRoot;
1049
1063
  this.compilerOptions = compilerOptions;
@@ -1336,7 +1350,14 @@ async function createService(projectRoot, opts, host = {
1336
1350
  });
1337
1351
  fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1338
1352
  },
1339
- rmFile: fs.rmSync
1353
+ rmFile: fs.rmSync,
1354
+ readBuffer: fileName => {
1355
+ try {
1356
+ return fs.readFileSync(fileName);
1357
+ } catch {
1358
+ return undefined;
1359
+ }
1360
+ }
1340
1361
  }, loader) {
1341
1362
  const compilerOptions = getCompilerOptions(projectRoot, host);
1342
1363
  const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
@@ -1669,8 +1690,10 @@ class ValOps {
1669
1690
  const filePath = op.filePath;
1670
1691
  fileLastUpdatedByPatchId[filePath] = {
1671
1692
  patchId: patch.patchId,
1672
- remote: op.remote
1693
+ remote: op.remote,
1694
+ isDelete: op.value === null
1673
1695
  };
1696
+ continue;
1674
1697
  }
1675
1698
  const path = patch.path;
1676
1699
  if (!patchesByModule[path]) {
@@ -1745,15 +1768,19 @@ class ValOps {
1745
1768
  const fileFixOps = {};
1746
1769
  for (const op of patchData.patch) {
1747
1770
  if (op.op === "file") {
1748
- // NOTE: We insert the last patch_id that modify a file
1749
- // when constructing the url we use the patch id (and the file path)
1750
- // to fetch the right file
1751
- // NOTE: overwrite and use last patch_id if multiple patches modify the same file
1752
- fileFixOps[op.path.join("/")] = [{
1753
- op: "add",
1754
- path: op.path.concat(...(op.nestedFilePath || [])).concat("patch_id"),
1755
- value: patchId
1756
- }];
1771
+ if (op.value !== null) {
1772
+ // NOTE: We insert the last patch_id that modify a file
1773
+ // when constructing the url we use the patch id (and the file path)
1774
+ // to fetch the right file
1775
+ // NOTE: overwrite and use last patch_id if multiple patches modify the same file
1776
+ fileFixOps[op.path.join("/")] = [{
1777
+ op: "add",
1778
+ path: op.path.concat(...(op.nestedFilePath || [])).concat("patch_id"),
1779
+ value: patchId
1780
+ }];
1781
+ }
1782
+ // null value = delete: no patch_id to inject; the "remove" op in
1783
+ // the patch already removes the metadata entry from the source
1757
1784
  } else {
1758
1785
  applicableOps.push(op);
1759
1786
  }
@@ -1866,6 +1893,21 @@ class ValOps {
1866
1893
  const files = {};
1867
1894
  const remoteFiles = {};
1868
1895
  const entries = Object.entries(schemas);
1896
+ // Build a map of gallery directory → [ModuleFilePath, ...] across ALL modules
1897
+ // (must include all modules, not just those being validated, since conflicts can come from any module)
1898
+ const galleryDirectoryToModules = new Map();
1899
+ for (const [moduleFilePathS, schema] of entries) {
1900
+ const serialized = schema["executeSerialize"]();
1901
+ if (serialized.type === "record" && serialized.mediaType && serialized.directory) {
1902
+ const dir = serialized.directory;
1903
+ const existing = galleryDirectoryToModules.get(dir);
1904
+ if (existing) {
1905
+ existing.push(moduleFilePathS);
1906
+ } else {
1907
+ galleryDirectoryToModules.set(dir, [moduleFilePathS]);
1908
+ }
1909
+ }
1910
+ }
1869
1911
  const modulePathsToValidate = patchesByModule && Object.keys(patchesByModule);
1870
1912
  for (const [pathS, schema] of entries) {
1871
1913
  if (modulePathsToValidate && !modulePathsToValidate.includes(pathS)) {
@@ -1906,7 +1948,7 @@ class ValOps {
1906
1948
  };
1907
1949
  if (validationErrors) {
1908
1950
  for (const validationError of validationErrors) {
1909
- var _validationError$fixe, _validationError$fixe2, _validationError$fixe3, _validationError$fixe4;
1951
+ var _validationError$fixe, _validationError$fixe2, _validationError$fixe3, _validationError$fixe4, _validationError$fixe5, _validationError$fixe6, _validationError$fixe7, _validationError$fixe8;
1910
1952
  if (isOnlyFileCheckValidationError(validationError)) {
1911
1953
  if (files[sourcePath]) {
1912
1954
  throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
@@ -1989,7 +2031,32 @@ class ValOps {
1989
2031
  }
1990
2032
  }
1991
2033
  }
1992
- } else {
2034
+ } else if ((_validationError$fixe5 = validationError.fixes) !== null && _validationError$fixe5 !== void 0 && _validationError$fixe5.includes("images:check-unique-folder") || (_validationError$fixe6 = validationError.fixes) !== null && _validationError$fixe6 !== void 0 && _validationError$fixe6.includes("files:check-unique-folder")) {
2035
+ const TYPE_ERROR_MESSAGE = `This is most likely a Val version mismatch or Val bug.`;
2036
+ if (!validationError.value || typeof validationError.value !== "object") {
2037
+ addError({
2038
+ message: `Could not find a directory value for gallery at ${sourcePath}. ${TYPE_ERROR_MESSAGE}`,
2039
+ typeError: true
2040
+ });
2041
+ } else {
2042
+ const directory = "directory" in validationError.value && validationError.value.directory;
2043
+ if (typeof directory !== "string") {
2044
+ addError({
2045
+ message: `Expected gallery validation error 'value' to have property 'directory' of type 'string'. Found: ${typeof directory}. ${TYPE_ERROR_MESSAGE}`,
2046
+ typeError: true
2047
+ });
2048
+ } else {
2049
+ const modulesUsingDir = galleryDirectoryToModules.get(directory) ?? [];
2050
+ const conflictingModules = modulesUsingDir.filter(m => m !== path);
2051
+ if (conflictingModules.length > 0) {
2052
+ addError({
2053
+ message: `Gallery directory '${directory}' in ${path} conflicts with: ${conflictingModules.join(", ")}. Each gallery must use a unique directory.`
2054
+ });
2055
+ }
2056
+ // If conflictingModules is empty, directory is unique — silently drop the error.
2057
+ }
2058
+ }
2059
+ } else if ((_validationError$fixe7 = validationError.fixes) !== null && _validationError$fixe7 !== void 0 && _validationError$fixe7.includes("images:check-all-files") || (_validationError$fixe8 = validationError.fixes) !== null && _validationError$fixe8 !== void 0 && _validationError$fixe8.includes("files:check-all-files")) ; else {
1993
2060
  addError(validationError);
1994
2061
  }
1995
2062
  }
@@ -2269,16 +2336,22 @@ class ValOps {
2269
2336
  await Promise.all(Object.entries(fileLastUpdatedByPatchId).map(async ([filePath, patchData]) => {
2270
2337
  const {
2271
2338
  patchId,
2272
- remote
2339
+ remote,
2340
+ isDelete
2273
2341
  } = patchData;
2274
2342
  if (globalAppliedPatches.includes(patchId)) {
2275
- // TODO: do we want to make sure the file is there? Then again, it should be rare that it happens (unless there's a Val bug) so it might be enough to fail later (at commit)
2276
- // TODO: include sha256? This way we can make sure we pick the right file since theoretically there could be multiple files with the same path in the same patch
2277
- // or is that the case? We are picking the latest file by path so, that should be enough?
2278
- patchedBinaryFilesDescriptors[filePath] = {
2279
- patchId,
2280
- remote
2281
- };
2343
+ if (isDelete) {
2344
+ // Signal file deletion via patchedSourceFiles null entry
2345
+ patchedSourceFiles[filePath] = null;
2346
+ } else {
2347
+ // TODO: do we want to make sure the file is there? Then again, it should be rare that it happens (unless there's a Val bug) so it might be enough to fail later (at commit)
2348
+ // TODO: include sha256? This way we can make sure we pick the right file since theoretically there could be multiple files with the same path in the same patch
2349
+ // or is that the case? We are picking the latest file by path so, that should be enough?
2350
+ patchedBinaryFilesDescriptors[filePath] = {
2351
+ patchId,
2352
+ remote
2353
+ };
2354
+ }
2282
2355
  } else {
2283
2356
  hasErrors = true;
2284
2357
  binaryFilePatchErrors[filePath] = {
@@ -2301,8 +2374,8 @@ class ValOps {
2301
2374
  }
2302
2375
 
2303
2376
  // #region createPatch
2304
- async createPatch(path, patch, patchId, parentRef, authorId) {
2305
- const saveRes = await this.saveSourceFilePatch(path, patch, patchId, parentRef, authorId);
2377
+ async createPatch(path, patch, patchId, parentRef, sessionId, authorId) {
2378
+ const saveRes = await this.saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId);
2306
2379
  if (result.isErr(saveRes)) {
2307
2380
  console.error(`Could not save source patch at path: '${path}'. Error: ${saveRes.error.errorType === "other" ? saveRes.error.message : saveRes.error.errorType}`);
2308
2381
  if (saveRes.error.errorType === "patch-head-conflict") {
@@ -2324,8 +2397,8 @@ class ValOps {
2324
2397
  // #region abstract ops
2325
2398
  }
2326
2399
  function isOnlyFileCheckValidationError(validationError) {
2327
- var _validationError$fixe5;
2328
- if ((_validationError$fixe5 = validationError.fixes) !== null && _validationError$fixe5 !== void 0 && _validationError$fixe5.every(f => f === "file:check-metadata" || f === "image:check-metadata")) {
2400
+ var _validationError$fixe9;
2401
+ if ((_validationError$fixe9 = validationError.fixes) !== null && _validationError$fixe9 !== void 0 && _validationError$fixe9.every(f => f === "file:check-metadata" || f === "image:check-metadata")) {
2329
2402
  return true;
2330
2403
  }
2331
2404
  return false;
@@ -2694,11 +2767,17 @@ class ValOpsFS extends ValOps {
2694
2767
  parentPatchId: dir
2695
2768
  });
2696
2769
  } else {
2697
- if (includes && includes.length > 0 && !includes.includes(parsedPatch.data.patchId)) {
2770
+ const patchId = parsedPatch.data.patchId;
2771
+ if (includes && includes.length > 0 && !includes.includes(patchId)) {
2698
2772
  return;
2699
2773
  }
2700
- patches[parsedPatch.data.patchId] = {
2701
- ...parsedPatch.data,
2774
+ patches[patchId] = {
2775
+ path: parsedPatch.data.path,
2776
+ patch: parsedPatch.data.patch,
2777
+ parentRef: parsedPatch.data.parentRef,
2778
+ baseSha: parsedPatch.data.baseSha,
2779
+ createdAt: parsedPatch.data.createdAt,
2780
+ authorId: parsedPatch.data.authorId,
2702
2781
  appliedAt: null
2703
2782
  };
2704
2783
  }
@@ -2869,7 +2948,7 @@ class ValOpsFS extends ValOps {
2869
2948
  };
2870
2949
  }
2871
2950
  }
2872
- async saveSourceFilePatch(path, patch, patchId, parentRef, authorId) {
2951
+ async saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId) {
2873
2952
  const patchDir = this.getParentPatchIdFromParentRef(parentRef);
2874
2953
  try {
2875
2954
  const baseSha = await this.getBaseSha();
@@ -2879,6 +2958,7 @@ class ValOpsFS extends ValOps {
2879
2958
  parentRef,
2880
2959
  path,
2881
2960
  authorId,
2961
+ sessionId,
2882
2962
  baseSha,
2883
2963
  coreVersion: Internal.VERSION.core,
2884
2964
  createdAt: new Date().toISOString()
@@ -2956,6 +3036,14 @@ class ValOpsFS extends ValOps {
2956
3036
  const patchFilePath = this.getBinaryFilePath(filePath, patchDir);
2957
3037
  const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchDir);
2958
3038
  try {
3039
+ if (data === null) {
3040
+ this.host.deleteFile(patchFilePath);
3041
+ this.host.deleteFile(metadataFilePath);
3042
+ return {
3043
+ patchId,
3044
+ filePath
3045
+ };
3046
+ }
2959
3047
  const buffer = bufferFromDataUrl(data);
2960
3048
  if (!buffer) {
2961
3049
  return {
@@ -3216,7 +3304,11 @@ class ValOpsFS extends ValOps {
3216
3304
  for (const [filePath, data] of Object.entries(preparedCommit.patchedSourceFiles)) {
3217
3305
  const absPath = path__default.join(this.rootDir, ...filePath.split("/"));
3218
3306
  try {
3219
- this.host.writeUf8File(absPath, data);
3307
+ if (data === null) {
3308
+ this.host.deleteFile(absPath);
3309
+ } else {
3310
+ this.host.writeUf8File(absPath, data);
3311
+ }
3220
3312
  updatedFiles.push(absPath);
3221
3313
  } catch (err) {
3222
3314
  errors[absPath] = {
@@ -3305,12 +3397,6 @@ class ValOpsFS extends ValOps {
3305
3397
  return result.ok(Object.fromEntries(Object.entries(patches.patches).map(([patchId, value]) => [patchId, this.getParentPatchIdFromParentRef(value.parentRef)])));
3306
3398
  }
3307
3399
 
3308
- // #region profiles
3309
- async getProfiles() {
3310
- // We do not have profiles in FS mode
3311
- return [];
3312
- }
3313
-
3314
3400
  // #region fs file path helpers
3315
3401
  getPatchesDir() {
3316
3402
  return path__default.join(this.rootDir, ValOpsFS.VAL_DIR, "patches");
@@ -3342,6 +3428,11 @@ class FSOpsHost {
3342
3428
  });
3343
3429
  }
3344
3430
  }
3431
+ deleteFile(path) {
3432
+ if (this.fileExists(path)) {
3433
+ fs.rmSync(path);
3434
+ }
3435
+ }
3345
3436
  moveDir(from, to) {
3346
3437
  fs.renameSync(from, to);
3347
3438
  }
@@ -3414,7 +3505,9 @@ const FSPatch = z.object({
3414
3505
  parentRef: ParentRef,
3415
3506
  authorId: z.string().refine(p => true).nullable(),
3416
3507
  createdAt: z.string().datetime(),
3417
- coreVersion: z.string().nullable() // TODO: use this to check if patch is compatible with current core version?
3508
+ coreVersion: z.string().nullable(),
3509
+ // TODO: use this to check if patch is compatible with current core version?
3510
+ sessionId: z.string().nullable()
3418
3511
  });
3419
3512
  const FSPatchBase = z.object({
3420
3513
  baseSha: z.string().refine(p => true),
@@ -3516,17 +3609,6 @@ const CommitResponse = z.object({
3516
3609
  commit: CommitSha,
3517
3610
  branch: z.string()
3518
3611
  });
3519
- const ProfilesResponse = z.object({
3520
- profiles: z.array(z.object({
3521
- profileId: z.string(),
3522
- fullName: z.string(),
3523
- email: z.string().optional(),
3524
- // TODO: make this required once this can be guaranteed
3525
- avatar: z.object({
3526
- url: z.string()
3527
- }).nullable()
3528
- }))
3529
- });
3530
3612
  const NonceResponse = z.object({
3531
3613
  nonce: z.string(),
3532
3614
  url: z.string()
@@ -3977,7 +4059,7 @@ class ValOpsHttp extends ValOps {
3977
4059
  };
3978
4060
  }
3979
4061
  }
3980
- async saveSourceFilePatch(path, patch, patchId, parentRef, authorId) {
4062
+ async saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId) {
3981
4063
  const baseSha = await this.getBaseSha();
3982
4064
  return fetch(`${this.contentUrl}/v1/${this.project}/patches`, {
3983
4065
  method: "POST",
@@ -3989,6 +4071,7 @@ class ValOpsHttp extends ValOps {
3989
4071
  path,
3990
4072
  patch,
3991
4073
  authorId,
4074
+ sessionId,
3992
4075
  patchId,
3993
4076
  parentPatchId: parentRef.type === "patch" ? parentRef.patchId : null,
3994
4077
  baseSha,
@@ -4412,31 +4495,6 @@ class ValOpsHttp extends ValOps {
4412
4495
  };
4413
4496
  }
4414
4497
  }
4415
-
4416
- // #region profiles
4417
- async getProfiles() {
4418
- var _res$headers$get6;
4419
- const res = await fetch(`${this.contentUrl}/v1/${this.project}/profiles`, {
4420
- headers: {
4421
- ...this.authHeaders,
4422
- "Content-Type": "application/json"
4423
- }
4424
- });
4425
- if (res.ok) {
4426
- const parsed = ProfilesResponse.safeParse(await res.json());
4427
- if (parsed.error) {
4428
- console.error("Could not parse profiles response", parsed.error);
4429
- throw Error(`Could not get profiles from remote server: wrong format. You might need to upgrade Val.`);
4430
- }
4431
- return parsed.data.profiles;
4432
- }
4433
- if ((_res$headers$get6 = res.headers.get("Content-Type")) !== null && _res$headers$get6 !== void 0 && _res$headers$get6.includes("application/json")) {
4434
- const json = await res.json();
4435
- const message = getErrorMessageFromUnknownJson(json, "Unknown error");
4436
- throw Error(`Could not get profiles (status: ${res.status}): ${message}`);
4437
- }
4438
- throw Error(`Could not get profiles. Got status: ${res.status}`);
4439
- }
4440
4498
  }
4441
4499
 
4442
4500
  const host = process.env.VAL_CONTENT_URL || DEFAULT_CONTENT_HOST;
@@ -4589,6 +4647,16 @@ function hasRemoteFileSchema(schema) {
4589
4647
 
4590
4648
  /* eslint-disable @typescript-eslint/no-unused-vars */
4591
4649
  const ValServer = (valModules, options, callbacks) => {
4650
+ const ProfilesResponse = z.object({
4651
+ profiles: z.array(z.object({
4652
+ profileId: z.string(),
4653
+ fullName: z.string(),
4654
+ email: z.string().optional(),
4655
+ avatar: z.object({
4656
+ url: z.string()
4657
+ }).nullable()
4658
+ }))
4659
+ });
4592
4660
  let serverOps;
4593
4661
  if (options.mode === "fs") {
4594
4662
  serverOps = new ValOpsFS(options.valContentUrl, options.cwd, valModules, {
@@ -5438,10 +5506,11 @@ const ValServer = (valModules, options, callbacks) => {
5438
5506
  }
5439
5507
  const patches = req.body.patches;
5440
5508
  let parentRef = req.body.parentRef;
5509
+ const sessionId = req.body.sessionId ?? null;
5441
5510
  const authorId = "id" in auth ? auth.id : null;
5442
5511
  const newPatchIds = [];
5443
5512
  for (const patch of patches) {
5444
- const createPatchRes = await serverOps.createPatch(patch.path, patch.patch, patch.patchId, parentRef, authorId);
5513
+ const createPatchRes = await serverOps.createPatch(patch.path, patch.patch, patch.patchId, parentRef, sessionId, authorId);
5445
5514
  if (result.isErr(createPatchRes)) {
5446
5515
  if (createPatchRes.error.errorType === "patch-head-conflict") {
5447
5516
  return {
@@ -5802,13 +5871,87 @@ const ValServer = (valModules, options, callbacks) => {
5802
5871
  }
5803
5872
  };
5804
5873
  }
5805
- const profiles = await serverOps.getProfiles();
5806
- return {
5807
- status: 200,
5808
- json: {
5809
- profiles
5874
+ if (!options.project) {
5875
+ return {
5876
+ status: 500,
5877
+ json: {
5878
+ message: "Project is not configured"
5879
+ }
5880
+ };
5881
+ }
5882
+ const authDataRes = await getRemoteFileAuth();
5883
+ if (authDataRes.status !== 200) {
5884
+ if (serverOps instanceof ValOpsFS && authDataRes.json.errorCode === "pat-error") {
5885
+ return {
5886
+ status: 200,
5887
+ json: {
5888
+ profiles: []
5889
+ }
5890
+ };
5891
+ }
5892
+ return {
5893
+ status: 500,
5894
+ json: {
5895
+ message: authDataRes.json.message
5896
+ }
5897
+ };
5898
+ }
5899
+ const authData = authDataRes.json.remoteFileAuth;
5900
+ const execFetch = async headers => {
5901
+ try {
5902
+ const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/profiles`;
5903
+ const upstreamRes = await fetch(upstreamUrl, {
5904
+ method: "GET",
5905
+ headers
5906
+ });
5907
+ if (!upstreamRes.ok) {
5908
+ const text = await upstreamRes.text();
5909
+ const isAuthError = upstreamRes.status === 401 || upstreamRes.status === 403;
5910
+ return {
5911
+ status: isAuthError ? 401 : 500,
5912
+ json: {
5913
+ message: isAuthError ? `Profile authentication failed: ${upstreamRes.status} ${text}` : `Profiles failed: ${upstreamRes.status} ${text}`
5914
+ }
5915
+ };
5916
+ }
5917
+ const parseRes = ProfilesResponse.safeParse(await upstreamRes.json());
5918
+ if (!parseRes.success) {
5919
+ return {
5920
+ status: 500,
5921
+ json: {
5922
+ message: "Could not parse profiles response: " + fromError(parseRes.error).toString()
5923
+ }
5924
+ };
5925
+ }
5926
+ return {
5927
+ status: 200,
5928
+ json: {
5929
+ profiles: parseRes.data.profiles
5930
+ }
5931
+ };
5932
+ } catch (err) {
5933
+ return {
5934
+ status: 500,
5935
+ json: {
5936
+ message: err instanceof Error ? err.message : "Profiles request failed"
5937
+ }
5938
+ };
5810
5939
  }
5811
5940
  };
5941
+ if (serverOps instanceof ValOpsFS) {
5942
+ return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
5943
+ }
5944
+ if (!options.valSecret) {
5945
+ return {
5946
+ status: 500,
5947
+ json: {
5948
+ message: "Secret is not configured"
5949
+ }
5950
+ };
5951
+ }
5952
+ return withAuth(options.valSecret, cookies, "profiles", data => {
5953
+ return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
5954
+ });
5812
5955
  }
5813
5956
  },
5814
5957
  "/commit-summary": {
@@ -5990,6 +6133,381 @@ const ValServer = (valModules, options, callbacks) => {
5990
6133
  }
5991
6134
  }
5992
6135
  },
6136
+ //#region ai proxy
6137
+ "/ai/initialize": {
6138
+ POST: async req => {
6139
+ const cookies = req.cookies;
6140
+ const auth = getAuth(cookies);
6141
+ if (auth.error) {
6142
+ return {
6143
+ status: 401,
6144
+ json: {
6145
+ message: auth.error
6146
+ }
6147
+ };
6148
+ }
6149
+ if (!options.project) {
6150
+ return {
6151
+ status: 500,
6152
+ json: {
6153
+ message: "Project is not configured"
6154
+ }
6155
+ };
6156
+ }
6157
+ const authDataRes = await getRemoteFileAuth();
6158
+ if (authDataRes.status !== 200) {
6159
+ return {
6160
+ status: 500,
6161
+ json: {
6162
+ message: authDataRes.json.message
6163
+ }
6164
+ };
6165
+ }
6166
+ const authData = authDataRes.json.remoteFileAuth;
6167
+ const execFetch = async headers => {
6168
+ try {
6169
+ const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/initialize`;
6170
+ const upstreamRes = await fetch(upstreamUrl, {
6171
+ method: "POST",
6172
+ headers,
6173
+ body: JSON.stringify({})
6174
+ });
6175
+ if (!upstreamRes.ok) {
6176
+ const text = await upstreamRes.text();
6177
+ return {
6178
+ status: 500,
6179
+ json: {
6180
+ message: `AI initialize failed: ${upstreamRes.status} ${text}`
6181
+ }
6182
+ };
6183
+ }
6184
+ const json = await upstreamRes.json();
6185
+ const wsUrl = options.valContentUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:") + `/v1/${options.project}/ai/connect`;
6186
+ return {
6187
+ status: 200,
6188
+ json: {
6189
+ nonce: json.nonce,
6190
+ wsUrl
6191
+ }
6192
+ };
6193
+ } catch (err) {
6194
+ return {
6195
+ status: 500,
6196
+ json: {
6197
+ message: err instanceof Error ? err.message : "AI initialize error"
6198
+ }
6199
+ };
6200
+ }
6201
+ };
6202
+ if (serverOps instanceof ValOpsFS) {
6203
+ return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
6204
+ }
6205
+ if (!options.valSecret) {
6206
+ return {
6207
+ status: 500,
6208
+ json: {
6209
+ message: "Secret is not configured"
6210
+ }
6211
+ };
6212
+ }
6213
+ return withAuth(options.valSecret, cookies, "ai/initialize", data => {
6214
+ return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
6215
+ });
6216
+ }
6217
+ },
6218
+ "/ai/sessions": {
6219
+ GET: async req => {
6220
+ const cookies = req.cookies;
6221
+ const auth = getAuth(cookies);
6222
+ if (auth.error) {
6223
+ return {
6224
+ status: 401,
6225
+ json: {
6226
+ message: auth.error
6227
+ }
6228
+ };
6229
+ }
6230
+ if (!options.project) {
6231
+ return {
6232
+ status: 500,
6233
+ json: {
6234
+ message: "Project is not configured"
6235
+ }
6236
+ };
6237
+ }
6238
+ const authDataRes = await getRemoteFileAuth();
6239
+ if (authDataRes.status !== 200) {
6240
+ return {
6241
+ status: 500,
6242
+ json: {
6243
+ message: authDataRes.json.message
6244
+ }
6245
+ };
6246
+ }
6247
+ const authData = authDataRes.json.remoteFileAuth;
6248
+ const execFetch = async headers => {
6249
+ try {
6250
+ const SessionsResponse = z.object({
6251
+ sessions: z.array(z.object({
6252
+ id: z.string(),
6253
+ name: z.string().nullable(),
6254
+ createdAt: z.string(),
6255
+ updatedAt: z.string()
6256
+ })),
6257
+ nextCursor: z.object({
6258
+ updatedAt: z.string(),
6259
+ id: z.string()
6260
+ }).nullable().optional()
6261
+ });
6262
+ const params = new URLSearchParams();
6263
+ if (req.query.limit) params.set("limit", req.query.limit);
6264
+ if (req.query.cursor_updatedAt) {
6265
+ params.set("cursor_updatedAt", req.query.cursor_updatedAt);
6266
+ }
6267
+ if (req.query.cursor_id) {
6268
+ params.set("cursor_id", req.query.cursor_id);
6269
+ }
6270
+ const qs = params.toString();
6271
+ const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/sessions${qs ? `?${qs}` : ""}`;
6272
+ const upstreamRes = await fetch(upstreamUrl, {
6273
+ headers
6274
+ });
6275
+ if (!upstreamRes.ok) {
6276
+ const text = await upstreamRes.text();
6277
+ return {
6278
+ status: 500,
6279
+ json: {
6280
+ message: `AI sessions failed: ${upstreamRes.status} ${text}`
6281
+ }
6282
+ };
6283
+ }
6284
+ const json = SessionsResponse.safeParse(await upstreamRes.json());
6285
+ if (!json.success) {
6286
+ return {
6287
+ status: 500,
6288
+ json: {
6289
+ message: "Could not parse AI sessions response: " + fromError(json.error).toString()
6290
+ }
6291
+ };
6292
+ }
6293
+ return {
6294
+ status: 200,
6295
+ json: json.data
6296
+ };
6297
+ } catch (err) {
6298
+ return {
6299
+ status: 500,
6300
+ json: {
6301
+ message: err instanceof Error ? err.message : "AI sessions error"
6302
+ }
6303
+ };
6304
+ }
6305
+ };
6306
+ if (serverOps instanceof ValOpsFS) {
6307
+ return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
6308
+ }
6309
+ if (!options.valSecret) {
6310
+ return {
6311
+ status: 500,
6312
+ json: {
6313
+ message: "Secret is not configured"
6314
+ }
6315
+ };
6316
+ }
6317
+ return withAuth(options.valSecret, cookies, "ai/sessions", data => {
6318
+ return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
6319
+ });
6320
+ },
6321
+ PATCH: async req => {
6322
+ const cookies = req.cookies;
6323
+ const auth = getAuth(cookies);
6324
+ if (auth.error) {
6325
+ return {
6326
+ status: 401,
6327
+ json: {
6328
+ message: auth.error
6329
+ }
6330
+ };
6331
+ }
6332
+ if (!options.project) {
6333
+ return {
6334
+ status: 500,
6335
+ json: {
6336
+ message: "Project is not configured"
6337
+ }
6338
+ };
6339
+ }
6340
+ const pathParts = (req.path || "").split("/").filter(Boolean);
6341
+ const sessionId = pathParts[0];
6342
+ if (!sessionId) {
6343
+ return {
6344
+ status: 500,
6345
+ json: {
6346
+ message: "Missing sessionId in path"
6347
+ }
6348
+ };
6349
+ }
6350
+ const authDataRes = await getRemoteFileAuth();
6351
+ if (authDataRes.status !== 200) {
6352
+ return {
6353
+ status: 500,
6354
+ json: {
6355
+ message: authDataRes.json.message
6356
+ }
6357
+ };
6358
+ }
6359
+ const authData = authDataRes.json.remoteFileAuth;
6360
+ const execFetch = async headers => {
6361
+ try {
6362
+ const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/sessions/${encodeURIComponent(sessionId)}`;
6363
+ const upstreamRes = await fetch(upstreamUrl, {
6364
+ method: "PATCH",
6365
+ headers,
6366
+ body: JSON.stringify({
6367
+ name: req.body.name
6368
+ })
6369
+ });
6370
+ if (!upstreamRes.ok) {
6371
+ const text = await upstreamRes.text();
6372
+ return {
6373
+ status: 500,
6374
+ json: {
6375
+ message: `AI session rename failed: ${upstreamRes.status} ${text}`
6376
+ }
6377
+ };
6378
+ }
6379
+ return {
6380
+ status: 200,
6381
+ json: {}
6382
+ };
6383
+ } catch (err) {
6384
+ return {
6385
+ status: 500,
6386
+ json: {
6387
+ message: err instanceof Error ? err.message : "AI session rename error"
6388
+ }
6389
+ };
6390
+ }
6391
+ };
6392
+ if (serverOps instanceof ValOpsFS) {
6393
+ return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
6394
+ }
6395
+ if (!options.valSecret) {
6396
+ return {
6397
+ status: 500,
6398
+ json: {
6399
+ message: "Secret is not configured"
6400
+ }
6401
+ };
6402
+ }
6403
+ return withAuth(options.valSecret, cookies, "ai/sessions/rename", data => {
6404
+ return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
6405
+ });
6406
+ }
6407
+ },
6408
+ "/ai/messages": {
6409
+ GET: async req => {
6410
+ const cookies = req.cookies;
6411
+ const auth = getAuth(cookies);
6412
+ if (auth.error) {
6413
+ return {
6414
+ status: 401,
6415
+ json: {
6416
+ message: auth.error
6417
+ }
6418
+ };
6419
+ }
6420
+ if (!options.project) {
6421
+ return {
6422
+ status: 500,
6423
+ json: {
6424
+ message: "Project is not configured"
6425
+ }
6426
+ };
6427
+ }
6428
+ const pathParts = (req.path || "").split("/").filter(Boolean);
6429
+ const sessionId = pathParts[0];
6430
+ if (!sessionId) {
6431
+ return {
6432
+ status: 500,
6433
+ json: {
6434
+ message: "Missing sessionId in path"
6435
+ }
6436
+ };
6437
+ }
6438
+ const authDataRes = await getRemoteFileAuth();
6439
+ if (authDataRes.status !== 200) {
6440
+ return {
6441
+ status: 500,
6442
+ json: {
6443
+ message: authDataRes.json.message
6444
+ }
6445
+ };
6446
+ }
6447
+ const authData = authDataRes.json.remoteFileAuth;
6448
+ const execFetch = async headers => {
6449
+ try {
6450
+ const SessionMessagesResponse = z.object({
6451
+ messages: z.array(z.object({
6452
+ role: z.string(),
6453
+ content: z.string()
6454
+ })),
6455
+ nextCursor: z.object({
6456
+ updatedAt: z.string(),
6457
+ id: z.string()
6458
+ }).nullable().optional()
6459
+ });
6460
+ const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/sessions/${encodeURIComponent(sessionId)}/messages`;
6461
+ const upstreamRes = await fetch(upstreamUrl, {
6462
+ headers
6463
+ });
6464
+ if (!upstreamRes.ok) {
6465
+ const text = await upstreamRes.text();
6466
+ return {
6467
+ status: 500,
6468
+ json: {
6469
+ message: `AI session messages failed: ${upstreamRes.status} ${text}`
6470
+ }
6471
+ };
6472
+ }
6473
+ const json = SessionMessagesResponse.safeParse(await upstreamRes.json());
6474
+ if (!json.success) {
6475
+ return {
6476
+ status: 500,
6477
+ json: {
6478
+ message: "Could not parse AI session messages response: " + fromError(json.error).toString()
6479
+ }
6480
+ };
6481
+ }
6482
+ return {
6483
+ status: 200,
6484
+ json: json.data
6485
+ };
6486
+ } catch (err) {
6487
+ return {
6488
+ status: 500,
6489
+ json: {
6490
+ message: err instanceof Error ? err.message : "AI session messages error"
6491
+ }
6492
+ };
6493
+ }
6494
+ };
6495
+ if (serverOps instanceof ValOpsFS) {
6496
+ return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
6497
+ }
6498
+ if (!options.valSecret) {
6499
+ return {
6500
+ status: 500,
6501
+ json: {
6502
+ message: "Secret is not configured"
6503
+ }
6504
+ };
6505
+ }
6506
+ return withAuth(options.valSecret, cookies, "ai/sessions/messages", data => {
6507
+ return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
6508
+ });
6509
+ }
6510
+ },
5993
6511
  //#region files
5994
6512
  "/files": {
5995
6513
  GET: async req => {
@@ -6211,6 +6729,21 @@ async function withAuth(secret, cookies, errorMessageType, handler) {
6211
6729
  };
6212
6730
  }
6213
6731
  }
6732
+ function getProfileAuthHeaders(auth, data, type) {
6733
+ if ("pat" in auth) {
6734
+ return {
6735
+ "x-val-pat": auth.pat,
6736
+ "Content-Type": type
6737
+ };
6738
+ }
6739
+ if ("apiKey" in auth && data) {
6740
+ return {
6741
+ ...getAuthHeaders(auth.apiKey, type),
6742
+ "x-val-profile-id": data.sub
6743
+ };
6744
+ }
6745
+ throw new Error("Invalid auth");
6746
+ }
6214
6747
  function getAuthHeaders(token, type) {
6215
6748
  if (!type) {
6216
6749
  return {
@@ -6738,6 +7271,9 @@ class ValFSHost {
6738
7271
  readFile(fileName) {
6739
7272
  return this.valFS.readFile(fileName);
6740
7273
  }
7274
+ readBuffer(fileName) {
7275
+ return this.valFS.readBuffer(fileName);
7276
+ }
6741
7277
  realpath(path) {
6742
7278
  return this.valFS.realpath(path);
6743
7279
  }
@@ -7212,6 +7748,88 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
7212
7748
  metadata: v.metadata
7213
7749
  } : value
7214
7750
  });
7751
+ } else if (fix === "images:check-all-files" || fix === "files:check-all-files") {
7752
+ if (!moduleSource || typeof moduleSource !== "object") {
7753
+ remainingErrors.push({
7754
+ ...validationError,
7755
+ message: "Unexpected error while checking gallery metadata (no moduleSource)",
7756
+ fixes: undefined
7757
+ });
7758
+ continue;
7759
+ }
7760
+ const gallerySource = moduleSource;
7761
+ for (const [entryKey, storedEntry] of Object.entries(gallerySource)) {
7762
+ const filename = path__default.join(config.projectRoot, entryKey);
7763
+ let buffer;
7764
+ try {
7765
+ buffer = fs.readFileSync(filename);
7766
+ } catch {
7767
+ if (apply) {
7768
+ const removePath = sourceToPatchPath(sourcePath).concat([entryKey]);
7769
+ if (isNotRoot(removePath)) {
7770
+ patch.push({
7771
+ op: "remove",
7772
+ path: removePath
7773
+ });
7774
+ }
7775
+ } else {
7776
+ remainingErrors.push({
7777
+ ...validationError,
7778
+ message: `Could not read file: ${filename} - file might not exist or can not be accessed`,
7779
+ fixes: undefined
7780
+ });
7781
+ }
7782
+ continue;
7783
+ }
7784
+ if (fix === "images:check-all-files") {
7785
+ const actualMetadata = await extractImageMetadata(filename, buffer);
7786
+ const stored = storedEntry;
7787
+ const metadataIsCorrect = stored.width === actualMetadata.width && stored.height === actualMetadata.height && stored.mimeType === actualMetadata.mimeType;
7788
+ if (!metadataIsCorrect) {
7789
+ if (apply) {
7790
+ patch.push({
7791
+ op: "replace",
7792
+ path: sourceToPatchPath(sourcePath).concat([entryKey]),
7793
+ value: {
7794
+ ...stored,
7795
+ width: actualMetadata.width ?? 0,
7796
+ height: actualMetadata.height ?? 0,
7797
+ mimeType: actualMetadata.mimeType ?? "application/octet-stream"
7798
+ }
7799
+ });
7800
+ } else {
7801
+ remainingErrors.push({
7802
+ ...validationError,
7803
+ message: `Image metadata for '${entryKey}' is incorrect (width: ${stored.width ?? "<empty>"} vs ${actualMetadata.width}, height: ${stored.height ?? "<empty>"} vs ${actualMetadata.height}, mimeType: ${stored.mimeType ?? "<empty>"} vs ${actualMetadata.mimeType}). Use --fix to update.`
7804
+ });
7805
+ }
7806
+ }
7807
+ } else if (fix === "files:check-all-files") {
7808
+ const actualMetadata = await extractFileMetadata(filename);
7809
+ const stored = storedEntry;
7810
+ const metadataIsCorrect = stored.mimeType === actualMetadata.mimeType;
7811
+ if (!metadataIsCorrect) {
7812
+ if (apply) {
7813
+ patch.push({
7814
+ op: "replace",
7815
+ path: sourceToPatchPath(sourcePath).concat([entryKey]),
7816
+ value: {
7817
+ ...stored,
7818
+ mimeType: actualMetadata.mimeType ?? "application/octet-stream"
7819
+ }
7820
+ });
7821
+ } else {
7822
+ remainingErrors.push({
7823
+ ...validationError,
7824
+ message: `File metadata for '${entryKey}' has incorrect mimeType: '${stored.mimeType ?? "<empty>"}' vs '${actualMetadata.mimeType}'. Use --fix to update.`
7825
+ });
7826
+ }
7827
+ }
7828
+ } else {
7829
+ const exhaustiveCheck = fix;
7830
+ throw new Error(`Internal error: unhandled fix type ${exhaustiveCheck}`);
7831
+ }
7832
+ }
7215
7833
  } else if (fix === "file:check-remote" || fix === "image:check-remote") {
7216
7834
  const v = getRemoteValueFromValidationError(validationError);
7217
7835
  if (!v.success) {