@valbuild/server 0.63.2 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,12 +8,12 @@ import fsPath__default from 'path';
8
8
  import fs, { promises } from 'fs';
9
9
  import { transform } from 'sucrase';
10
10
  import { VAL_CSS_PATH, VAL_APP_ID, VAL_OVERLAY_ID } from '@valbuild/ui';
11
+ import { VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE, VAL_SESSION_COOKIE, Api } from '@valbuild/shared/internal';
11
12
  import { createUIRequestHandler } from '@valbuild/ui/server';
12
- import { MIME_TYPES_TO_EXT, filenameToMimeType, VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE as VAL_STATE_COOKIE$1, VAL_SESSION_COOKIE as VAL_SESSION_COOKIE$1 } from '@valbuild/shared/internal';
13
13
  import crypto$1 from 'crypto';
14
14
  import z$1, { z } from 'zod';
15
15
  import sizeOf from 'image-size';
16
- import { fromError } from 'zod-validation-error';
16
+ import { fromError, fromZodError } from 'zod-validation-error';
17
17
 
18
18
  class ValSyntaxError {
19
19
  constructor(message, node) {
@@ -1249,10 +1249,10 @@ async function extractImageMetadata(filename, input) {
1249
1249
  let mimeType = null;
1250
1250
  if (imageSize.type) {
1251
1251
  const possibleMimeType = `image/${imageSize.type}`;
1252
- if (MIME_TYPES_TO_EXT[possibleMimeType]) {
1252
+ if (Internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
1253
1253
  mimeType = possibleMimeType;
1254
1254
  }
1255
- const filenameBasedLookup = filenameToMimeType(filename);
1255
+ const filenameBasedLookup = Internal.filenameToMimeType(filename);
1256
1256
  if (filenameBasedLookup) {
1257
1257
  mimeType = filenameBasedLookup;
1258
1258
  }
@@ -1282,7 +1282,7 @@ function getSha256(mimeType, input) {
1282
1282
  `data:${mimeType};base64,${input.toString("base64")}`));
1283
1283
  }
1284
1284
  async function extractFileMetadata(filename, input) {
1285
- let mimeType = filenameToMimeType(filename);
1285
+ let mimeType = Internal.filenameToMimeType(filename);
1286
1286
  if (!mimeType) {
1287
1287
  mimeType = "application/octet-stream";
1288
1288
  }
@@ -1490,10 +1490,14 @@ class ValOps {
1490
1490
  if (!errors[path]) {
1491
1491
  errors[path] = [];
1492
1492
  }
1493
- errors[path].push({
1493
+ errors[path].push(...patches.map(({
1494
+ patchId
1495
+ }) => ({
1496
+ patchId,
1494
1497
  invalidPath: true,
1498
+ skipped: true,
1495
1499
  error: new PatchError(`Module at path: '${path}' not found`)
1496
- });
1500
+ })));
1497
1501
  }
1498
1502
  patchedSources[path] = sources[path];
1499
1503
  for (const {
@@ -1502,15 +1506,17 @@ class ValOps {
1502
1506
  if (errors[path]) {
1503
1507
  errors[path].push({
1504
1508
  patchId: patchId,
1509
+ skipped: true,
1505
1510
  error: new PatchError(`Cannot apply patch: previous errors exists`)
1506
1511
  });
1507
1512
  } else {
1508
1513
  const patchData = analysis.patches[patchId];
1509
1514
  if (!patchData) {
1510
- errors[path].push({
1515
+ errors[path] = [{
1511
1516
  patchId: patchId,
1517
+ skipped: false,
1512
1518
  error: new PatchError(`Patch not found`)
1513
- });
1519
+ }];
1514
1520
  continue;
1515
1521
  }
1516
1522
  const applicableOps = [];
@@ -1539,6 +1545,7 @@ class ValOps {
1539
1545
  }
1540
1546
  errors[path].push({
1541
1547
  patchId: patchId,
1548
+ skipped: false,
1542
1549
  error: patchRes.error
1543
1550
  });
1544
1551
  } else {
@@ -1807,7 +1814,7 @@ class ValOps {
1807
1814
  let sourceFileText = unescape(tsSourceFile.getText(tsSourceFile).replace(/\\u/g, "%u"));
1808
1815
  if ((_this$options = this.options) !== null && _this$options !== void 0 && _this$options.formatter) {
1809
1816
  try {
1810
- sourceFileText = this.options.formatter(sourceFileText, path);
1817
+ sourceFileText = await this.options.formatter(sourceFileText, path);
1811
1818
  } catch (err) {
1812
1819
  errors.push({
1813
1820
  message: "Could not format source file: " + (err instanceof Error ? err.message : "Unknown error")
@@ -2237,7 +2244,7 @@ class ValOpsFS extends ValOps {
2237
2244
  throw new Error("Could not parse patch id from file name. Files found: " + patchJsonFiles.join(", "));
2238
2245
  }
2239
2246
  const patchId = patchIdNum.toString();
2240
- if (includes && !includes.includes(patchId)) {
2247
+ if (includes && includes.length > 0 && !includes.includes(patchId)) {
2241
2248
  continue;
2242
2249
  }
2243
2250
  const parsedFSPatchRes = this.parseJsonFile(this.getPatchFilePath(patchId), FSPatch);
@@ -2266,22 +2273,23 @@ class ValOpsFS extends ValOps {
2266
2273
  patches
2267
2274
  };
2268
2275
  }
2269
- async getPatchOpsById(patchIds) {
2270
- return this.readPatches(patchIds);
2271
- }
2272
- async findPatches(filters) {
2276
+ async fetchPatches(filters) {
2273
2277
  const patches = {};
2274
2278
  const errors = {};
2275
2279
  const {
2276
2280
  errors: allErrors,
2277
2281
  patches: allPatches
2278
- } = await this.readPatches();
2282
+ } = await this.readPatches(filters.patchIds);
2279
2283
  for (const [patchIdS, patch] of Object.entries(allPatches)) {
2280
2284
  const patchId = patchIdS;
2281
2285
  if (filters.authors && !(patch.authorId === null || filters.authors.includes(patch.authorId))) {
2282
2286
  continue;
2283
2287
  }
2288
+ if (filters.moduleFilePaths && !filters.moduleFilePaths.includes(patch.path)) {
2289
+ continue;
2290
+ }
2284
2291
  patches[patchId] = {
2292
+ patch: filters.omitPatch ? undefined : patch.patch,
2285
2293
  path: patch.path,
2286
2294
  createdAt: patch.createdAt,
2287
2295
  authorId: patch.authorId,
@@ -2737,16 +2745,13 @@ const BasePatchResponse = z.object({
2737
2745
  });
2738
2746
  const GetPatches = z.object({
2739
2747
  patches: z.array(z.intersection(z.object({
2740
- patch: Patch
2748
+ patch: Patch.optional()
2741
2749
  }), BasePatchResponse)),
2742
2750
  errors: z.array(z.object({
2743
2751
  patchId: PatchId.optional(),
2744
2752
  message: z.string()
2745
2753
  })).optional()
2746
2754
  });
2747
- const SearchPatches = z.object({
2748
- patches: z.array(BasePatchResponse)
2749
- });
2750
2755
  const FilesResponse = z.object({
2751
2756
  files: z.array(z.union([z.object({
2752
2757
  filePath: z.string(),
@@ -2805,13 +2810,29 @@ class ValOpsHttp extends ValOps {
2805
2810
  async onInit() {
2806
2811
  // TODO: unused for now. Implement or remove
2807
2812
  }
2808
- async getPatchOpsById(patchIds) {
2809
- const params = new URLSearchParams();
2810
- params.set("branch", this.branch);
2811
- if (patchIds.length > 0) {
2812
- params.set("patch_ids", encodeURIComponent(patchIds.join(",")));
2813
+ async fetchPatches(filters) {
2814
+ const params = [];
2815
+ params.push(["branch", this.branch]);
2816
+ if (filters.patchIds) {
2817
+ for (const patchId of filters.patchIds) {
2818
+ params.push(["patch_id", patchId]);
2819
+ }
2820
+ }
2821
+ if (filters.authors) {
2822
+ for (const author of filters.authors) {
2823
+ params.push(["author_id", author]);
2824
+ }
2825
+ }
2826
+ if (filters.omitPatch) {
2827
+ params.push(["omit_patch", "true"]);
2828
+ }
2829
+ if (filters.moduleFilePaths) {
2830
+ for (const moduleFilePath of filters.moduleFilePaths) {
2831
+ params.push(["module_file_path", moduleFilePath]);
2832
+ }
2813
2833
  }
2814
- return fetch(`${this.hostUrl}/v1/${this.project}/patches${params.size > 0 ? "?" + params : ""}`, {
2834
+ const searchParams = new URLSearchParams(params);
2835
+ return fetch(`${this.hostUrl}/v1/${this.project}/patches${searchParams.size > 0 ? `?${searchParams.toString()}` : ""}`, {
2815
2836
  headers: {
2816
2837
  ...this.authHeaders,
2817
2838
  "Content-Type": "application/json"
@@ -2859,55 +2880,6 @@ class ValOpsHttp extends ValOps {
2859
2880
  };
2860
2881
  });
2861
2882
  }
2862
- async findPatches(filters) {
2863
- const params = new URLSearchParams();
2864
- params.set("branch", this.branch);
2865
- if (filters.authors && filters.authors.length > 0) {
2866
- params.set("author_ids", encodeURIComponent(filters.authors.join(",")));
2867
- }
2868
- return fetch(`${this.hostUrl}/v1/${this.project}/search/patches${params.size > 0 ? "?" + params : ""}`, {
2869
- headers: {
2870
- ...this.authHeaders,
2871
- "Content-Type": "application/json"
2872
- }
2873
- }).then(async res => {
2874
- const patches = {};
2875
- if (res.ok) {
2876
- const parsed = SearchPatches.safeParse(await res.json());
2877
- if (parsed.success) {
2878
- for (const patchesRes of parsed.data.patches) {
2879
- patches[patchesRes.patchId] = {
2880
- path: patchesRes.path,
2881
- authorId: patchesRes.authorId,
2882
- createdAt: patchesRes.createdAt,
2883
- appliedAt: patchesRes.applied && {
2884
- baseSha: patchesRes.applied.baseSha,
2885
- timestamp: patchesRes.applied.appliedAt,
2886
- git: {
2887
- commitSha: patchesRes.applied.commitSha
2888
- }
2889
- }
2890
- };
2891
- }
2892
- return {
2893
- patches
2894
- };
2895
- }
2896
- return {
2897
- patches,
2898
- error: {
2899
- message: `Could not parse search patches response. Error: ${fromError(parsed.error)}`
2900
- }
2901
- };
2902
- }
2903
- return {
2904
- patches,
2905
- error: {
2906
- message: "Could not find patches. HTTP error: " + res.status + " " + res.statusText
2907
- }
2908
- };
2909
- });
2910
- }
2911
2883
  async saveSourceFilePatch(path, patch, authorId) {
2912
2884
  return fetch(`${this.hostUrl}/v1/${this.project}/patches`, {
2913
2885
  method: "POST",
@@ -3246,251 +3218,59 @@ class ValOpsHttp extends ValOps {
3246
3218
  }
3247
3219
 
3248
3220
  /* eslint-disable @typescript-eslint/no-unused-vars */
3249
- class ValServer {
3250
- constructor(valModules, options, callbacks) {
3251
- this.valModules = valModules;
3252
- this.options = options;
3253
- this.callbacks = callbacks;
3254
- if (options.mode === "fs") {
3255
- this.serverOps = new ValOpsFS(options.cwd, valModules, {
3256
- formatter: options.formatter
3257
- });
3258
- } else if (options.mode === "http") {
3259
- this.serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
3260
- formatter: options.formatter,
3261
- root: options.root
3262
- });
3263
- } else {
3264
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3265
- throw new Error("Invalid mode: " + (options === null || options === void 0 ? void 0 : options.mode));
3266
- }
3267
- }
3268
-
3269
- //#region auth
3270
- async enable(query) {
3271
- const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
3272
- if (typeof redirectToRes !== "string") {
3273
- return redirectToRes;
3274
- }
3275
- await this.callbacks.onEnable(true);
3276
- return {
3277
- cookies: {
3278
- [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
3279
- },
3280
- status: 302,
3281
- redirectTo: redirectToRes
3282
- };
3283
- }
3284
- async disable(query) {
3285
- const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
3286
- if (typeof redirectToRes !== "string") {
3287
- return redirectToRes;
3288
- }
3289
- await this.callbacks.onDisable(true);
3290
- return {
3291
- cookies: {
3292
- [VAL_ENABLE_COOKIE_NAME]: {
3293
- value: "false"
3294
- }
3295
- },
3296
- status: 302,
3297
- redirectTo: redirectToRes
3298
- };
3299
- }
3300
- async authorize(query) {
3301
- if (typeof query.redirect_to !== "string") {
3302
- return {
3303
- status: 400,
3304
- json: {
3305
- message: "Missing redirect_to query param"
3306
- }
3307
- };
3308
- }
3309
- const token = crypto.randomUUID();
3310
- const redirectUrl = new URL(query.redirect_to);
3311
- const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
3312
- await this.callbacks.onEnable(true);
3313
- return {
3314
- cookies: {
3315
- [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3316
- [VAL_STATE_COOKIE$1]: {
3317
- value: createStateCookie({
3318
- redirect_to: query.redirect_to,
3319
- token
3320
- }),
3321
- options: {
3322
- httpOnly: true,
3323
- sameSite: "lax",
3324
- expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
3325
- }
3326
- }
3327
- },
3328
- status: 302,
3329
- redirectTo: appAuthorizeUrl
3330
- };
3221
+ const ValServer = (valModules, options, callbacks) => {
3222
+ let serverOps;
3223
+ if (options.mode === "fs") {
3224
+ serverOps = new ValOpsFS(options.cwd, valModules, {
3225
+ formatter: options.formatter
3226
+ });
3227
+ } else if (options.mode === "http") {
3228
+ serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
3229
+ formatter: options.formatter,
3230
+ root: options.root
3231
+ });
3232
+ } else {
3233
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3234
+ throw new Error("Invalid mode: " + (options === null || options === void 0 ? void 0 : options.mode));
3331
3235
  }
3332
- async callback(query, cookies) {
3333
- if (!this.options.project) {
3334
- return {
3335
- status: 302,
3336
- cookies: {
3337
- [VAL_STATE_COOKIE$1]: {
3338
- value: null
3339
- }
3340
- },
3341
- redirectTo: this.getAppErrorUrl("Project is not set")
3342
- };
3343
- }
3344
- if (!this.options.valSecret) {
3345
- return {
3346
- status: 302,
3347
- cookies: {
3348
- [VAL_STATE_COOKIE$1]: {
3349
- value: null
3350
- }
3351
- },
3352
- redirectTo: this.getAppErrorUrl("Secret is not set")
3353
- };
3354
- }
3355
- const {
3356
- success: callbackReqSuccess,
3357
- error: callbackReqError
3358
- } = verifyCallbackReq(cookies[VAL_STATE_COOKIE$1], query);
3359
- if (callbackReqError !== null) {
3360
- return {
3361
- status: 302,
3362
- cookies: {
3363
- [VAL_STATE_COOKIE$1]: {
3364
- value: null
3365
- }
3366
- },
3367
- redirectTo: this.getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`)
3368
- };
3369
- }
3370
- const data = await this.consumeCode(callbackReqSuccess.code);
3371
- if (data === null) {
3372
- return {
3373
- status: 302,
3374
- cookies: {
3375
- [VAL_STATE_COOKIE$1]: {
3376
- value: null
3377
- }
3378
- },
3379
- redirectTo: this.getAppErrorUrl("Failed to exchange code for user")
3380
- };
3381
- }
3382
- const exp = getExpire();
3383
- const valSecret = this.options.valSecret;
3384
- if (!valSecret) {
3385
- return {
3386
- status: 302,
3387
- cookies: {
3388
- [VAL_STATE_COOKIE$1]: {
3389
- value: null
3390
- }
3391
- },
3392
- redirectTo: this.getAppErrorUrl("Setup is not correct: secret is missing")
3393
- };
3236
+ const getAuthorizeUrl = (publicValApiRe, token) => {
3237
+ if (!options.project) {
3238
+ throw new Error("Project is not set");
3394
3239
  }
3395
- const cookie = encodeJwt({
3396
- ...data,
3397
- exp // this is the client side exp
3398
- }, valSecret);
3399
- return {
3400
- status: 302,
3401
- cookies: {
3402
- [VAL_STATE_COOKIE$1]: {
3403
- value: null
3404
- },
3405
- [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3406
- [VAL_SESSION_COOKIE$1]: {
3407
- value: cookie,
3408
- options: {
3409
- httpOnly: true,
3410
- sameSite: "strict",
3411
- path: "/",
3412
- secure: true,
3413
- expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
3414
- }
3415
- }
3416
- },
3417
- redirectTo: callbackReqSuccess.redirect_uri || "/"
3418
- };
3419
- }
3420
- async session(cookies) {
3421
- if (this.serverOps instanceof ValOpsFS) {
3422
- return {
3423
- status: 200,
3424
- json: {
3425
- mode: "local",
3426
- enabled: await this.callbacks.isEnabled()
3427
- }
3428
- };
3240
+ if (!options.valBuildUrl) {
3241
+ throw new Error("Val build url is not set");
3429
3242
  }
3430
- if (!this.options.project) {
3431
- return {
3432
- status: 500,
3433
- json: {
3434
- message: "Project is not set"
3435
- }
3436
- };
3243
+ const url = new URL(`/auth/${options.project}/authorize`, options.valBuildUrl);
3244
+ url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRe}/callback`));
3245
+ url.searchParams.set("state", token);
3246
+ return url.toString();
3247
+ };
3248
+ const getAppErrorUrl = error => {
3249
+ if (!options.project) {
3250
+ throw new Error("Project is not set");
3437
3251
  }
3438
- if (!this.options.valSecret) {
3439
- return {
3440
- status: 500,
3441
- json: {
3442
- message: "Secret is not set"
3443
- }
3444
- };
3252
+ if (!options.valBuildUrl) {
3253
+ throw new Error("Val build url is not set");
3445
3254
  }
3446
- return withAuth(this.options.valSecret, cookies, "session", async data => {
3447
- if (!this.options.valBuildUrl) {
3448
- return {
3449
- status: 500,
3450
- json: {
3451
- message: "Val is not correctly setup. Build url is missing"
3452
- }
3453
- };
3454
- }
3455
- const url = new URL(`/api/val/${this.options.project}/auth/session`, this.options.valBuildUrl);
3456
- const fetchRes = await fetch(url, {
3457
- headers: getAuthHeaders(data.token, "application/json")
3458
- });
3459
- if (fetchRes.status === 200) {
3460
- return {
3461
- status: fetchRes.status,
3462
- json: {
3463
- mode: "proxy",
3464
- enabled: await this.callbacks.isEnabled(),
3465
- ...(await fetchRes.json())
3466
- }
3467
- };
3468
- } else {
3469
- return {
3470
- status: fetchRes.status,
3471
- json: {
3472
- message: "Failed to authorize",
3473
- ...(await fetchRes.json())
3474
- }
3475
- };
3476
- }
3477
- });
3478
- }
3479
- async consumeCode(code) {
3480
- if (!this.options.project) {
3255
+ const url = new URL(`/auth/${options.project}/authorize`, options.valBuildUrl);
3256
+ url.searchParams.set("error", encodeURIComponent(error));
3257
+ return url.toString();
3258
+ };
3259
+ const consumeCode = async code => {
3260
+ if (!options.project) {
3481
3261
  throw new Error("Project is not set");
3482
3262
  }
3483
- if (!this.options.valBuildUrl) {
3263
+ if (!options.valBuildUrl) {
3484
3264
  throw new Error("Val build url is not set");
3485
3265
  }
3486
- const url = new URL(`/api/val/${this.options.project}/auth/token`, this.options.valBuildUrl);
3266
+ const url = new URL(`/api/val/${options.project}/auth/token`, options.valBuildUrl);
3487
3267
  url.searchParams.set("code", encodeURIComponent(code));
3488
- if (!this.options.apiKey) {
3268
+ if (!options.apiKey) {
3489
3269
  return null;
3490
3270
  }
3491
3271
  return fetch(url, {
3492
3272
  method: "POST",
3493
- headers: getAuthHeaders(this.options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
3273
+ headers: getAuthHeaders(options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
3494
3274
  }).then(async res => {
3495
3275
  if (res.status === 200) {
3496
3276
  const token = await res.text();
@@ -3510,34 +3290,11 @@ class ValServer {
3510
3290
  console.debug("Failed to get user from code: ", err);
3511
3291
  return null;
3512
3292
  });
3513
- }
3514
- getAuthorizeUrl(publicValApiRe, token) {
3515
- if (!this.options.project) {
3516
- throw new Error("Project is not set");
3517
- }
3518
- if (!this.options.valBuildUrl) {
3519
- throw new Error("Val build url is not set");
3520
- }
3521
- const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
3522
- url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRe}/callback`));
3523
- url.searchParams.set("state", token);
3524
- return url.toString();
3525
- }
3526
- getAppErrorUrl(error) {
3527
- if (!this.options.project) {
3528
- throw new Error("Project is not set");
3529
- }
3530
- if (!this.options.valBuildUrl) {
3531
- throw new Error("Val build url is not set");
3532
- }
3533
- const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
3534
- url.searchParams.set("error", encodeURIComponent(error));
3535
- return url.toString();
3536
- }
3537
- getAuth(cookies) {
3538
- const cookie = cookies[VAL_SESSION_COOKIE$1];
3539
- if (!this.options.valSecret) {
3540
- if (this.serverOps instanceof ValOpsFS) {
3293
+ };
3294
+ const getAuth = cookies => {
3295
+ const cookie = cookies[VAL_SESSION_COOKIE];
3296
+ if (!options.valSecret) {
3297
+ if (serverOps instanceof ValOpsFS) {
3541
3298
  return {
3542
3299
  error: null,
3543
3300
  id: null
@@ -3549,9 +3306,9 @@ class ValServer {
3549
3306
  }
3550
3307
  }
3551
3308
  if (typeof cookie === "string") {
3552
- const decodedToken = decodeJwt(cookie, this.options.valSecret);
3309
+ const decodedToken = decodeJwt(cookie, options.valSecret);
3553
3310
  if (!decodedToken) {
3554
- if (this.serverOps instanceof ValOpsFS) {
3311
+ if (serverOps instanceof ValOpsFS) {
3555
3312
  return {
3556
3313
  error: null,
3557
3314
  id: null
@@ -3563,7 +3320,7 @@ class ValServer {
3563
3320
  }
3564
3321
  const verification = IntegratedServerJwtPayload.safeParse(decodedToken);
3565
3322
  if (!verification.success) {
3566
- if (this.serverOps instanceof ValOpsFS) {
3323
+ if (serverOps instanceof ValOpsFS) {
3567
3324
  return {
3568
3325
  error: null,
3569
3326
  id: null
@@ -3577,7 +3334,7 @@ class ValServer {
3577
3334
  id: verification.data.sub
3578
3335
  };
3579
3336
  } else {
3580
- if (this.serverOps instanceof ValOpsFS) {
3337
+ if (serverOps instanceof ValOpsFS) {
3581
3338
  return {
3582
3339
  error: null,
3583
3340
  id: null
@@ -3587,420 +3344,687 @@ class ValServer {
3587
3344
  error: "Login required: cookie not found"
3588
3345
  };
3589
3346
  }
3590
- }
3591
- async logout() {
3592
- return {
3593
- status: 200,
3594
- cookies: {
3595
- [VAL_SESSION_COOKIE$1]: {
3596
- value: null
3597
- },
3598
- [VAL_STATE_COOKIE$1]: {
3599
- value: null
3347
+ };
3348
+ return {
3349
+ //#region auth
3350
+ "/enable": {
3351
+ GET: async req => {
3352
+ const query = req.query;
3353
+ const redirectToRes = getRedirectUrl(query, options.valEnableRedirectUrl);
3354
+ if (typeof redirectToRes !== "string") {
3355
+ return redirectToRes;
3600
3356
  }
3357
+ await callbacks.onEnable(true);
3358
+ return {
3359
+ cookies: {
3360
+ [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
3361
+ },
3362
+ status: 302,
3363
+ redirectTo: redirectToRes
3364
+ };
3601
3365
  }
3602
- };
3603
- }
3604
-
3605
- //#region patches
3606
- async getPatches(query, cookies) {
3607
- const auth = this.getAuth(cookies);
3608
- if (auth.error) {
3609
- return {
3610
- status: 401,
3611
- json: {
3612
- message: auth.error
3366
+ },
3367
+ "/disable": {
3368
+ GET: async req => {
3369
+ const query = req.query;
3370
+ const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
3371
+ if (typeof redirectToRes !== "string") {
3372
+ return redirectToRes;
3613
3373
  }
3614
- };
3615
- }
3616
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3617
- return {
3618
- status: 401,
3619
- json: {
3620
- message: "Unauthorized"
3374
+ await callbacks.onDisable(true);
3375
+ return {
3376
+ cookies: {
3377
+ [VAL_ENABLE_COOKIE_NAME]: {
3378
+ value: "false"
3379
+ }
3380
+ },
3381
+ status: 302,
3382
+ redirectTo: redirectToRes
3383
+ };
3384
+ }
3385
+ },
3386
+ "/authorize": {
3387
+ GET: async req => {
3388
+ const query = req.query;
3389
+ if (typeof query.redirect_to !== "string") {
3390
+ return {
3391
+ status: 400,
3392
+ json: {
3393
+ message: "Missing redirect_to query param"
3394
+ }
3395
+ };
3621
3396
  }
3622
- };
3623
- }
3624
- const authors = query.authors;
3625
- const patches = await this.serverOps.findPatches({
3626
- authors
3627
- });
3628
- if (patches.errors && Object.keys(patches.errors).length > 0) {
3629
- console.error("Val: Failed to get patches", patches.errors);
3630
- return {
3631
- status: 500,
3632
- json: {
3633
- message: "Failed to get patches",
3634
- details: patches.errors
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
+ };
3419
+ }
3420
+ },
3421
+ "/callback": {
3422
+ GET: async req => {
3423
+ const cookies = req.cookies;
3424
+ const query = req.query;
3425
+ if (!options.project) {
3426
+ return {
3427
+ status: 302,
3428
+ cookies: {
3429
+ [VAL_STATE_COOKIE]: {
3430
+ value: null
3431
+ }
3432
+ },
3433
+ redirectTo: getAppErrorUrl("Project is not set")
3434
+ };
3635
3435
  }
3636
- };
3637
- }
3638
- const res = {};
3639
- for (const [patchIdS, patchData] of Object.entries(patches.patches)) {
3640
- var _patchData$appliedAt;
3641
- const patchId = patchIdS;
3642
- if (!res[patchData.path]) {
3643
- res[patchData.path] = [];
3644
- }
3645
- res[patchData.path].push({
3646
- patch_id: patchId,
3647
- created_at: patchData.createdAt,
3648
- applied_at_base_sha: ((_patchData$appliedAt = patchData.appliedAt) === null || _patchData$appliedAt === void 0 ? void 0 : _patchData$appliedAt.baseSha) || null,
3649
- author: patchData.authorId ?? undefined
3650
- });
3651
- }
3652
- return {
3653
- status: 200,
3654
- json: res
3655
- };
3656
- }
3657
- async deletePatches(query, cookies) {
3658
- const auth = this.getAuth(cookies);
3659
- if (auth.error) {
3660
- return {
3661
- status: 401,
3662
- json: {
3663
- message: auth.error
3436
+ if (!options.valSecret) {
3437
+ return {
3438
+ status: 302,
3439
+ cookies: {
3440
+ [VAL_STATE_COOKIE]: {
3441
+ value: null
3442
+ }
3443
+ },
3444
+ redirectTo: getAppErrorUrl("Secret is not set")
3445
+ };
3664
3446
  }
3665
- };
3666
- }
3667
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3668
- return {
3669
- status: 401,
3670
- json: {
3671
- message: "Unauthorized"
3447
+ const {
3448
+ success: callbackReqSuccess,
3449
+ error: callbackReqError
3450
+ } = verifyCallbackReq(cookies[VAL_STATE_COOKIE], query);
3451
+ if (callbackReqError !== null) {
3452
+ return {
3453
+ status: 302,
3454
+ cookies: {
3455
+ [VAL_STATE_COOKIE]: {
3456
+ value: null
3457
+ }
3458
+ },
3459
+ redirectTo: getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`)
3460
+ };
3672
3461
  }
3673
- };
3674
- }
3675
- const ids = query.id;
3676
- const deleteRes = await this.serverOps.deletePatches(ids);
3677
- if (deleteRes.errors && Object.keys(deleteRes.errors).length > 0) {
3678
- console.error("Val: Failed to delete patches", deleteRes.errors);
3679
- return {
3680
- status: 500,
3681
- json: {
3682
- message: "Failed to delete patches",
3683
- details: deleteRes.errors
3462
+ const data = await consumeCode(callbackReqSuccess.code);
3463
+ if (data === null) {
3464
+ return {
3465
+ status: 302,
3466
+ cookies: {
3467
+ [VAL_STATE_COOKIE]: {
3468
+ value: null
3469
+ }
3470
+ },
3471
+ redirectTo: getAppErrorUrl("Failed to exchange code for user")
3472
+ };
3684
3473
  }
3685
- };
3686
- }
3687
- return {
3688
- status: 200,
3689
- json: ids
3690
- };
3691
- }
3692
-
3693
- //#region tree ops
3694
- async getSchema(cookies) {
3695
- const auth = this.getAuth(cookies);
3696
- if (auth.error) {
3697
- return {
3698
- status: 401,
3699
- json: {
3700
- message: auth.error
3474
+ const exp = getExpire();
3475
+ const valSecret = options.valSecret;
3476
+ if (!valSecret) {
3477
+ return {
3478
+ status: 302,
3479
+ cookies: {
3480
+ [VAL_STATE_COOKIE]: {
3481
+ value: null
3482
+ }
3483
+ },
3484
+ redirectTo: getAppErrorUrl("Setup is not correct: secret is missing")
3485
+ };
3701
3486
  }
3702
- };
3703
- }
3704
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3705
- return {
3706
- status: 401,
3707
- json: {
3708
- message: "Unauthorized"
3487
+ const cookie = encodeJwt({
3488
+ ...data,
3489
+ exp // this is the client side exp
3490
+ }, valSecret);
3491
+ return {
3492
+ status: 302,
3493
+ cookies: {
3494
+ [VAL_STATE_COOKIE]: {
3495
+ value: null
3496
+ },
3497
+ [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3498
+ [VAL_SESSION_COOKIE]: {
3499
+ value: cookie,
3500
+ options: {
3501
+ httpOnly: true,
3502
+ sameSite: "strict",
3503
+ path: "/",
3504
+ secure: true,
3505
+ expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
3506
+ }
3507
+ }
3508
+ },
3509
+ redirectTo: callbackReqSuccess.redirect_uri || "/"
3510
+ };
3511
+ }
3512
+ },
3513
+ "/session": {
3514
+ GET: async req => {
3515
+ const cookies = req.cookies;
3516
+ if (serverOps instanceof ValOpsFS) {
3517
+ return {
3518
+ status: 200,
3519
+ json: {
3520
+ mode: "local",
3521
+ enabled: await callbacks.isEnabled()
3522
+ }
3523
+ };
3709
3524
  }
3710
- };
3711
- }
3712
- const moduleErrors = await this.serverOps.getModuleErrors();
3713
- if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
3714
- console.error("Val: Module errors", moduleErrors);
3715
- return {
3716
- status: 500,
3717
- json: {
3718
- message: "Val is not correctly setup. Check the val.modules file",
3719
- details: moduleErrors
3525
+ if (!options.project) {
3526
+ return {
3527
+ status: 500,
3528
+ json: {
3529
+ message: "Project is not set"
3530
+ }
3531
+ };
3720
3532
  }
3721
- };
3722
- }
3723
- const schemaSha = await this.serverOps.getSchemaSha();
3724
- const schemas = await this.serverOps.getSchemas();
3725
- const serializedSchemas = {};
3726
- for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
3727
- const moduleFilePath = moduleFilePathS;
3728
- serializedSchemas[moduleFilePath] = schema.serialize();
3729
- }
3730
- return {
3731
- status: 200,
3732
- json: {
3733
- schemaSha,
3734
- schemas: serializedSchemas
3735
- }
3736
- };
3737
- }
3738
- async putTree(body, treePath, query, cookies) {
3739
- var _bodyRes$data, _bodyRes$data2, _bodyRes$data3;
3740
- const auth = this.getAuth(cookies);
3741
- if (auth.error) {
3742
- return {
3743
- status: 401,
3744
- json: {
3745
- message: auth.error
3533
+ if (!options.valSecret) {
3534
+ return {
3535
+ status: 500,
3536
+ json: {
3537
+ message: "Secret is not set"
3538
+ }
3539
+ };
3746
3540
  }
3747
- };
3748
- }
3749
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3750
- return {
3751
- status: 401,
3752
- json: {
3753
- message: "Unauthorized"
3541
+ return withAuth(options.valSecret, cookies, "session", async data => {
3542
+ if (!options.valBuildUrl) {
3543
+ return {
3544
+ status: 500,
3545
+ json: {
3546
+ message: "Val is not correctly setup. Build url is missing"
3547
+ }
3548
+ };
3549
+ }
3550
+ const url = new URL(`/api/val/${options.project}/auth/session`, options.valBuildUrl);
3551
+ const fetchRes = await fetch(url, {
3552
+ headers: getAuthHeaders(data.token, "application/json")
3553
+ });
3554
+ if (fetchRes.status === 200) {
3555
+ return {
3556
+ status: fetchRes.status,
3557
+ json: {
3558
+ mode: "proxy",
3559
+ enabled: await callbacks.isEnabled(),
3560
+ ...(await fetchRes.json())
3561
+ }
3562
+ };
3563
+ } else {
3564
+ return {
3565
+ status: fetchRes.status,
3566
+ json: {
3567
+ message: "Failed to authorize",
3568
+ ...(await fetchRes.json())
3569
+ }
3570
+ };
3571
+ }
3572
+ });
3573
+ }
3574
+ },
3575
+ "/logout": {
3576
+ GET: async () => {
3577
+ return {
3578
+ status: 200,
3579
+ cookies: {
3580
+ [VAL_SESSION_COOKIE]: {
3581
+ value: null
3582
+ },
3583
+ [VAL_STATE_COOKIE]: {
3584
+ value: null
3585
+ }
3586
+ }
3587
+ };
3588
+ }
3589
+ },
3590
+ //#region patches
3591
+ "/patches/~": {
3592
+ GET: async req => {
3593
+ const query = req.query;
3594
+ const cookies = req.cookies;
3595
+ const auth = getAuth(cookies);
3596
+ if (auth.error) {
3597
+ return {
3598
+ status: 401,
3599
+ json: {
3600
+ message: auth.error
3601
+ }
3602
+ };
3754
3603
  }
3755
- };
3756
- }
3757
- // TODO: move
3758
- const PutTreeBody = z.object({
3759
- patchIds: z.array(z.string().refine(id => true // TODO:
3760
- )).optional(),
3761
- addPatch: z.object({
3762
- path: z.string().refine(path => true // TODO:
3763
- ),
3764
- patch: Patch
3765
- }).optional()
3766
- }).optional();
3767
- const moduleErrors = await this.serverOps.getModuleErrors();
3768
- if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
3769
- console.error("Val: Module errors", moduleErrors);
3770
- return {
3771
- status: 500,
3772
- json: {
3773
- message: "Val is not correctly setup. Check the val.modules file",
3774
- details: moduleErrors
3604
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
3605
+ return {
3606
+ status: 401,
3607
+ json: {
3608
+ message: "Unauthorized"
3609
+ }
3610
+ };
3775
3611
  }
3776
- };
3777
- }
3778
- const bodyRes = PutTreeBody.safeParse(body);
3779
- if (!bodyRes.success) {
3780
- return {
3781
- status: 400,
3782
- json: {
3783
- message: "Invalid body: " + fromError(bodyRes.error).toString(),
3784
- details: bodyRes.error.errors
3612
+ const authors = query.author;
3613
+ const patches = await serverOps.fetchPatches({
3614
+ authors,
3615
+ patchIds: query.patch_id,
3616
+ omitPatch: query.omit_patch === true,
3617
+ moduleFilePaths: query.module_file_path
3618
+ });
3619
+ if (patches.error) {
3620
+ // Error is singular
3621
+ console.error("Val: Failed to get patches", patches.errors);
3622
+ return {
3623
+ status: 500,
3624
+ json: {
3625
+ message: patches.error.message,
3626
+ details: patches.error
3627
+ }
3628
+ };
3785
3629
  }
3786
- };
3787
- }
3788
- let tree;
3789
- let patchAnalysis = null;
3790
- if ((_bodyRes$data = bodyRes.data) !== null && _bodyRes$data !== void 0 && _bodyRes$data.patchIds && ((_bodyRes$data2 = bodyRes.data) === null || _bodyRes$data2 === void 0 || (_bodyRes$data2 = _bodyRes$data2.patchIds) === null || _bodyRes$data2 === void 0 ? void 0 : _bodyRes$data2.length) > 0 || (_bodyRes$data3 = bodyRes.data) !== null && _bodyRes$data3 !== void 0 && _bodyRes$data3.addPatch) {
3791
- var _bodyRes$data4, _bodyRes$data5;
3792
- // TODO: validate patches_sha
3793
- const patchIds = (_bodyRes$data4 = bodyRes.data) === null || _bodyRes$data4 === void 0 ? void 0 : _bodyRes$data4.patchIds;
3794
- const patchOps = patchIds && patchIds.length > 0 ? await this.serverOps.getPatchOpsById(patchIds) : {
3795
- patches: {}
3796
- };
3797
- let patchErrors = undefined;
3798
- for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
3799
- const patchId = patchIdS;
3800
- if (!patchErrors) {
3801
- patchErrors = {};
3630
+ if (patches.errors && Object.keys(patches.errors).length > 0) {
3631
+ // Errors is plural. Different property than above.
3632
+ console.error("Val: Failed to get patches", patches.errors);
3633
+ return {
3634
+ status: 500,
3635
+ json: {
3636
+ message: "Failed to get patches",
3637
+ details: patches.errors
3638
+ }
3639
+ };
3802
3640
  }
3803
- patchErrors[patchId] = {
3804
- message: error.message
3641
+ return {
3642
+ status: 200,
3643
+ json: patches
3805
3644
  };
3806
- }
3807
- if ((_bodyRes$data5 = bodyRes.data) !== null && _bodyRes$data5 !== void 0 && _bodyRes$data5.addPatch) {
3808
- const newPatchModuleFilePath = bodyRes.data.addPatch.path;
3809
- const newPatchOps = bodyRes.data.addPatch.patch;
3810
- const authorId = "id" in auth ? auth.id : null;
3811
- const createPatchRes = await this.serverOps.createPatch(newPatchModuleFilePath, newPatchOps, authorId);
3812
- if (createPatchRes.error) {
3645
+ },
3646
+ DELETE: async req => {
3647
+ const query = req.query;
3648
+ const cookies = req.cookies;
3649
+ const auth = getAuth(cookies);
3650
+ if (auth.error) {
3651
+ return {
3652
+ status: 401,
3653
+ json: {
3654
+ message: auth.error
3655
+ }
3656
+ };
3657
+ }
3658
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
3659
+ return {
3660
+ status: 401,
3661
+ json: {
3662
+ message: "Unauthorized"
3663
+ }
3664
+ };
3665
+ }
3666
+ const ids = query.id;
3667
+ const deleteRes = await serverOps.deletePatches(ids);
3668
+ if (deleteRes.errors && Object.keys(deleteRes.errors).length > 0) {
3669
+ console.error("Val: Failed to delete patches", deleteRes.errors);
3813
3670
  return {
3814
3671
  status: 500,
3815
3672
  json: {
3816
- message: "Failed to create patch: " + createPatchRes.error.message,
3817
- details: createPatchRes.error
3673
+ message: "Failed to delete patches",
3674
+ details: deleteRes.errors
3818
3675
  }
3819
3676
  };
3820
3677
  }
3821
- // TODO: evaluate if we need this: seems wrong to delete patches that are not applied
3822
- // for (const fileRes of createPatchRes.files) {
3823
- // if (fileRes.error) {
3824
- // // clean up broken patch:
3825
- // await this.serverOps.deletePatches([createPatchRes.patchId]);
3826
- // return {
3827
- // status: 500,
3828
- // json: {
3829
- // message: "Failed to create patch",
3830
- // details: fileRes.error,
3831
- // },
3832
- // };
3833
- // }
3834
- // }
3835
- patchOps.patches[createPatchRes.patchId] = {
3836
- path: newPatchModuleFilePath,
3837
- patch: newPatchOps,
3838
- authorId,
3839
- createdAt: createPatchRes.createdAt,
3840
- appliedAt: null
3678
+ return {
3679
+ status: 200,
3680
+ json: ids
3841
3681
  };
3842
3682
  }
3843
- // TODO: errors
3844
- patchAnalysis = this.serverOps.analyzePatches(patchOps.patches);
3845
- tree = {
3846
- ...(await this.serverOps.getTree({
3847
- ...patchAnalysis,
3848
- ...patchOps
3849
- }))
3850
- };
3851
- if (query.validate_all === "true") {
3852
- const allTree = await this.serverOps.getTree();
3853
- tree = {
3854
- sources: {
3855
- ...allTree.sources,
3856
- ...tree.sources
3857
- },
3858
- errors: {
3859
- ...allTree.errors,
3860
- ...tree.errors
3683
+ },
3684
+ //#region tree ops
3685
+ "/schema": {
3686
+ GET: async req => {
3687
+ const cookies = req.cookies;
3688
+ const auth = getAuth(cookies);
3689
+ if (auth.error) {
3690
+ return {
3691
+ status: 401,
3692
+ json: {
3693
+ message: auth.error
3694
+ }
3695
+ };
3696
+ }
3697
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
3698
+ return {
3699
+ status: 401,
3700
+ json: {
3701
+ message: "Unauthorized"
3702
+ }
3703
+ };
3704
+ }
3705
+ const moduleErrors = await serverOps.getModuleErrors();
3706
+ if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
3707
+ console.error("Val: Module errors", moduleErrors);
3708
+ return {
3709
+ status: 500,
3710
+ json: {
3711
+ message: "Val is not correctly setup. Check the val.modules file",
3712
+ details: moduleErrors
3713
+ }
3714
+ };
3715
+ }
3716
+ const schemaSha = await serverOps.getSchemaSha();
3717
+ const schemas = await serverOps.getSchemas();
3718
+ const serializedSchemas = {};
3719
+ for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
3720
+ const moduleFilePath = moduleFilePathS;
3721
+ serializedSchemas[moduleFilePath] = schema.serialize();
3722
+ }
3723
+ return {
3724
+ status: 200,
3725
+ json: {
3726
+ schemaSha,
3727
+ schemas: serializedSchemas
3861
3728
  }
3862
3729
  };
3863
3730
  }
3864
- } else {
3865
- tree = await this.serverOps.getTree();
3866
- }
3867
- if (tree.errors && Object.keys(tree.errors).length > 0) {
3868
- console.error("Val: Failed to get tree", JSON.stringify(tree.errors));
3869
- }
3870
- if (query.validate_sources === "true" || query.validate_binary_files === "true") {
3871
- const schemas = await this.serverOps.getSchemas();
3872
- const sourcesValidation = await this.serverOps.validateSources(schemas, tree.sources);
3731
+ },
3732
+ "/tree/~": {
3733
+ PUT: async req => {
3734
+ var _body$patchIds;
3735
+ const query = req.query;
3736
+ const cookies = req.cookies;
3737
+ const body = req.body;
3738
+ const treePath = req.path || "";
3739
+ const auth = getAuth(cookies);
3740
+ if (auth.error) {
3741
+ return {
3742
+ status: 401,
3743
+ json: {
3744
+ message: auth.error
3745
+ }
3746
+ };
3747
+ }
3748
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
3749
+ return {
3750
+ status: 401,
3751
+ json: {
3752
+ message: "Unauthorized"
3753
+ }
3754
+ };
3755
+ }
3756
+ const moduleErrors = await serverOps.getModuleErrors();
3757
+ if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
3758
+ console.error("Val: Module errors", moduleErrors);
3759
+ return {
3760
+ status: 500,
3761
+ json: {
3762
+ message: "Val is not correctly setup. Check the val.modules file",
3763
+ details: moduleErrors
3764
+ }
3765
+ };
3766
+ }
3767
+ let tree;
3768
+ 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) {
3771
+ // TODO: validate patches_sha
3772
+ const patchIds = body === null || body === void 0 ? void 0 : body.patchIds;
3773
+ const patchOps = patchIds && patchIds.length > 0 ? await serverOps.fetchPatches({
3774
+ patchIds,
3775
+ omitPatch: false
3776
+ }) : {
3777
+ patches: {}
3778
+ };
3779
+ let patchErrors = undefined;
3780
+ for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
3781
+ const patchId = patchIdS;
3782
+ if (!patchErrors) {
3783
+ patchErrors = {};
3784
+ }
3785
+ patchErrors[patchId] = {
3786
+ message: error.message
3787
+ };
3788
+ }
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
+ }
3801
+ };
3802
+ }
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
+ }
3826
+ // TODO: errors
3827
+ patchAnalysis = serverOps.analyzePatches(patchOps.patches);
3828
+ tree = {
3829
+ ...(await serverOps.getTree({
3830
+ ...patchAnalysis,
3831
+ ...patchOps
3832
+ }))
3833
+ };
3834
+ if (query.validate_all) {
3835
+ const allTree = await serverOps.getTree();
3836
+ tree = {
3837
+ sources: {
3838
+ ...allTree.sources,
3839
+ ...tree.sources
3840
+ },
3841
+ errors: {
3842
+ ...allTree.errors,
3843
+ ...tree.errors
3844
+ }
3845
+ };
3846
+ }
3847
+ } else {
3848
+ tree = await serverOps.getTree();
3849
+ }
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
+ }
3868
+ if (query.validate_sources || query.validate_binary_files) {
3869
+ const schemas = await serverOps.getSchemas();
3870
+ const sourcesValidation = await serverOps.validateSources(schemas, tree.sources);
3873
3871
 
3874
- // TODO: send validation errors
3875
- if (query.validate_binary_files === "true") {
3876
- await this.serverOps.validateFiles(schemas, tree.sources, sourcesValidation.files);
3877
- }
3878
- }
3879
- const schemaSha = await this.serverOps.getSchemaSha();
3880
- const modules = {};
3881
- for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
3882
- const moduleFilePath = moduleFilePathS;
3883
- if (moduleFilePath.startsWith(treePath)) {
3884
- modules[moduleFilePath] = {
3885
- source: module,
3886
- patches: patchAnalysis ? {
3887
- applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
3888
- } : undefined
3872
+ // TODO: send validation errors
3873
+ if (query.validate_binary_files) {
3874
+ await serverOps.validateFiles(schemas, tree.sources, sourcesValidation.files);
3875
+ }
3876
+ }
3877
+ const schemaSha = await serverOps.getSchemaSha();
3878
+ const modules = {};
3879
+ for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
3880
+ const moduleFilePath = moduleFilePathS;
3881
+ if (moduleFilePath.startsWith(treePath)) {
3882
+ modules[moduleFilePath] = {
3883
+ source: module,
3884
+ patches: patchAnalysis && patchAnalysis.patchesByModule[moduleFilePath] ? {
3885
+ applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
3886
+ } : undefined
3887
+ };
3888
+ }
3889
+ }
3890
+ const res = {
3891
+ status: 200,
3892
+ json: {
3893
+ schemaSha,
3894
+ modules,
3895
+ newPatchId
3896
+ }
3889
3897
  };
3898
+ return res;
3890
3899
  }
3891
- }
3892
- return {
3893
- status: 200,
3894
- json: {
3895
- schemaSha,
3896
- modules
3897
- }
3898
- };
3899
- }
3900
- async postSave(body, cookies) {
3901
- const auth = this.getAuth(cookies);
3902
- if (auth.error) {
3903
- return {
3904
- status: 401,
3905
- json: {
3906
- message: auth.error
3900
+ },
3901
+ "/save": {
3902
+ POST: async req => {
3903
+ const cookies = req.cookies;
3904
+ const body = req.body;
3905
+ const auth = getAuth(cookies);
3906
+ if (auth.error) {
3907
+ return {
3908
+ status: 401,
3909
+ json: {
3910
+ message: auth.error
3911
+ }
3912
+ };
3907
3913
  }
3908
- };
3909
- }
3910
- const PostSaveBody = z.object({
3911
- patchIds: z.array(z.string().refine(id => true // TODO:
3912
- ))
3913
- });
3914
- const bodyRes = PostSaveBody.safeParse(body);
3915
- if (!bodyRes.success) {
3916
- return {
3917
- status: 400,
3918
- json: {
3919
- message: "Invalid body: " + fromError(bodyRes.error).toString(),
3920
- details: bodyRes.error.errors
3914
+ const PostSaveBody = z.object({
3915
+ patchIds: z.array(z.string().refine(id => true // TODO:
3916
+ ))
3917
+ });
3918
+ const bodyRes = PostSaveBody.safeParse(body);
3919
+ if (!bodyRes.success) {
3920
+ return {
3921
+ status: 400,
3922
+ json: {
3923
+ message: "Invalid body: " + fromError(bodyRes.error).toString(),
3924
+ details: bodyRes.error.errors
3925
+ }
3926
+ };
3921
3927
  }
3922
- };
3923
- }
3924
- const {
3925
- patchIds
3926
- } = bodyRes.data;
3927
- const patches = await this.serverOps.getPatchOpsById(patchIds);
3928
- const analysis = this.serverOps.analyzePatches(patches.patches);
3929
- const preparedCommit = await this.serverOps.prepare({
3930
- ...analysis,
3931
- ...patches
3932
- });
3933
- if (preparedCommit.hasErrors) {
3934
- console.error("Failed to create commit", {
3935
- sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
3936
- binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
3937
- });
3938
- return {
3939
- status: 400,
3940
- json: {
3941
- message: "Failed to create commit",
3942
- details: {
3928
+ const {
3929
+ patchIds
3930
+ } = bodyRes.data;
3931
+ const patches = await serverOps.fetchPatches({
3932
+ patchIds,
3933
+ omitPatch: false
3934
+ });
3935
+ const analysis = serverOps.analyzePatches(patches.patches);
3936
+ const preparedCommit = await serverOps.prepare({
3937
+ ...analysis,
3938
+ ...patches
3939
+ });
3940
+ if (preparedCommit.hasErrors) {
3941
+ console.error("Failed to create commit", {
3943
3942
  sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
3944
3943
  binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
3944
+ });
3945
+ return {
3946
+ status: 400,
3947
+ json: {
3948
+ message: "Failed to create commit",
3949
+ details: {
3950
+ sourceFilePatchErrors: Object.fromEntries(Object.entries(preparedCommit.sourceFilePatchErrors).map(([key, errors]) => [key, errors.map(e => ({
3951
+ message: formatPatchSourceError(e)
3952
+ }))])),
3953
+ binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
3954
+ }
3955
+ }
3956
+ };
3957
+ }
3958
+ if (serverOps instanceof ValOpsFS) {
3959
+ await serverOps.saveFiles(preparedCommit);
3960
+ return {
3961
+ status: 200,
3962
+ json: {} // TODO:
3963
+ };
3964
+ } else if (serverOps instanceof ValOpsHttp) {
3965
+ if (auth.error === undefined && auth.id) {
3966
+ await serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
3967
+ return {
3968
+ status: 200,
3969
+ json: {} // TODO:
3970
+ };
3945
3971
  }
3972
+ return {
3973
+ status: 401,
3974
+ json: {
3975
+ message: "Unauthorized"
3976
+ }
3977
+ };
3978
+ } else {
3979
+ throw new Error("Invalid server ops");
3946
3980
  }
3947
- };
3948
- }
3949
- if (this.serverOps instanceof ValOpsFS) {
3950
- await this.serverOps.saveFiles(preparedCommit);
3951
- return {
3952
- status: 200,
3953
- json: {} // TODO:
3954
- };
3955
- } else if (this.serverOps instanceof ValOpsHttp) {
3956
- if (auth.error === undefined && auth.id) {
3957
- await this.serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
3958
- return {
3959
- status: 200,
3960
- json: {} // TODO:
3961
- };
3962
3981
  }
3963
- return {
3964
- status: 401,
3965
- json: {
3966
- message: "Unauthorized"
3982
+ },
3983
+ //#region files
3984
+ "/files": {
3985
+ GET: async req => {
3986
+ const query = req.query;
3987
+ const filePath = req.path;
3988
+ // NOTE: no auth here since you would need the patch_id to get something that is not published.
3989
+ // For everything that is published, well they are already public so no auth required there...
3990
+ // We could imagine adding auth just to be a 200% certain,
3991
+ // However that won't work since images are requested by the nextjs backend as a part of image optimization (again: as an example) which is a backend-to-backend op (no cookies, ...).
3992
+ // So: 1) patch ids are not possible to guess (but possible to brute force)
3993
+ // 2) the process of shimming a patch into the frontend would be quite challenging (so just trying out this attack would require a lot of effort)
3994
+ // 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
3995
+ // Thus: attack surface + ease of attack + benefit = low probability of attack
3996
+ // If we couldn't argue that patch ids are secret enough, then this would be a problem.
3997
+ let fileBuffer;
3998
+ if (query.patch_id) {
3999
+ fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
4000
+ } else {
4001
+ fileBuffer = await serverOps.getBinaryFile(filePath);
3967
4002
  }
3968
- };
3969
- } else {
3970
- throw new Error("Invalid server ops");
3971
- }
3972
- }
3973
-
3974
- //#region files
3975
- async getFiles(filePath, query) {
3976
- // NOTE: no auth here since you would need the patch_id to get something that is not published.
3977
- // For everything that is published, well they are already public so no auth required there...
3978
- // We could imagine adding auth just to be a 200% certain,
3979
- // However that won't work since images are requested by the nextjs backend as a part of image optimization (again: as an example) which is a backend-to-backend op (no cookies, ...).
3980
- // So: 1) patch ids are not possible to guess (but possible to brute force)
3981
- // 2) the process of shimming a patch into the frontend would be quite challenging (so just trying out this attack would require a lot of effort)
3982
- // 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
3983
- // Thus: attack surface + ease of attack + benefit = low probability of attack
3984
- // If we couldn't argue that patch ids are secret enough, then this would be a problem.
3985
- let fileBuffer;
3986
- if (query.patch_id) {
3987
- fileBuffer = await this.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
3988
- } else {
3989
- fileBuffer = await this.serverOps.getBinaryFile(filePath);
3990
- }
3991
- if (fileBuffer) {
3992
- return {
3993
- status: 200,
3994
- body: bufferToReadableStream(fileBuffer)
3995
- };
3996
- } else {
3997
- return {
3998
- status: 404,
3999
- json: {
4000
- message: "File not found"
4003
+ if (fileBuffer) {
4004
+ return {
4005
+ status: 200,
4006
+ body: bufferToReadableStream(fileBuffer)
4007
+ };
4008
+ } else {
4009
+ return {
4010
+ status: 404,
4011
+ json: {
4012
+ message: "File not found"
4013
+ }
4014
+ };
4001
4015
  }
4002
- };
4016
+ }
4003
4017
  }
4018
+ };
4019
+ };
4020
+ function formatPatchSourceError(error) {
4021
+ if ("message" in error) {
4022
+ return error.message;
4023
+ } else if (Array.isArray(error)) {
4024
+ return error.map(formatPatchSourceError).join("\n");
4025
+ } else {
4026
+ const _exhaustiveCheck = error;
4027
+ return "Unknown patch source error: " + JSON.stringify(_exhaustiveCheck);
4004
4028
  }
4005
4029
  }
4006
4030
  function verifyCallbackReq(stateCookie, queryParams) {
@@ -4120,7 +4144,7 @@ const IntegratedServerJwtPayload = z.object({
4120
4144
  project: z.string()
4121
4145
  });
4122
4146
  async function withAuth(secret, cookies, errorMessageType, handler) {
4123
- const cookie = cookies[VAL_SESSION_COOKIE$1];
4147
+ const cookie = cookies[VAL_SESSION_COOKIE];
4124
4148
  if (typeof cookie === "string") {
4125
4149
  const decodedToken = decodeJwt(cookie, secret);
4126
4150
  if (!decodedToken) {
@@ -4138,7 +4162,7 @@ async function withAuth(secret, cookies, errorMessageType, handler) {
4138
4162
  status: 401,
4139
4163
  json: {
4140
4164
  message: "Session invalid or, most likely, expired. You will need to login again.",
4141
- details: verification.error
4165
+ details: fromError(verification.error).toString()
4142
4166
  }
4143
4167
  };
4144
4168
  }
@@ -4300,7 +4324,7 @@ function guessMimeTypeFromPath(filePath) {
4300
4324
 
4301
4325
  async function createValServer(valModules, route, opts, callbacks, formatter) {
4302
4326
  const valServerConfig = await initHandlerOptions(route, opts);
4303
- return new ValServer(valModules, {
4327
+ return ValServer(valModules, {
4304
4328
  formatter,
4305
4329
  ...valServerConfig
4306
4330
  }, callbacks);
@@ -4425,17 +4449,9 @@ async function readCommit(gitDir, branchName) {
4425
4449
  return undefined;
4426
4450
  }
4427
4451
  }
4428
- const {
4429
- VAL_SESSION_COOKIE,
4430
- VAL_STATE_COOKIE
4431
- } = Internal;
4432
- const TREE_PATH_PREFIX = "/tree/~";
4433
- const PATCHES_PATH_PREFIX = "/patches/~";
4434
- const FILES_PATH_PREFIX = "/files";
4435
4452
  function createValApiRouter(route, valServerPromise, convert) {
4436
4453
  const uiRequestHandler = createUIRequestHandler();
4437
4454
  return async req => {
4438
- var _req$method;
4439
4455
  const valServer = await valServerPromise;
4440
4456
  const url = new URL(req.url);
4441
4457
  if (!url.pathname.startsWith(route)) {
@@ -4449,113 +4465,213 @@ function createValApiRouter(route, valServerPromise, convert) {
4449
4465
  json: error
4450
4466
  });
4451
4467
  }
4452
- const method = (_req$method = req.method) === null || _req$method === void 0 ? void 0 : _req$method.toUpperCase();
4453
- function withTreePath(path, prefix) {
4454
- return async useTreePath => {
4455
- const pathIndex = path.indexOf("~");
4456
- if (path.startsWith(prefix) && pathIndex !== -1) {
4457
- return useTreePath(path.slice(pathIndex + 1));
4458
- } else {
4459
- if (prefix.indexOf("/~") === -1) {
4460
- return convert({
4461
- status: 500,
4462
- json: {
4463
- message: `Route is incorrectly formed: ${prefix}!`
4468
+ const path = url.pathname.slice(route.length);
4469
+ const groupQueryParams = arr => {
4470
+ const map = {};
4471
+ for (const [key, value] of arr) {
4472
+ const list = map[key] || [];
4473
+ list.push(value);
4474
+ map[key] = list;
4475
+ }
4476
+ return map;
4477
+ };
4478
+ async function getValServerResponse(reqApiRoutePath, req) {
4479
+ var _req$method, _anyApi$route, _anyValServer$route;
4480
+ const anyApi =
4481
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4482
+ Api;
4483
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4484
+ const anyValServer = valServer;
4485
+ const method = (_req$method = req.method) === null || _req$method === void 0 ? void 0 : _req$method.toUpperCase();
4486
+ let route = null;
4487
+ let path = undefined;
4488
+ for (const routeDef of Object.keys(Api)) {
4489
+ if (routeDef === reqApiRoutePath) {
4490
+ route = routeDef;
4491
+ break;
4492
+ }
4493
+ if (reqApiRoutePath.startsWith(routeDef)) {
4494
+ var _anyApi$routeDef;
4495
+ const reqDefinition = anyApi === null || anyApi === void 0 || (_anyApi$routeDef = anyApi[routeDef]) === null || _anyApi$routeDef === void 0 || (_anyApi$routeDef = _anyApi$routeDef[method]) === null || _anyApi$routeDef === void 0 ? void 0 : _anyApi$routeDef.req;
4496
+ if (reqDefinition) {
4497
+ route = routeDef;
4498
+ if (reqDefinition.path) {
4499
+ const subPath = reqApiRoutePath.slice(routeDef.length);
4500
+ const pathRes = reqDefinition.path.safeParse(subPath);
4501
+ if (!pathRes.success) {
4502
+ return zodErrorResult(pathRes.error, `invalid path: '${subPath}' endpoint: '${routeDef}'`);
4503
+ } else {
4504
+ path = pathRes.data;
4464
4505
  }
4465
- });
4506
+ }
4507
+ break;
4466
4508
  }
4467
- return convert({
4468
- status: 404,
4509
+ }
4510
+ }
4511
+ if (!route) {
4512
+ return {
4513
+ status: 404,
4514
+ json: {
4515
+ message: "Route not found. Valid routes are: " + Object.keys(Api),
4516
+ details: {
4517
+ route,
4518
+ method
4519
+ }
4520
+ }
4521
+ };
4522
+ }
4523
+ const apiEndpoint = anyApi === null || anyApi === void 0 || (_anyApi$route = anyApi[route]) === null || _anyApi$route === void 0 ? void 0 : _anyApi$route[method];
4524
+ const reqDefinition = apiEndpoint === null || apiEndpoint === void 0 ? void 0 : apiEndpoint.req;
4525
+ if (!reqDefinition) {
4526
+ return {
4527
+ status: 404,
4528
+ json: {
4529
+ message: `Requested method ${method} on route ${route} is not valid. Valid methods are: ${Object.keys(anyApi[route]).join(", ")}`,
4530
+ details: {
4531
+ route,
4532
+ method
4533
+ }
4534
+ }
4535
+ };
4536
+ }
4537
+ const endpointImpl = anyValServer === null || anyValServer === void 0 || (_anyValServer$route = anyValServer[route]) === null || _anyValServer$route === void 0 ? void 0 : _anyValServer$route[method];
4538
+ if (!endpointImpl) {
4539
+ return {
4540
+ status: 500,
4541
+ json: {
4542
+ message: "Missing server implementation of route with method. This might be caused by a mismatch between Val package versions.",
4543
+ details: {
4544
+ valid: {
4545
+ route: {
4546
+ server: Object.keys(anyValServer || {}),
4547
+ api: Object.keys(anyApi || {})
4548
+ },
4549
+ method: {
4550
+ server: Object.keys((anyValServer === null || anyValServer === void 0 ? void 0 : anyValServer[route]) || {}),
4551
+ api: Object.keys((anyApi === null || anyApi === void 0 ? void 0 : anyApi[route]) || {})
4552
+ }
4553
+ },
4554
+ route,
4555
+ method
4556
+ }
4557
+ }
4558
+ };
4559
+ }
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");
4566
+ }
4567
+ const cookiesRes = reqDefinition.cookies ? getCookies(req, reqDefinition.cookies) : {
4568
+ success: true,
4569
+ data: {}
4570
+ };
4571
+ if (!cookiesRes.success) {
4572
+ return zodErrorResult(cookiesRes.error, "invalid cookies");
4573
+ }
4574
+ const actualQueryParams = groupQueryParams(Array.from(url.searchParams.entries()));
4575
+ let query = {};
4576
+ if (reqDefinition.query) {
4577
+ // This is code is particularly heavy, however
4578
+ // @see ValidQueryParamTypes in ApiRouter.ts where we explain what we want to support
4579
+ // We prioritized a declarative ApiRouter, so this code is what we ended up with for better of worse
4580
+ const queryRules = {};
4581
+ for (const [key, zodRule] of Object.entries(reqDefinition.query)) {
4582
+ let innerType = zodRule;
4583
+ let isOptional = false;
4584
+ let isArray = false;
4585
+ // extract inner types:
4586
+ if (innerType instanceof z.ZodOptional) {
4587
+ isOptional = true;
4588
+ innerType = innerType.unwrap();
4589
+ }
4590
+ if (innerType instanceof z.ZodArray) {
4591
+ isArray = true;
4592
+ innerType = innerType.element;
4593
+ }
4594
+ // convert boolean to union of literals true and false so we can parse it as a string
4595
+ if (innerType instanceof z.ZodBoolean) {
4596
+ innerType = z.union([z.literal("true"), z.literal("false")]).transform(arg => Boolean(arg));
4597
+ }
4598
+ // re-build rules:
4599
+ let arrayCompatibleRule = innerType;
4600
+ arrayCompatibleRule = z.array(innerType); // we always want to parse an array because we group the query params by into an array
4601
+ if (isOptional) {
4602
+ arrayCompatibleRule = arrayCompatibleRule.optional();
4603
+ }
4604
+ if (!isArray) {
4605
+ arrayCompatibleRule = arrayCompatibleRule.transform(arg => arg && arg[0]);
4606
+ }
4607
+ queryRules[key] = arrayCompatibleRule;
4608
+ }
4609
+ const queryRes = z.object(queryRules).safeParse(actualQueryParams);
4610
+ if (!queryRes.success) {
4611
+ return zodErrorResult(queryRes.error, `invalid query params: (${JSON.stringify(actualQueryParams)})`);
4612
+ }
4613
+ query = queryRes.data;
4614
+ }
4615
+ const res = await endpointImpl({
4616
+ body: bodyRes.data,
4617
+ cookies: cookiesRes.data,
4618
+ query,
4619
+ path
4620
+ });
4621
+ const resDef = apiEndpoint.res;
4622
+ if (resDef) {
4623
+ const responseResult = resDef.safeParse(res);
4624
+ if (!responseResult.success) {
4625
+ return {
4626
+ status: 500,
4469
4627
  json: {
4470
- message: `Malformed ${prefix} path! Expected: '${prefix}'`
4628
+ message: "Could not validate response. This is likely a bug in Val server.",
4629
+ details: {
4630
+ response: res,
4631
+ errors: formatZodErrorString(responseResult.error)
4632
+ }
4471
4633
  }
4472
- });
4634
+ };
4473
4635
  }
4474
- };
4636
+ }
4637
+ return res;
4475
4638
  }
4476
- const path = url.pathname.slice(route.length);
4477
4639
  if (path.startsWith("/static")) {
4478
4640
  return convert(await uiRequestHandler(path.slice("/static".length), url.href));
4479
- } else if (path === "/session") {
4480
- return convert(await valServer.session(getCookies(req, [VAL_SESSION_COOKIE])));
4481
- } else if (path === "/authorize") {
4482
- return convert(await valServer.authorize({
4483
- redirect_to: url.searchParams.get("redirect_to") || undefined
4484
- }));
4485
- } else if (path === "/callback") {
4486
- return convert(await valServer.callback({
4487
- code: url.searchParams.get("code") || undefined,
4488
- state: url.searchParams.get("state") || undefined
4489
- }, getCookies(req, [VAL_STATE_COOKIE])));
4490
- } else if (path === "/logout") {
4491
- return convert(await valServer.logout());
4492
- } else if (path === "/enable") {
4493
- return convert(await valServer.enable({
4494
- redirect_to: url.searchParams.get("redirect_to") || undefined
4495
- }));
4496
- } else if (path === "/disable") {
4497
- return convert(await valServer.disable({
4498
- redirect_to: url.searchParams.get("redirect_to") || undefined
4499
- }));
4500
- } else if (method === "POST" && path === "/save") {
4501
- const body = await req.json();
4502
- return convert(await valServer.postSave(body, getCookies(req, [VAL_SESSION_COOKIE])));
4503
- // } else if (method === "POST" && path === "/validate") {
4504
- // const body = (await req.json()) as unknown;
4505
- // return convert(
4506
- // await valServer.postValidate(
4507
- // body,
4508
- // getCookies(req, [VAL_SESSION_COOKIE]),
4509
- // requestHeaders
4510
- // )
4511
- // );
4512
- } else if (method === "GET" && path === "/schema") {
4513
- return convert(await valServer.getSchema(getCookies(req, [VAL_SESSION_COOKIE])));
4514
- } else if (method === "PUT" && path.startsWith(TREE_PATH_PREFIX)) {
4515
- return withTreePath(path, TREE_PATH_PREFIX)(async treePath => convert(await valServer.putTree(await req.json(), treePath, {
4516
- patches_sha: url.searchParams.get("patches_sha") || undefined,
4517
- validate_all: url.searchParams.get("validate_all") || undefined,
4518
- validate_binary_files: url.searchParams.get("validate_binary_files") || undefined,
4519
- validate_sources: url.searchParams.get("validate_sources") || undefined
4520
- }, getCookies(req, [VAL_SESSION_COOKIE]))));
4521
- } else if (method === "GET" && path.startsWith(PATCHES_PATH_PREFIX)) {
4522
- return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.getPatches({
4523
- authors: url.searchParams.getAll("author")
4524
- }, getCookies(req, [VAL_SESSION_COOKIE]))));
4525
- } else if (method === "DELETE" && path.startsWith(PATCHES_PATH_PREFIX)) {
4526
- return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.deletePatches({
4527
- id: url.searchParams.getAll("id")
4528
- }, getCookies(req, [VAL_SESSION_COOKIE]))));
4529
- } else if (method === "GET" && path.startsWith(FILES_PATH_PREFIX)) {
4530
- const treePath = path.slice(FILES_PATH_PREFIX.length);
4531
- return convert(await valServer.getFiles(treePath, {
4532
- patch_id: url.searchParams.get("patch_id") || undefined
4533
- }));
4534
4641
  } else {
4535
- return convert({
4536
- status: 404,
4537
- json: {
4538
- message: "Not Found",
4539
- details: {
4540
- method,
4541
- path
4542
- }
4543
- }
4544
- });
4642
+ return convert(await getValServerResponse(path, req));
4643
+ }
4644
+ };
4645
+ }
4646
+ function formatZodErrorString(error) {
4647
+ const errors = fromZodError(error).toString();
4648
+ return errors.length > 640 ? `${errors.slice(0, 640)}...` : errors;
4649
+ }
4650
+ function zodErrorResult(error, message) {
4651
+ return {
4652
+ status: 400,
4653
+ json: {
4654
+ message: "Bad Request: " + message,
4655
+ details: {
4656
+ errors: formatZodErrorString(error)
4657
+ }
4545
4658
  }
4546
4659
  };
4547
4660
  }
4548
4661
 
4549
4662
  // TODO: is this naive implementation is too naive?
4550
- function getCookies(req, names) {
4551
- var _req$headers$get;
4552
- return ((_req$headers$get = req.headers.get("Cookie")) === null || _req$headers$get === void 0 ? void 0 : _req$headers$get.split("; ").reduce((acc, cookie) => {
4553
- const [name, value] = cookie.split("=");
4554
- if (names.includes(name.trim())) {
4555
- acc[name.trim()] = decodeURIComponent(value.trim());
4556
- }
4557
- return acc;
4558
- }, {})) || {};
4663
+ function getCookies(req, cookiesDef) {
4664
+ var _req$headers;
4665
+ const input = {};
4666
+ const cookieParts = (_req$headers = req.headers) === null || _req$headers === void 0 || (_req$headers = _req$headers.get("Cookie")) === null || _req$headers === void 0 ? void 0 : _req$headers.split("; ");
4667
+ for (const name of Object.keys(cookiesDef)) {
4668
+ const cookie = cookieParts === null || cookieParts === void 0 ? void 0 : cookieParts.find(cookie => cookie.startsWith(`${name}=`));
4669
+ const value = cookie ? decodeURIComponent(cookie === null || cookie === void 0 ? void 0 : cookie.split("=")[1]) : undefined;
4670
+ if (value) {
4671
+ input[name.trim()] = value;
4672
+ }
4673
+ }
4674
+ return z.object(cookiesDef).safeParse(input);
4559
4675
  }
4560
4676
 
4561
4677
  /**