@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.
@@ -659,7 +659,7 @@ function removeFromNode(document, node, key) {
659
659
  } else if (ts__default["default"].isObjectLiteralExpression(node)) {
660
660
  return fp.pipe(findObjectPropertyAssignment(node, key), fp.result.flatMap(assignment => {
661
661
  if (!assignment) {
662
- return fp.result.err(new patch.PatchError("Cannot replace object element which does not exist"));
662
+ return fp.result.err(new patch.PatchError("Cannot remove object element which does not exist"));
663
663
  }
664
664
  return fp.result.ok(assignment);
665
665
  }), fp.result.map(assignment => [removeAt(document, node.properties, node.properties.indexOf(assignment))[0], assignment.initializer]));
@@ -1026,7 +1026,14 @@ class ValSourceFileHandler {
1026
1026
  });
1027
1027
  fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1028
1028
  },
1029
- rmFile: fs__default["default"].rmSync
1029
+ rmFile: fs__default["default"].rmSync,
1030
+ readBuffer: fileName => {
1031
+ try {
1032
+ return fs__default["default"].readFileSync(fileName);
1033
+ } catch {
1034
+ return undefined;
1035
+ }
1036
+ }
1030
1037
  }) {
1031
1038
  this.projectRoot = projectRoot;
1032
1039
  this.compilerOptions = compilerOptions;
@@ -1074,7 +1081,14 @@ class ValModuleLoader {
1074
1081
  });
1075
1082
  fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1076
1083
  },
1077
- rmFile: fs__default["default"].rmSync
1084
+ rmFile: fs__default["default"].rmSync,
1085
+ readBuffer: fileName => {
1086
+ try {
1087
+ return fs__default["default"].readFileSync(fileName);
1088
+ } catch {
1089
+ return undefined;
1090
+ }
1091
+ }
1078
1092
  }, disableCache = false) {
1079
1093
  this.projectRoot = projectRoot;
1080
1094
  this.compilerOptions = compilerOptions;
@@ -1367,7 +1381,14 @@ async function createService(projectRoot, opts, host = {
1367
1381
  });
1368
1382
  fs__default["default"].writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
1369
1383
  },
1370
- rmFile: fs__default["default"].rmSync
1384
+ rmFile: fs__default["default"].rmSync,
1385
+ readBuffer: fileName => {
1386
+ try {
1387
+ return fs__default["default"].readFileSync(fileName);
1388
+ } catch {
1389
+ return undefined;
1390
+ }
1391
+ }
1371
1392
  }, loader) {
1372
1393
  const compilerOptions = getCompilerOptions(projectRoot, host);
1373
1394
  const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
@@ -1700,8 +1721,10 @@ class ValOps {
1700
1721
  const filePath = op.filePath;
1701
1722
  fileLastUpdatedByPatchId[filePath] = {
1702
1723
  patchId: patch.patchId,
1703
- remote: op.remote
1724
+ remote: op.remote,
1725
+ isDelete: op.value === null
1704
1726
  };
1727
+ continue;
1705
1728
  }
1706
1729
  const path = patch.path;
1707
1730
  if (!patchesByModule[path]) {
@@ -1776,15 +1799,19 @@ class ValOps {
1776
1799
  const fileFixOps = {};
1777
1800
  for (const op of patchData.patch) {
1778
1801
  if (op.op === "file") {
1779
- // NOTE: We insert the last patch_id that modify a file
1780
- // when constructing the url we use the patch id (and the file path)
1781
- // to fetch the right file
1782
- // NOTE: overwrite and use last patch_id if multiple patches modify the same file
1783
- fileFixOps[op.path.join("/")] = [{
1784
- op: "add",
1785
- path: op.path.concat(...(op.nestedFilePath || [])).concat("patch_id"),
1786
- value: patchId
1787
- }];
1802
+ if (op.value !== null) {
1803
+ // NOTE: We insert the last patch_id that modify a file
1804
+ // when constructing the url we use the patch id (and the file path)
1805
+ // to fetch the right file
1806
+ // NOTE: overwrite and use last patch_id if multiple patches modify the same file
1807
+ fileFixOps[op.path.join("/")] = [{
1808
+ op: "add",
1809
+ path: op.path.concat(...(op.nestedFilePath || [])).concat("patch_id"),
1810
+ value: patchId
1811
+ }];
1812
+ }
1813
+ // null value = delete: no patch_id to inject; the "remove" op in
1814
+ // the patch already removes the metadata entry from the source
1788
1815
  } else {
1789
1816
  applicableOps.push(op);
1790
1817
  }
@@ -1897,6 +1924,21 @@ class ValOps {
1897
1924
  const files = {};
1898
1925
  const remoteFiles = {};
1899
1926
  const entries = Object.entries(schemas);
1927
+ // Build a map of gallery directory → [ModuleFilePath, ...] across ALL modules
1928
+ // (must include all modules, not just those being validated, since conflicts can come from any module)
1929
+ const galleryDirectoryToModules = new Map();
1930
+ for (const [moduleFilePathS, schema] of entries) {
1931
+ const serialized = schema["executeSerialize"]();
1932
+ if (serialized.type === "record" && serialized.mediaType && serialized.directory) {
1933
+ const dir = serialized.directory;
1934
+ const existing = galleryDirectoryToModules.get(dir);
1935
+ if (existing) {
1936
+ existing.push(moduleFilePathS);
1937
+ } else {
1938
+ galleryDirectoryToModules.set(dir, [moduleFilePathS]);
1939
+ }
1940
+ }
1941
+ }
1900
1942
  const modulePathsToValidate = patchesByModule && Object.keys(patchesByModule);
1901
1943
  for (const [pathS, schema] of entries) {
1902
1944
  if (modulePathsToValidate && !modulePathsToValidate.includes(pathS)) {
@@ -1937,7 +1979,7 @@ class ValOps {
1937
1979
  };
1938
1980
  if (validationErrors) {
1939
1981
  for (const validationError of validationErrors) {
1940
- var _validationError$fixe, _validationError$fixe2, _validationError$fixe3, _validationError$fixe4;
1982
+ var _validationError$fixe, _validationError$fixe2, _validationError$fixe3, _validationError$fixe4, _validationError$fixe5, _validationError$fixe6, _validationError$fixe7, _validationError$fixe8;
1941
1983
  if (isOnlyFileCheckValidationError(validationError)) {
1942
1984
  if (files[sourcePath]) {
1943
1985
  throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
@@ -2020,7 +2062,32 @@ class ValOps {
2020
2062
  }
2021
2063
  }
2022
2064
  }
2023
- } else {
2065
+ } 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")) {
2066
+ const TYPE_ERROR_MESSAGE = `This is most likely a Val version mismatch or Val bug.`;
2067
+ if (!validationError.value || typeof validationError.value !== "object") {
2068
+ addError({
2069
+ message: `Could not find a directory value for gallery at ${sourcePath}. ${TYPE_ERROR_MESSAGE}`,
2070
+ typeError: true
2071
+ });
2072
+ } else {
2073
+ const directory = "directory" in validationError.value && validationError.value.directory;
2074
+ if (typeof directory !== "string") {
2075
+ addError({
2076
+ message: `Expected gallery validation error 'value' to have property 'directory' of type 'string'. Found: ${typeof directory}. ${TYPE_ERROR_MESSAGE}`,
2077
+ typeError: true
2078
+ });
2079
+ } else {
2080
+ const modulesUsingDir = galleryDirectoryToModules.get(directory) ?? [];
2081
+ const conflictingModules = modulesUsingDir.filter(m => m !== path);
2082
+ if (conflictingModules.length > 0) {
2083
+ addError({
2084
+ message: `Gallery directory '${directory}' in ${path} conflicts with: ${conflictingModules.join(", ")}. Each gallery must use a unique directory.`
2085
+ });
2086
+ }
2087
+ // If conflictingModules is empty, directory is unique — silently drop the error.
2088
+ }
2089
+ }
2090
+ } 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 {
2024
2091
  addError(validationError);
2025
2092
  }
2026
2093
  }
@@ -2300,16 +2367,22 @@ class ValOps {
2300
2367
  await Promise.all(Object.entries(fileLastUpdatedByPatchId).map(async ([filePath, patchData]) => {
2301
2368
  const {
2302
2369
  patchId,
2303
- remote
2370
+ remote,
2371
+ isDelete
2304
2372
  } = patchData;
2305
2373
  if (globalAppliedPatches.includes(patchId)) {
2306
- // 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)
2307
- // 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
2308
- // or is that the case? We are picking the latest file by path so, that should be enough?
2309
- patchedBinaryFilesDescriptors[filePath] = {
2310
- patchId,
2311
- remote
2312
- };
2374
+ if (isDelete) {
2375
+ // Signal file deletion via patchedSourceFiles null entry
2376
+ patchedSourceFiles[filePath] = null;
2377
+ } else {
2378
+ // 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)
2379
+ // 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
2380
+ // or is that the case? We are picking the latest file by path so, that should be enough?
2381
+ patchedBinaryFilesDescriptors[filePath] = {
2382
+ patchId,
2383
+ remote
2384
+ };
2385
+ }
2313
2386
  } else {
2314
2387
  hasErrors = true;
2315
2388
  binaryFilePatchErrors[filePath] = {
@@ -2332,8 +2405,8 @@ class ValOps {
2332
2405
  }
2333
2406
 
2334
2407
  // #region createPatch
2335
- async createPatch(path, patch, patchId, parentRef, authorId) {
2336
- const saveRes = await this.saveSourceFilePatch(path, patch, patchId, parentRef, authorId);
2408
+ async createPatch(path, patch, patchId, parentRef, sessionId, authorId) {
2409
+ const saveRes = await this.saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId);
2337
2410
  if (fp.result.isErr(saveRes)) {
2338
2411
  console.error(`Could not save source patch at path: '${path}'. Error: ${saveRes.error.errorType === "other" ? saveRes.error.message : saveRes.error.errorType}`);
2339
2412
  if (saveRes.error.errorType === "patch-head-conflict") {
@@ -2355,8 +2428,8 @@ class ValOps {
2355
2428
  // #region abstract ops
2356
2429
  }
2357
2430
  function isOnlyFileCheckValidationError(validationError) {
2358
- var _validationError$fixe5;
2359
- if ((_validationError$fixe5 = validationError.fixes) !== null && _validationError$fixe5 !== void 0 && _validationError$fixe5.every(f => f === "file:check-metadata" || f === "image:check-metadata")) {
2431
+ var _validationError$fixe9;
2432
+ if ((_validationError$fixe9 = validationError.fixes) !== null && _validationError$fixe9 !== void 0 && _validationError$fixe9.every(f => f === "file:check-metadata" || f === "image:check-metadata")) {
2360
2433
  return true;
2361
2434
  }
2362
2435
  return false;
@@ -2725,11 +2798,17 @@ class ValOpsFS extends ValOps {
2725
2798
  parentPatchId: dir
2726
2799
  });
2727
2800
  } else {
2728
- if (includes && includes.length > 0 && !includes.includes(parsedPatch.data.patchId)) {
2801
+ const patchId = parsedPatch.data.patchId;
2802
+ if (includes && includes.length > 0 && !includes.includes(patchId)) {
2729
2803
  return;
2730
2804
  }
2731
- patches[parsedPatch.data.patchId] = {
2732
- ...parsedPatch.data,
2805
+ patches[patchId] = {
2806
+ path: parsedPatch.data.path,
2807
+ patch: parsedPatch.data.patch,
2808
+ parentRef: parsedPatch.data.parentRef,
2809
+ baseSha: parsedPatch.data.baseSha,
2810
+ createdAt: parsedPatch.data.createdAt,
2811
+ authorId: parsedPatch.data.authorId,
2733
2812
  appliedAt: null
2734
2813
  };
2735
2814
  }
@@ -2900,7 +2979,7 @@ class ValOpsFS extends ValOps {
2900
2979
  };
2901
2980
  }
2902
2981
  }
2903
- async saveSourceFilePatch(path, patch, patchId, parentRef, authorId) {
2982
+ async saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId) {
2904
2983
  const patchDir = this.getParentPatchIdFromParentRef(parentRef);
2905
2984
  try {
2906
2985
  const baseSha = await this.getBaseSha();
@@ -2910,6 +2989,7 @@ class ValOpsFS extends ValOps {
2910
2989
  parentRef,
2911
2990
  path,
2912
2991
  authorId,
2992
+ sessionId,
2913
2993
  baseSha,
2914
2994
  coreVersion: core.Internal.VERSION.core,
2915
2995
  createdAt: new Date().toISOString()
@@ -2987,6 +3067,14 @@ class ValOpsFS extends ValOps {
2987
3067
  const patchFilePath = this.getBinaryFilePath(filePath, patchDir);
2988
3068
  const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchDir);
2989
3069
  try {
3070
+ if (data === null) {
3071
+ this.host.deleteFile(patchFilePath);
3072
+ this.host.deleteFile(metadataFilePath);
3073
+ return {
3074
+ patchId,
3075
+ filePath
3076
+ };
3077
+ }
2990
3078
  const buffer = bufferFromDataUrl(data);
2991
3079
  if (!buffer) {
2992
3080
  return {
@@ -3247,7 +3335,11 @@ class ValOpsFS extends ValOps {
3247
3335
  for (const [filePath, data] of Object.entries(preparedCommit.patchedSourceFiles)) {
3248
3336
  const absPath = path__namespace["default"].join(this.rootDir, ...filePath.split("/"));
3249
3337
  try {
3250
- this.host.writeUf8File(absPath, data);
3338
+ if (data === null) {
3339
+ this.host.deleteFile(absPath);
3340
+ } else {
3341
+ this.host.writeUf8File(absPath, data);
3342
+ }
3251
3343
  updatedFiles.push(absPath);
3252
3344
  } catch (err) {
3253
3345
  errors[absPath] = {
@@ -3336,12 +3428,6 @@ class ValOpsFS extends ValOps {
3336
3428
  return fp.result.ok(Object.fromEntries(Object.entries(patches.patches).map(([patchId, value]) => [patchId, this.getParentPatchIdFromParentRef(value.parentRef)])));
3337
3429
  }
3338
3430
 
3339
- // #region profiles
3340
- async getProfiles() {
3341
- // We do not have profiles in FS mode
3342
- return [];
3343
- }
3344
-
3345
3431
  // #region fs file path helpers
3346
3432
  getPatchesDir() {
3347
3433
  return path__namespace["default"].join(this.rootDir, ValOpsFS.VAL_DIR, "patches");
@@ -3373,6 +3459,11 @@ class FSOpsHost {
3373
3459
  });
3374
3460
  }
3375
3461
  }
3462
+ deleteFile(path) {
3463
+ if (this.fileExists(path)) {
3464
+ fs__default["default"].rmSync(path);
3465
+ }
3466
+ }
3376
3467
  moveDir(from, to) {
3377
3468
  fs__default["default"].renameSync(from, to);
3378
3469
  }
@@ -3445,7 +3536,9 @@ const FSPatch = zod.z.object({
3445
3536
  parentRef: internal.ParentRef,
3446
3537
  authorId: zod.z.string().refine(p => true).nullable(),
3447
3538
  createdAt: zod.z.string().datetime(),
3448
- coreVersion: zod.z.string().nullable() // TODO: use this to check if patch is compatible with current core version?
3539
+ coreVersion: zod.z.string().nullable(),
3540
+ // TODO: use this to check if patch is compatible with current core version?
3541
+ sessionId: zod.z.string().nullable()
3449
3542
  });
3450
3543
  const FSPatchBase = zod.z.object({
3451
3544
  baseSha: zod.z.string().refine(p => true),
@@ -3547,17 +3640,6 @@ const CommitResponse = zod.z.object({
3547
3640
  commit: CommitSha,
3548
3641
  branch: zod.z.string()
3549
3642
  });
3550
- const ProfilesResponse = zod.z.object({
3551
- profiles: zod.z.array(zod.z.object({
3552
- profileId: zod.z.string(),
3553
- fullName: zod.z.string(),
3554
- email: zod.z.string().optional(),
3555
- // TODO: make this required once this can be guaranteed
3556
- avatar: zod.z.object({
3557
- url: zod.z.string()
3558
- }).nullable()
3559
- }))
3560
- });
3561
3643
  const NonceResponse = zod.z.object({
3562
3644
  nonce: zod.z.string(),
3563
3645
  url: zod.z.string()
@@ -4008,7 +4090,7 @@ class ValOpsHttp extends ValOps {
4008
4090
  };
4009
4091
  }
4010
4092
  }
4011
- async saveSourceFilePatch(path, patch, patchId, parentRef, authorId) {
4093
+ async saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId) {
4012
4094
  const baseSha = await this.getBaseSha();
4013
4095
  return fetch(`${this.contentUrl}/v1/${this.project}/patches`, {
4014
4096
  method: "POST",
@@ -4020,6 +4102,7 @@ class ValOpsHttp extends ValOps {
4020
4102
  path,
4021
4103
  patch,
4022
4104
  authorId,
4105
+ sessionId,
4023
4106
  patchId,
4024
4107
  parentPatchId: parentRef.type === "patch" ? parentRef.patchId : null,
4025
4108
  baseSha,
@@ -4443,31 +4526,6 @@ class ValOpsHttp extends ValOps {
4443
4526
  };
4444
4527
  }
4445
4528
  }
4446
-
4447
- // #region profiles
4448
- async getProfiles() {
4449
- var _res$headers$get6;
4450
- const res = await fetch(`${this.contentUrl}/v1/${this.project}/profiles`, {
4451
- headers: {
4452
- ...this.authHeaders,
4453
- "Content-Type": "application/json"
4454
- }
4455
- });
4456
- if (res.ok) {
4457
- const parsed = ProfilesResponse.safeParse(await res.json());
4458
- if (parsed.error) {
4459
- console.error("Could not parse profiles response", parsed.error);
4460
- throw Error(`Could not get profiles from remote server: wrong format. You might need to upgrade Val.`);
4461
- }
4462
- return parsed.data.profiles;
4463
- }
4464
- if ((_res$headers$get6 = res.headers.get("Content-Type")) !== null && _res$headers$get6 !== void 0 && _res$headers$get6.includes("application/json")) {
4465
- const json = await res.json();
4466
- const message = internal.getErrorMessageFromUnknownJson(json, "Unknown error");
4467
- throw Error(`Could not get profiles (status: ${res.status}): ${message}`);
4468
- }
4469
- throw Error(`Could not get profiles. Got status: ${res.status}`);
4470
- }
4471
4529
  }
4472
4530
 
4473
4531
  const host = process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST;
@@ -4620,6 +4678,16 @@ function hasRemoteFileSchema(schema) {
4620
4678
 
4621
4679
  /* eslint-disable @typescript-eslint/no-unused-vars */
4622
4680
  const ValServer = (valModules, options, callbacks) => {
4681
+ const ProfilesResponse = zod.z.object({
4682
+ profiles: zod.z.array(zod.z.object({
4683
+ profileId: zod.z.string(),
4684
+ fullName: zod.z.string(),
4685
+ email: zod.z.string().optional(),
4686
+ avatar: zod.z.object({
4687
+ url: zod.z.string()
4688
+ }).nullable()
4689
+ }))
4690
+ });
4623
4691
  let serverOps;
4624
4692
  if (options.mode === "fs") {
4625
4693
  serverOps = new ValOpsFS(options.valContentUrl, options.cwd, valModules, {
@@ -5469,10 +5537,11 @@ const ValServer = (valModules, options, callbacks) => {
5469
5537
  }
5470
5538
  const patches = req.body.patches;
5471
5539
  let parentRef = req.body.parentRef;
5540
+ const sessionId = req.body.sessionId ?? null;
5472
5541
  const authorId = "id" in auth ? auth.id : null;
5473
5542
  const newPatchIds = [];
5474
5543
  for (const patch of patches) {
5475
- const createPatchRes = await serverOps.createPatch(patch.path, patch.patch, patch.patchId, parentRef, authorId);
5544
+ const createPatchRes = await serverOps.createPatch(patch.path, patch.patch, patch.patchId, parentRef, sessionId, authorId);
5476
5545
  if (fp.result.isErr(createPatchRes)) {
5477
5546
  if (createPatchRes.error.errorType === "patch-head-conflict") {
5478
5547
  return {
@@ -5833,13 +5902,87 @@ const ValServer = (valModules, options, callbacks) => {
5833
5902
  }
5834
5903
  };
5835
5904
  }
5836
- const profiles = await serverOps.getProfiles();
5837
- return {
5838
- status: 200,
5839
- json: {
5840
- profiles
5905
+ if (!options.project) {
5906
+ return {
5907
+ status: 500,
5908
+ json: {
5909
+ message: "Project is not configured"
5910
+ }
5911
+ };
5912
+ }
5913
+ const authDataRes = await getRemoteFileAuth();
5914
+ if (authDataRes.status !== 200) {
5915
+ if (serverOps instanceof ValOpsFS && authDataRes.json.errorCode === "pat-error") {
5916
+ return {
5917
+ status: 200,
5918
+ json: {
5919
+ profiles: []
5920
+ }
5921
+ };
5922
+ }
5923
+ return {
5924
+ status: 500,
5925
+ json: {
5926
+ message: authDataRes.json.message
5927
+ }
5928
+ };
5929
+ }
5930
+ const authData = authDataRes.json.remoteFileAuth;
5931
+ const execFetch = async headers => {
5932
+ try {
5933
+ const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/profiles`;
5934
+ const upstreamRes = await fetch(upstreamUrl, {
5935
+ method: "GET",
5936
+ headers
5937
+ });
5938
+ if (!upstreamRes.ok) {
5939
+ const text = await upstreamRes.text();
5940
+ const isAuthError = upstreamRes.status === 401 || upstreamRes.status === 403;
5941
+ return {
5942
+ status: isAuthError ? 401 : 500,
5943
+ json: {
5944
+ message: isAuthError ? `Profile authentication failed: ${upstreamRes.status} ${text}` : `Profiles failed: ${upstreamRes.status} ${text}`
5945
+ }
5946
+ };
5947
+ }
5948
+ const parseRes = ProfilesResponse.safeParse(await upstreamRes.json());
5949
+ if (!parseRes.success) {
5950
+ return {
5951
+ status: 500,
5952
+ json: {
5953
+ message: "Could not parse profiles response: " + zodValidationError.fromError(parseRes.error).toString()
5954
+ }
5955
+ };
5956
+ }
5957
+ return {
5958
+ status: 200,
5959
+ json: {
5960
+ profiles: parseRes.data.profiles
5961
+ }
5962
+ };
5963
+ } catch (err) {
5964
+ return {
5965
+ status: 500,
5966
+ json: {
5967
+ message: err instanceof Error ? err.message : "Profiles request failed"
5968
+ }
5969
+ };
5841
5970
  }
5842
5971
  };
5972
+ if (serverOps instanceof ValOpsFS) {
5973
+ return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
5974
+ }
5975
+ if (!options.valSecret) {
5976
+ return {
5977
+ status: 500,
5978
+ json: {
5979
+ message: "Secret is not configured"
5980
+ }
5981
+ };
5982
+ }
5983
+ return withAuth(options.valSecret, cookies, "profiles", data => {
5984
+ return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
5985
+ });
5843
5986
  }
5844
5987
  },
5845
5988
  "/commit-summary": {
@@ -6021,6 +6164,381 @@ const ValServer = (valModules, options, callbacks) => {
6021
6164
  }
6022
6165
  }
6023
6166
  },
6167
+ //#region ai proxy
6168
+ "/ai/initialize": {
6169
+ POST: async req => {
6170
+ const cookies = req.cookies;
6171
+ const auth = getAuth(cookies);
6172
+ if (auth.error) {
6173
+ return {
6174
+ status: 401,
6175
+ json: {
6176
+ message: auth.error
6177
+ }
6178
+ };
6179
+ }
6180
+ if (!options.project) {
6181
+ return {
6182
+ status: 500,
6183
+ json: {
6184
+ message: "Project is not configured"
6185
+ }
6186
+ };
6187
+ }
6188
+ const authDataRes = await getRemoteFileAuth();
6189
+ if (authDataRes.status !== 200) {
6190
+ return {
6191
+ status: 500,
6192
+ json: {
6193
+ message: authDataRes.json.message
6194
+ }
6195
+ };
6196
+ }
6197
+ const authData = authDataRes.json.remoteFileAuth;
6198
+ const execFetch = async headers => {
6199
+ try {
6200
+ const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/initialize`;
6201
+ const upstreamRes = await fetch(upstreamUrl, {
6202
+ method: "POST",
6203
+ headers,
6204
+ body: JSON.stringify({})
6205
+ });
6206
+ if (!upstreamRes.ok) {
6207
+ const text = await upstreamRes.text();
6208
+ return {
6209
+ status: 500,
6210
+ json: {
6211
+ message: `AI initialize failed: ${upstreamRes.status} ${text}`
6212
+ }
6213
+ };
6214
+ }
6215
+ const json = await upstreamRes.json();
6216
+ const wsUrl = options.valContentUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:") + `/v1/${options.project}/ai/connect`;
6217
+ return {
6218
+ status: 200,
6219
+ json: {
6220
+ nonce: json.nonce,
6221
+ wsUrl
6222
+ }
6223
+ };
6224
+ } catch (err) {
6225
+ return {
6226
+ status: 500,
6227
+ json: {
6228
+ message: err instanceof Error ? err.message : "AI initialize error"
6229
+ }
6230
+ };
6231
+ }
6232
+ };
6233
+ if (serverOps instanceof ValOpsFS) {
6234
+ return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
6235
+ }
6236
+ if (!options.valSecret) {
6237
+ return {
6238
+ status: 500,
6239
+ json: {
6240
+ message: "Secret is not configured"
6241
+ }
6242
+ };
6243
+ }
6244
+ return withAuth(options.valSecret, cookies, "ai/initialize", data => {
6245
+ return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
6246
+ });
6247
+ }
6248
+ },
6249
+ "/ai/sessions": {
6250
+ GET: async req => {
6251
+ const cookies = req.cookies;
6252
+ const auth = getAuth(cookies);
6253
+ if (auth.error) {
6254
+ return {
6255
+ status: 401,
6256
+ json: {
6257
+ message: auth.error
6258
+ }
6259
+ };
6260
+ }
6261
+ if (!options.project) {
6262
+ return {
6263
+ status: 500,
6264
+ json: {
6265
+ message: "Project is not configured"
6266
+ }
6267
+ };
6268
+ }
6269
+ const authDataRes = await getRemoteFileAuth();
6270
+ if (authDataRes.status !== 200) {
6271
+ return {
6272
+ status: 500,
6273
+ json: {
6274
+ message: authDataRes.json.message
6275
+ }
6276
+ };
6277
+ }
6278
+ const authData = authDataRes.json.remoteFileAuth;
6279
+ const execFetch = async headers => {
6280
+ try {
6281
+ const SessionsResponse = zod.z.object({
6282
+ sessions: zod.z.array(zod.z.object({
6283
+ id: zod.z.string(),
6284
+ name: zod.z.string().nullable(),
6285
+ createdAt: zod.z.string(),
6286
+ updatedAt: zod.z.string()
6287
+ })),
6288
+ nextCursor: zod.z.object({
6289
+ updatedAt: zod.z.string(),
6290
+ id: zod.z.string()
6291
+ }).nullable().optional()
6292
+ });
6293
+ const params = new URLSearchParams();
6294
+ if (req.query.limit) params.set("limit", req.query.limit);
6295
+ if (req.query.cursor_updatedAt) {
6296
+ params.set("cursor_updatedAt", req.query.cursor_updatedAt);
6297
+ }
6298
+ if (req.query.cursor_id) {
6299
+ params.set("cursor_id", req.query.cursor_id);
6300
+ }
6301
+ const qs = params.toString();
6302
+ const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/sessions${qs ? `?${qs}` : ""}`;
6303
+ const upstreamRes = await fetch(upstreamUrl, {
6304
+ headers
6305
+ });
6306
+ if (!upstreamRes.ok) {
6307
+ const text = await upstreamRes.text();
6308
+ return {
6309
+ status: 500,
6310
+ json: {
6311
+ message: `AI sessions failed: ${upstreamRes.status} ${text}`
6312
+ }
6313
+ };
6314
+ }
6315
+ const json = SessionsResponse.safeParse(await upstreamRes.json());
6316
+ if (!json.success) {
6317
+ return {
6318
+ status: 500,
6319
+ json: {
6320
+ message: "Could not parse AI sessions response: " + zodValidationError.fromError(json.error).toString()
6321
+ }
6322
+ };
6323
+ }
6324
+ return {
6325
+ status: 200,
6326
+ json: json.data
6327
+ };
6328
+ } catch (err) {
6329
+ return {
6330
+ status: 500,
6331
+ json: {
6332
+ message: err instanceof Error ? err.message : "AI sessions error"
6333
+ }
6334
+ };
6335
+ }
6336
+ };
6337
+ if (serverOps instanceof ValOpsFS) {
6338
+ return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
6339
+ }
6340
+ if (!options.valSecret) {
6341
+ return {
6342
+ status: 500,
6343
+ json: {
6344
+ message: "Secret is not configured"
6345
+ }
6346
+ };
6347
+ }
6348
+ return withAuth(options.valSecret, cookies, "ai/sessions", data => {
6349
+ return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
6350
+ });
6351
+ },
6352
+ PATCH: async req => {
6353
+ const cookies = req.cookies;
6354
+ const auth = getAuth(cookies);
6355
+ if (auth.error) {
6356
+ return {
6357
+ status: 401,
6358
+ json: {
6359
+ message: auth.error
6360
+ }
6361
+ };
6362
+ }
6363
+ if (!options.project) {
6364
+ return {
6365
+ status: 500,
6366
+ json: {
6367
+ message: "Project is not configured"
6368
+ }
6369
+ };
6370
+ }
6371
+ const pathParts = (req.path || "").split("/").filter(Boolean);
6372
+ const sessionId = pathParts[0];
6373
+ if (!sessionId) {
6374
+ return {
6375
+ status: 500,
6376
+ json: {
6377
+ message: "Missing sessionId in path"
6378
+ }
6379
+ };
6380
+ }
6381
+ const authDataRes = await getRemoteFileAuth();
6382
+ if (authDataRes.status !== 200) {
6383
+ return {
6384
+ status: 500,
6385
+ json: {
6386
+ message: authDataRes.json.message
6387
+ }
6388
+ };
6389
+ }
6390
+ const authData = authDataRes.json.remoteFileAuth;
6391
+ const execFetch = async headers => {
6392
+ try {
6393
+ const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/sessions/${encodeURIComponent(sessionId)}`;
6394
+ const upstreamRes = await fetch(upstreamUrl, {
6395
+ method: "PATCH",
6396
+ headers,
6397
+ body: JSON.stringify({
6398
+ name: req.body.name
6399
+ })
6400
+ });
6401
+ if (!upstreamRes.ok) {
6402
+ const text = await upstreamRes.text();
6403
+ return {
6404
+ status: 500,
6405
+ json: {
6406
+ message: `AI session rename failed: ${upstreamRes.status} ${text}`
6407
+ }
6408
+ };
6409
+ }
6410
+ return {
6411
+ status: 200,
6412
+ json: {}
6413
+ };
6414
+ } catch (err) {
6415
+ return {
6416
+ status: 500,
6417
+ json: {
6418
+ message: err instanceof Error ? err.message : "AI session rename error"
6419
+ }
6420
+ };
6421
+ }
6422
+ };
6423
+ if (serverOps instanceof ValOpsFS) {
6424
+ return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
6425
+ }
6426
+ if (!options.valSecret) {
6427
+ return {
6428
+ status: 500,
6429
+ json: {
6430
+ message: "Secret is not configured"
6431
+ }
6432
+ };
6433
+ }
6434
+ return withAuth(options.valSecret, cookies, "ai/sessions/rename", data => {
6435
+ return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
6436
+ });
6437
+ }
6438
+ },
6439
+ "/ai/messages": {
6440
+ GET: async req => {
6441
+ const cookies = req.cookies;
6442
+ const auth = getAuth(cookies);
6443
+ if (auth.error) {
6444
+ return {
6445
+ status: 401,
6446
+ json: {
6447
+ message: auth.error
6448
+ }
6449
+ };
6450
+ }
6451
+ if (!options.project) {
6452
+ return {
6453
+ status: 500,
6454
+ json: {
6455
+ message: "Project is not configured"
6456
+ }
6457
+ };
6458
+ }
6459
+ const pathParts = (req.path || "").split("/").filter(Boolean);
6460
+ const sessionId = pathParts[0];
6461
+ if (!sessionId) {
6462
+ return {
6463
+ status: 500,
6464
+ json: {
6465
+ message: "Missing sessionId in path"
6466
+ }
6467
+ };
6468
+ }
6469
+ const authDataRes = await getRemoteFileAuth();
6470
+ if (authDataRes.status !== 200) {
6471
+ return {
6472
+ status: 500,
6473
+ json: {
6474
+ message: authDataRes.json.message
6475
+ }
6476
+ };
6477
+ }
6478
+ const authData = authDataRes.json.remoteFileAuth;
6479
+ const execFetch = async headers => {
6480
+ try {
6481
+ const SessionMessagesResponse = zod.z.object({
6482
+ messages: zod.z.array(zod.z.object({
6483
+ role: zod.z.string(),
6484
+ content: zod.z.string()
6485
+ })),
6486
+ nextCursor: zod.z.object({
6487
+ updatedAt: zod.z.string(),
6488
+ id: zod.z.string()
6489
+ }).nullable().optional()
6490
+ });
6491
+ const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/sessions/${encodeURIComponent(sessionId)}/messages`;
6492
+ const upstreamRes = await fetch(upstreamUrl, {
6493
+ headers
6494
+ });
6495
+ if (!upstreamRes.ok) {
6496
+ const text = await upstreamRes.text();
6497
+ return {
6498
+ status: 500,
6499
+ json: {
6500
+ message: `AI session messages failed: ${upstreamRes.status} ${text}`
6501
+ }
6502
+ };
6503
+ }
6504
+ const json = SessionMessagesResponse.safeParse(await upstreamRes.json());
6505
+ if (!json.success) {
6506
+ return {
6507
+ status: 500,
6508
+ json: {
6509
+ message: "Could not parse AI session messages response: " + zodValidationError.fromError(json.error).toString()
6510
+ }
6511
+ };
6512
+ }
6513
+ return {
6514
+ status: 200,
6515
+ json: json.data
6516
+ };
6517
+ } catch (err) {
6518
+ return {
6519
+ status: 500,
6520
+ json: {
6521
+ message: err instanceof Error ? err.message : "AI session messages error"
6522
+ }
6523
+ };
6524
+ }
6525
+ };
6526
+ if (serverOps instanceof ValOpsFS) {
6527
+ return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
6528
+ }
6529
+ if (!options.valSecret) {
6530
+ return {
6531
+ status: 500,
6532
+ json: {
6533
+ message: "Secret is not configured"
6534
+ }
6535
+ };
6536
+ }
6537
+ return withAuth(options.valSecret, cookies, "ai/sessions/messages", data => {
6538
+ return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
6539
+ });
6540
+ }
6541
+ },
6024
6542
  //#region files
6025
6543
  "/files": {
6026
6544
  GET: async req => {
@@ -6242,6 +6760,21 @@ async function withAuth(secret, cookies, errorMessageType, handler) {
6242
6760
  };
6243
6761
  }
6244
6762
  }
6763
+ function getProfileAuthHeaders(auth, data, type) {
6764
+ if ("pat" in auth) {
6765
+ return {
6766
+ "x-val-pat": auth.pat,
6767
+ "Content-Type": type
6768
+ };
6769
+ }
6770
+ if ("apiKey" in auth && data) {
6771
+ return {
6772
+ ...getAuthHeaders(auth.apiKey, type),
6773
+ "x-val-profile-id": data.sub
6774
+ };
6775
+ }
6776
+ throw new Error("Invalid auth");
6777
+ }
6245
6778
  function getAuthHeaders(token, type) {
6246
6779
  if (!type) {
6247
6780
  return {
@@ -6769,6 +7302,9 @@ class ValFSHost {
6769
7302
  readFile(fileName) {
6770
7303
  return this.valFS.readFile(fileName);
6771
7304
  }
7305
+ readBuffer(fileName) {
7306
+ return this.valFS.readBuffer(fileName);
7307
+ }
6772
7308
  realpath(path) {
6773
7309
  return this.valFS.realpath(path);
6774
7310
  }
@@ -7243,6 +7779,88 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
7243
7779
  metadata: v.metadata
7244
7780
  } : value
7245
7781
  });
7782
+ } else if (fix === "images:check-all-files" || fix === "files:check-all-files") {
7783
+ if (!moduleSource || typeof moduleSource !== "object") {
7784
+ remainingErrors.push({
7785
+ ...validationError,
7786
+ message: "Unexpected error while checking gallery metadata (no moduleSource)",
7787
+ fixes: undefined
7788
+ });
7789
+ continue;
7790
+ }
7791
+ const gallerySource = moduleSource;
7792
+ for (const [entryKey, storedEntry] of Object.entries(gallerySource)) {
7793
+ const filename = path__namespace["default"].join(config.projectRoot, entryKey);
7794
+ let buffer;
7795
+ try {
7796
+ buffer = fs__default["default"].readFileSync(filename);
7797
+ } catch {
7798
+ if (apply) {
7799
+ const removePath = patch.sourceToPatchPath(sourcePath).concat([entryKey]);
7800
+ if (patch.isNotRoot(removePath)) {
7801
+ patch$1.push({
7802
+ op: "remove",
7803
+ path: removePath
7804
+ });
7805
+ }
7806
+ } else {
7807
+ remainingErrors.push({
7808
+ ...validationError,
7809
+ message: `Could not read file: ${filename} - file might not exist or can not be accessed`,
7810
+ fixes: undefined
7811
+ });
7812
+ }
7813
+ continue;
7814
+ }
7815
+ if (fix === "images:check-all-files") {
7816
+ const actualMetadata = await extractImageMetadata(filename, buffer);
7817
+ const stored = storedEntry;
7818
+ const metadataIsCorrect = stored.width === actualMetadata.width && stored.height === actualMetadata.height && stored.mimeType === actualMetadata.mimeType;
7819
+ if (!metadataIsCorrect) {
7820
+ if (apply) {
7821
+ patch$1.push({
7822
+ op: "replace",
7823
+ path: patch.sourceToPatchPath(sourcePath).concat([entryKey]),
7824
+ value: {
7825
+ ...stored,
7826
+ width: actualMetadata.width ?? 0,
7827
+ height: actualMetadata.height ?? 0,
7828
+ mimeType: actualMetadata.mimeType ?? "application/octet-stream"
7829
+ }
7830
+ });
7831
+ } else {
7832
+ remainingErrors.push({
7833
+ ...validationError,
7834
+ 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.`
7835
+ });
7836
+ }
7837
+ }
7838
+ } else if (fix === "files:check-all-files") {
7839
+ const actualMetadata = await extractFileMetadata(filename);
7840
+ const stored = storedEntry;
7841
+ const metadataIsCorrect = stored.mimeType === actualMetadata.mimeType;
7842
+ if (!metadataIsCorrect) {
7843
+ if (apply) {
7844
+ patch$1.push({
7845
+ op: "replace",
7846
+ path: patch.sourceToPatchPath(sourcePath).concat([entryKey]),
7847
+ value: {
7848
+ ...stored,
7849
+ mimeType: actualMetadata.mimeType ?? "application/octet-stream"
7850
+ }
7851
+ });
7852
+ } else {
7853
+ remainingErrors.push({
7854
+ ...validationError,
7855
+ message: `File metadata for '${entryKey}' has incorrect mimeType: '${stored.mimeType ?? "<empty>"}' vs '${actualMetadata.mimeType}'. Use --fix to update.`
7856
+ });
7857
+ }
7858
+ }
7859
+ } else {
7860
+ const exhaustiveCheck = fix;
7861
+ throw new Error(`Internal error: unhandled fix type ${exhaustiveCheck}`);
7862
+ }
7863
+ }
7246
7864
  } else if (fix === "file:check-remote" || fix === "image:check-remote") {
7247
7865
  const v = getRemoteValueFromValidationError(validationError);
7248
7866
  if (!v.success) {