@valbuild/server 0.63.5 → 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,6 +1506,7 @@ 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 {
@@ -1509,6 +1514,7 @@ class ValOps {
1509
1514
  if (!patchData) {
1510
1515
  errors[path] = [{
1511
1516
  patchId: patchId,
1517
+ skipped: false,
1512
1518
  error: new PatchError(`Patch not found`)
1513
1519
  }];
1514
1520
  continue;
@@ -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 {
@@ -2738,7 +2745,7 @@ const BasePatchResponse = z.object({
2738
2745
  });
2739
2746
  const GetPatches = z.object({
2740
2747
  patches: z.array(z.intersection(z.object({
2741
- patch: Patch
2748
+ patch: Patch.optional()
2742
2749
  }), BasePatchResponse)),
2743
2750
  errors: z.array(z.object({
2744
2751
  patchId: PatchId.optional(),
@@ -3211,251 +3218,59 @@ class ValOpsHttp extends ValOps {
3211
3218
  }
3212
3219
 
3213
3220
  /* eslint-disable @typescript-eslint/no-unused-vars */
3214
- class ValServer {
3215
- constructor(valModules, options, callbacks) {
3216
- this.valModules = valModules;
3217
- this.options = options;
3218
- this.callbacks = callbacks;
3219
- if (options.mode === "fs") {
3220
- this.serverOps = new ValOpsFS(options.cwd, valModules, {
3221
- formatter: options.formatter
3222
- });
3223
- } else if (options.mode === "http") {
3224
- this.serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
3225
- formatter: options.formatter,
3226
- root: options.root
3227
- });
3228
- } else {
3229
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3230
- throw new Error("Invalid mode: " + (options === null || options === void 0 ? void 0 : options.mode));
3231
- }
3232
- }
3233
-
3234
- //#region auth
3235
- async enable(query) {
3236
- const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
3237
- if (typeof redirectToRes !== "string") {
3238
- return redirectToRes;
3239
- }
3240
- await this.callbacks.onEnable(true);
3241
- return {
3242
- cookies: {
3243
- [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
3244
- },
3245
- status: 302,
3246
- redirectTo: redirectToRes
3247
- };
3248
- }
3249
- async disable(query) {
3250
- const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
3251
- if (typeof redirectToRes !== "string") {
3252
- return redirectToRes;
3253
- }
3254
- await this.callbacks.onDisable(true);
3255
- return {
3256
- cookies: {
3257
- [VAL_ENABLE_COOKIE_NAME]: {
3258
- value: "false"
3259
- }
3260
- },
3261
- status: 302,
3262
- redirectTo: redirectToRes
3263
- };
3264
- }
3265
- async authorize(query) {
3266
- if (typeof query.redirect_to !== "string") {
3267
- return {
3268
- status: 400,
3269
- json: {
3270
- message: "Missing redirect_to query param"
3271
- }
3272
- };
3273
- }
3274
- const token = crypto.randomUUID();
3275
- const redirectUrl = new URL(query.redirect_to);
3276
- const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
3277
- await this.callbacks.onEnable(true);
3278
- return {
3279
- cookies: {
3280
- [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3281
- [VAL_STATE_COOKIE$1]: {
3282
- value: createStateCookie({
3283
- redirect_to: query.redirect_to,
3284
- token
3285
- }),
3286
- options: {
3287
- httpOnly: true,
3288
- sameSite: "lax",
3289
- expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
3290
- }
3291
- }
3292
- },
3293
- status: 302,
3294
- redirectTo: appAuthorizeUrl
3295
- };
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));
3296
3235
  }
3297
- async callback(query, cookies) {
3298
- if (!this.options.project) {
3299
- return {
3300
- status: 302,
3301
- cookies: {
3302
- [VAL_STATE_COOKIE$1]: {
3303
- value: null
3304
- }
3305
- },
3306
- redirectTo: this.getAppErrorUrl("Project is not set")
3307
- };
3308
- }
3309
- if (!this.options.valSecret) {
3310
- return {
3311
- status: 302,
3312
- cookies: {
3313
- [VAL_STATE_COOKIE$1]: {
3314
- value: null
3315
- }
3316
- },
3317
- redirectTo: this.getAppErrorUrl("Secret is not set")
3318
- };
3319
- }
3320
- const {
3321
- success: callbackReqSuccess,
3322
- error: callbackReqError
3323
- } = verifyCallbackReq(cookies[VAL_STATE_COOKIE$1], query);
3324
- if (callbackReqError !== null) {
3325
- return {
3326
- status: 302,
3327
- cookies: {
3328
- [VAL_STATE_COOKIE$1]: {
3329
- value: null
3330
- }
3331
- },
3332
- redirectTo: this.getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`)
3333
- };
3334
- }
3335
- const data = await this.consumeCode(callbackReqSuccess.code);
3336
- if (data === null) {
3337
- return {
3338
- status: 302,
3339
- cookies: {
3340
- [VAL_STATE_COOKIE$1]: {
3341
- value: null
3342
- }
3343
- },
3344
- redirectTo: this.getAppErrorUrl("Failed to exchange code for user")
3345
- };
3346
- }
3347
- const exp = getExpire();
3348
- const valSecret = this.options.valSecret;
3349
- if (!valSecret) {
3350
- return {
3351
- status: 302,
3352
- cookies: {
3353
- [VAL_STATE_COOKIE$1]: {
3354
- value: null
3355
- }
3356
- },
3357
- redirectTo: this.getAppErrorUrl("Setup is not correct: secret is missing")
3358
- };
3236
+ const getAuthorizeUrl = (publicValApiRe, token) => {
3237
+ if (!options.project) {
3238
+ throw new Error("Project is not set");
3359
3239
  }
3360
- const cookie = encodeJwt({
3361
- ...data,
3362
- exp // this is the client side exp
3363
- }, valSecret);
3364
- return {
3365
- status: 302,
3366
- cookies: {
3367
- [VAL_STATE_COOKIE$1]: {
3368
- value: null
3369
- },
3370
- [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3371
- [VAL_SESSION_COOKIE$1]: {
3372
- value: cookie,
3373
- options: {
3374
- httpOnly: true,
3375
- sameSite: "strict",
3376
- path: "/",
3377
- secure: true,
3378
- expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
3379
- }
3380
- }
3381
- },
3382
- redirectTo: callbackReqSuccess.redirect_uri || "/"
3383
- };
3384
- }
3385
- async session(cookies) {
3386
- if (this.serverOps instanceof ValOpsFS) {
3387
- return {
3388
- status: 200,
3389
- json: {
3390
- mode: "local",
3391
- enabled: await this.callbacks.isEnabled()
3392
- }
3393
- };
3240
+ if (!options.valBuildUrl) {
3241
+ throw new Error("Val build url is not set");
3394
3242
  }
3395
- if (!this.options.project) {
3396
- return {
3397
- status: 500,
3398
- json: {
3399
- message: "Project is not set"
3400
- }
3401
- };
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");
3402
3251
  }
3403
- if (!this.options.valSecret) {
3404
- return {
3405
- status: 500,
3406
- json: {
3407
- message: "Secret is not set"
3408
- }
3409
- };
3252
+ if (!options.valBuildUrl) {
3253
+ throw new Error("Val build url is not set");
3410
3254
  }
3411
- return withAuth(this.options.valSecret, cookies, "session", async data => {
3412
- if (!this.options.valBuildUrl) {
3413
- return {
3414
- status: 500,
3415
- json: {
3416
- message: "Val is not correctly setup. Build url is missing"
3417
- }
3418
- };
3419
- }
3420
- const url = new URL(`/api/val/${this.options.project}/auth/session`, this.options.valBuildUrl);
3421
- const fetchRes = await fetch(url, {
3422
- headers: getAuthHeaders(data.token, "application/json")
3423
- });
3424
- if (fetchRes.status === 200) {
3425
- return {
3426
- status: fetchRes.status,
3427
- json: {
3428
- mode: "proxy",
3429
- enabled: await this.callbacks.isEnabled(),
3430
- ...(await fetchRes.json())
3431
- }
3432
- };
3433
- } else {
3434
- return {
3435
- status: fetchRes.status,
3436
- json: {
3437
- message: "Failed to authorize",
3438
- ...(await fetchRes.json())
3439
- }
3440
- };
3441
- }
3442
- });
3443
- }
3444
- async consumeCode(code) {
3445
- 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) {
3446
3261
  throw new Error("Project is not set");
3447
3262
  }
3448
- if (!this.options.valBuildUrl) {
3263
+ if (!options.valBuildUrl) {
3449
3264
  throw new Error("Val build url is not set");
3450
3265
  }
3451
- 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);
3452
3267
  url.searchParams.set("code", encodeURIComponent(code));
3453
- if (!this.options.apiKey) {
3268
+ if (!options.apiKey) {
3454
3269
  return null;
3455
3270
  }
3456
3271
  return fetch(url, {
3457
3272
  method: "POST",
3458
- 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)
3459
3274
  }).then(async res => {
3460
3275
  if (res.status === 200) {
3461
3276
  const token = await res.text();
@@ -3475,34 +3290,11 @@ class ValServer {
3475
3290
  console.debug("Failed to get user from code: ", err);
3476
3291
  return null;
3477
3292
  });
3478
- }
3479
- getAuthorizeUrl(publicValApiRe, token) {
3480
- if (!this.options.project) {
3481
- throw new Error("Project is not set");
3482
- }
3483
- if (!this.options.valBuildUrl) {
3484
- throw new Error("Val build url is not set");
3485
- }
3486
- const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
3487
- url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRe}/callback`));
3488
- url.searchParams.set("state", token);
3489
- return url.toString();
3490
- }
3491
- getAppErrorUrl(error) {
3492
- if (!this.options.project) {
3493
- throw new Error("Project is not set");
3494
- }
3495
- if (!this.options.valBuildUrl) {
3496
- throw new Error("Val build url is not set");
3497
- }
3498
- const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
3499
- url.searchParams.set("error", encodeURIComponent(error));
3500
- return url.toString();
3501
- }
3502
- getAuth(cookies) {
3503
- const cookie = cookies[VAL_SESSION_COOKIE$1];
3504
- if (!this.options.valSecret) {
3505
- 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) {
3506
3298
  return {
3507
3299
  error: null,
3508
3300
  id: null
@@ -3514,9 +3306,9 @@ class ValServer {
3514
3306
  }
3515
3307
  }
3516
3308
  if (typeof cookie === "string") {
3517
- const decodedToken = decodeJwt(cookie, this.options.valSecret);
3309
+ const decodedToken = decodeJwt(cookie, options.valSecret);
3518
3310
  if (!decodedToken) {
3519
- if (this.serverOps instanceof ValOpsFS) {
3311
+ if (serverOps instanceof ValOpsFS) {
3520
3312
  return {
3521
3313
  error: null,
3522
3314
  id: null
@@ -3528,7 +3320,7 @@ class ValServer {
3528
3320
  }
3529
3321
  const verification = IntegratedServerJwtPayload.safeParse(decodedToken);
3530
3322
  if (!verification.success) {
3531
- if (this.serverOps instanceof ValOpsFS) {
3323
+ if (serverOps instanceof ValOpsFS) {
3532
3324
  return {
3533
3325
  error: null,
3534
3326
  id: null
@@ -3542,7 +3334,7 @@ class ValServer {
3542
3334
  id: verification.data.sub
3543
3335
  };
3544
3336
  } else {
3545
- if (this.serverOps instanceof ValOpsFS) {
3337
+ if (serverOps instanceof ValOpsFS) {
3546
3338
  return {
3547
3339
  error: null,
3548
3340
  id: null
@@ -3552,417 +3344,687 @@ class ValServer {
3552
3344
  error: "Login required: cookie not found"
3553
3345
  };
3554
3346
  }
3555
- }
3556
- async logout() {
3557
- return {
3558
- status: 200,
3559
- cookies: {
3560
- [VAL_SESSION_COOKIE$1]: {
3561
- value: null
3562
- },
3563
- [VAL_STATE_COOKIE$1]: {
3564
- value: null
3565
- }
3566
- }
3567
- };
3568
- }
3569
-
3570
- //#region patches
3571
- async getPatches(query, cookies) {
3572
- const auth = this.getAuth(cookies);
3573
- if (auth.error) {
3574
- return {
3575
- status: 401,
3576
- json: {
3577
- message: auth.error
3578
- }
3579
- };
3580
- }
3581
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3582
- return {
3583
- status: 401,
3584
- json: {
3585
- message: "Unauthorized"
3586
- }
3587
- };
3588
- }
3589
- const authors = query.authors;
3590
- const patches = await this.serverOps.fetchPatches({
3591
- authors,
3592
- patchIds: query.patchIds,
3593
- omitPatch: query.omitPatch === "true"
3594
- });
3595
- if (patches.errors && Object.keys(patches.errors).length > 0) {
3596
- console.error("Val: Failed to get patches", patches.errors);
3597
- return {
3598
- status: 500,
3599
- json: {
3600
- message: "Failed to get patches",
3601
- details: patches.errors
3602
- }
3603
- };
3604
- }
3605
- return {
3606
- status: 200,
3607
- json: patches
3608
- };
3609
- }
3610
- async deletePatches(query, cookies) {
3611
- const auth = this.getAuth(cookies);
3612
- if (auth.error) {
3613
- return {
3614
- status: 401,
3615
- json: {
3616
- message: auth.error
3617
- }
3618
- };
3619
- }
3620
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3621
- return {
3622
- status: 401,
3623
- json: {
3624
- message: "Unauthorized"
3625
- }
3626
- };
3627
- }
3628
- const ids = query.id;
3629
- const deleteRes = await this.serverOps.deletePatches(ids);
3630
- if (deleteRes.errors && Object.keys(deleteRes.errors).length > 0) {
3631
- console.error("Val: Failed to delete patches", deleteRes.errors);
3632
- return {
3633
- status: 500,
3634
- json: {
3635
- message: "Failed to delete patches",
3636
- details: deleteRes.errors
3637
- }
3638
- };
3639
- }
3640
- return {
3641
- status: 200,
3642
- json: ids
3643
- };
3644
- }
3645
-
3646
- //#region tree ops
3647
- async getSchema(cookies) {
3648
- const auth = this.getAuth(cookies);
3649
- if (auth.error) {
3650
- return {
3651
- status: 401,
3652
- json: {
3653
- message: auth.error
3654
- }
3655
- };
3656
- }
3657
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3658
- return {
3659
- status: 401,
3660
- json: {
3661
- message: "Unauthorized"
3662
- }
3663
- };
3664
- }
3665
- const moduleErrors = await this.serverOps.getModuleErrors();
3666
- if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
3667
- console.error("Val: Module errors", moduleErrors);
3668
- return {
3669
- status: 500,
3670
- json: {
3671
- message: "Val is not correctly setup. Check the val.modules file",
3672
- details: moduleErrors
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;
3673
3356
  }
3674
- };
3675
- }
3676
- const schemaSha = await this.serverOps.getSchemaSha();
3677
- const schemas = await this.serverOps.getSchemas();
3678
- const serializedSchemas = {};
3679
- for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
3680
- const moduleFilePath = moduleFilePathS;
3681
- serializedSchemas[moduleFilePath] = schema.serialize();
3682
- }
3683
- return {
3684
- status: 200,
3685
- json: {
3686
- schemaSha,
3687
- schemas: serializedSchemas
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
+ };
3688
3365
  }
3689
- };
3690
- }
3691
- async putTree(body, treePath, query, cookies) {
3692
- var _bodyRes$data, _bodyRes$data2, _bodyRes$data3;
3693
- const auth = this.getAuth(cookies);
3694
- if (auth.error) {
3695
- return {
3696
- status: 401,
3697
- json: {
3698
- message: auth.error
3699
- }
3700
- };
3701
- }
3702
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3703
- return {
3704
- status: 401,
3705
- json: {
3706
- message: "Unauthorized"
3707
- }
3708
- };
3709
- }
3710
- // TODO: move
3711
- const PutTreeBody = z.object({
3712
- patchIds: z.array(z.string().refine(id => true // TODO:
3713
- )).optional(),
3714
- addPatch: z.object({
3715
- path: z.string().refine(path => true // TODO:
3716
- ),
3717
- patch: Patch
3718
- }).optional()
3719
- }).optional();
3720
- const moduleErrors = await this.serverOps.getModuleErrors();
3721
- if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
3722
- console.error("Val: Module errors", moduleErrors);
3723
- return {
3724
- status: 500,
3725
- json: {
3726
- message: "Val is not correctly setup. Check the val.modules file",
3727
- details: moduleErrors
3728
- }
3729
- };
3730
- }
3731
- const bodyRes = PutTreeBody.safeParse(body);
3732
- if (!bodyRes.success) {
3733
- return {
3734
- status: 400,
3735
- json: {
3736
- message: "Invalid body: " + fromError(bodyRes.error).toString(),
3737
- details: bodyRes.error.errors
3738
- }
3739
- };
3740
- }
3741
- let tree;
3742
- let patchAnalysis = null;
3743
- let newPatchId = undefined;
3744
- 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) {
3745
- var _bodyRes$data4, _bodyRes$data5;
3746
- // TODO: validate patches_sha
3747
- const patchIds = (_bodyRes$data4 = bodyRes.data) === null || _bodyRes$data4 === void 0 ? void 0 : _bodyRes$data4.patchIds;
3748
- const patchOps = patchIds && patchIds.length > 0 ? await this.serverOps.fetchPatches({
3749
- patchIds,
3750
- omitPatch: false
3751
- }) : {
3752
- patches: {}
3753
- };
3754
- let patchErrors = undefined;
3755
- for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
3756
- const patchId = patchIdS;
3757
- if (!patchErrors) {
3758
- patchErrors = {};
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;
3759
3373
  }
3760
- patchErrors[patchId] = {
3761
- message: error.message
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
3762
3383
  };
3763
3384
  }
3764
- if ((_bodyRes$data5 = bodyRes.data) !== null && _bodyRes$data5 !== void 0 && _bodyRes$data5.addPatch) {
3765
- const newPatchModuleFilePath = bodyRes.data.addPatch.path;
3766
- const newPatchOps = bodyRes.data.addPatch.patch;
3767
- const authorId = "id" in auth ? auth.id : null;
3768
- const createPatchRes = await this.serverOps.createPatch(newPatchModuleFilePath, newPatchOps, authorId);
3769
- if (createPatchRes.error) {
3385
+ },
3386
+ "/authorize": {
3387
+ GET: async req => {
3388
+ const query = req.query;
3389
+ if (typeof query.redirect_to !== "string") {
3770
3390
  return {
3771
- status: 500,
3391
+ status: 400,
3772
3392
  json: {
3773
- message: "Failed to create patch: " + createPatchRes.error.message,
3774
- details: createPatchRes.error
3393
+ message: "Missing redirect_to query param"
3775
3394
  }
3776
3395
  };
3777
3396
  }
3778
- // TODO: evaluate if we need this: seems wrong to delete patches that are not applied
3779
- // for (const fileRes of createPatchRes.files) {
3780
- // if (fileRes.error) {
3781
- // // clean up broken patch:
3782
- // await this.serverOps.deletePatches([createPatchRes.patchId]);
3783
- // return {
3784
- // status: 500,
3785
- // json: {
3786
- // message: "Failed to create patch",
3787
- // details: fileRes.error,
3788
- // },
3789
- // };
3790
- // }
3791
- // }
3792
- newPatchId = createPatchRes.patchId;
3793
- patchOps.patches[createPatchRes.patchId] = {
3794
- path: newPatchModuleFilePath,
3795
- patch: newPatchOps,
3796
- authorId,
3797
- createdAt: createPatchRes.createdAt,
3798
- appliedAt: null
3799
- };
3800
- }
3801
- // TODO: errors
3802
- patchAnalysis = this.serverOps.analyzePatches(patchOps.patches);
3803
- tree = {
3804
- ...(await this.serverOps.getTree({
3805
- ...patchAnalysis,
3806
- ...patchOps
3807
- }))
3808
- };
3809
- if (query.validate_all === "true") {
3810
- const allTree = await this.serverOps.getTree();
3811
- tree = {
3812
- sources: {
3813
- ...allTree.sources,
3814
- ...tree.sources
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
+ }
3815
3415
  },
3816
- errors: {
3817
- ...allTree.errors,
3818
- ...tree.errors
3819
- }
3820
- };
3821
- }
3822
- } else {
3823
- tree = await this.serverOps.getTree();
3824
- }
3825
- if (tree.errors && Object.keys(tree.errors).length > 0) {
3826
- console.error("Val: Failed to get tree", JSON.stringify(tree.errors));
3827
- }
3828
- if (query.validate_sources === "true" || query.validate_binary_files === "true") {
3829
- const schemas = await this.serverOps.getSchemas();
3830
- const sourcesValidation = await this.serverOps.validateSources(schemas, tree.sources);
3831
-
3832
- // TODO: send validation errors
3833
- if (query.validate_binary_files === "true") {
3834
- await this.serverOps.validateFiles(schemas, tree.sources, sourcesValidation.files);
3835
- }
3836
- }
3837
- const schemaSha = await this.serverOps.getSchemaSha();
3838
- const modules = {};
3839
- for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
3840
- const moduleFilePath = moduleFilePathS;
3841
- if (moduleFilePath.startsWith(treePath)) {
3842
- modules[moduleFilePath] = {
3843
- source: module,
3844
- patches: patchAnalysis ? {
3845
- applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
3846
- } : undefined
3416
+ status: 302,
3417
+ redirectTo: appAuthorizeUrl
3847
3418
  };
3848
3419
  }
3849
- }
3850
- return {
3851
- status: 200,
3852
- json: {
3853
- schemaSha,
3854
- modules,
3855
- newPatchId
3856
- }
3857
- };
3858
- }
3859
- async postSave(body, cookies) {
3860
- const auth = this.getAuth(cookies);
3861
- if (auth.error) {
3862
- return {
3863
- status: 401,
3864
- json: {
3865
- message: auth.error
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
+ };
3866
3435
  }
3867
- };
3868
- }
3869
- const PostSaveBody = z.object({
3870
- patchIds: z.array(z.string().refine(id => true // TODO:
3871
- ))
3872
- });
3873
- const bodyRes = PostSaveBody.safeParse(body);
3874
- if (!bodyRes.success) {
3875
- return {
3876
- status: 400,
3877
- json: {
3878
- message: "Invalid body: " + fromError(bodyRes.error).toString(),
3879
- details: bodyRes.error.errors
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
+ };
3880
3446
  }
3881
- };
3882
- }
3883
- const {
3884
- patchIds
3885
- } = bodyRes.data;
3886
- const patches = await this.serverOps.fetchPatches({
3887
- patchIds,
3888
- omitPatch: false
3889
- });
3890
- const analysis = this.serverOps.analyzePatches(patches.patches);
3891
- const preparedCommit = await this.serverOps.prepare({
3892
- ...analysis,
3893
- ...patches
3894
- });
3895
- if (preparedCommit.hasErrors) {
3896
- console.error("Failed to create commit", {
3897
- sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
3898
- binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
3899
- });
3900
- return {
3901
- status: 400,
3902
- json: {
3903
- message: "Failed to create commit",
3904
- details: {
3905
- sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
3906
- binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
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
+ };
3461
+ }
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
+ };
3473
+ }
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
+ };
3486
+ }
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
+ };
3524
+ }
3525
+ if (!options.project) {
3526
+ return {
3527
+ status: 500,
3528
+ json: {
3529
+ message: "Project is not set"
3530
+ }
3531
+ };
3532
+ }
3533
+ if (!options.valSecret) {
3534
+ return {
3535
+ status: 500,
3536
+ json: {
3537
+ message: "Secret is not set"
3538
+ }
3539
+ };
3540
+ }
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
+ }
3907
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
+ };
3603
+ }
3604
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
3605
+ return {
3606
+ status: 401,
3607
+ json: {
3608
+ message: "Unauthorized"
3609
+ }
3610
+ };
3611
+ }
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
+ };
3629
+ }
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
+ };
3640
+ }
3641
+ return {
3642
+ status: 200,
3643
+ json: patches
3644
+ };
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);
3670
+ return {
3671
+ status: 500,
3672
+ json: {
3673
+ message: "Failed to delete patches",
3674
+ details: deleteRes.errors
3675
+ }
3676
+ };
3908
3677
  }
3909
- };
3910
- }
3911
- if (this.serverOps instanceof ValOpsFS) {
3912
- await this.serverOps.saveFiles(preparedCommit);
3913
- return {
3914
- status: 200,
3915
- json: {} // TODO:
3916
- };
3917
- } else if (this.serverOps instanceof ValOpsHttp) {
3918
- if (auth.error === undefined && auth.id) {
3919
- await this.serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
3920
3678
  return {
3921
3679
  status: 200,
3922
- json: {} // TODO:
3680
+ json: ids
3923
3681
  };
3924
3682
  }
3925
- return {
3926
- status: 401,
3927
- json: {
3928
- message: "Unauthorized"
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
+ };
3929
3696
  }
3930
- };
3931
- } else {
3932
- throw new Error("Invalid server ops");
3933
- }
3934
- }
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
3728
+ }
3729
+ };
3730
+ }
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);
3935
3871
 
3936
- //#region files
3937
- async getFiles(filePath, query) {
3938
- // NOTE: no auth here since you would need the patch_id to get something that is not published.
3939
- // For everything that is published, well they are already public so no auth required there...
3940
- // We could imagine adding auth just to be a 200% certain,
3941
- // 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, ...).
3942
- // So: 1) patch ids are not possible to guess (but possible to brute force)
3943
- // 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)
3944
- // 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
3945
- // Thus: attack surface + ease of attack + benefit = low probability of attack
3946
- // If we couldn't argue that patch ids are secret enough, then this would be a problem.
3947
- let fileBuffer;
3948
- if (query.patch_id) {
3949
- fileBuffer = await this.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
3950
- } else {
3951
- fileBuffer = await this.serverOps.getBinaryFile(filePath);
3952
- }
3953
- if (fileBuffer) {
3954
- return {
3955
- status: 200,
3956
- body: bufferToReadableStream(fileBuffer)
3957
- };
3958
- } else {
3959
- return {
3960
- status: 404,
3961
- json: {
3962
- message: "File not found"
3872
+ // TODO: send validation errors
3873
+ if (query.validate_binary_files) {
3874
+ await serverOps.validateFiles(schemas, tree.sources, sourcesValidation.files);
3875
+ }
3963
3876
  }
3964
- };
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
+ }
3897
+ };
3898
+ return res;
3899
+ }
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
+ };
3913
+ }
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
+ };
3927
+ }
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", {
3942
+ sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
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
+ };
3971
+ }
3972
+ return {
3973
+ status: 401,
3974
+ json: {
3975
+ message: "Unauthorized"
3976
+ }
3977
+ };
3978
+ } else {
3979
+ throw new Error("Invalid server ops");
3980
+ }
3981
+ }
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);
4002
+ }
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
+ };
4015
+ }
4016
+ }
3965
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);
3966
4028
  }
3967
4029
  }
3968
4030
  function verifyCallbackReq(stateCookie, queryParams) {
@@ -4082,7 +4144,7 @@ const IntegratedServerJwtPayload = z.object({
4082
4144
  project: z.string()
4083
4145
  });
4084
4146
  async function withAuth(secret, cookies, errorMessageType, handler) {
4085
- const cookie = cookies[VAL_SESSION_COOKIE$1];
4147
+ const cookie = cookies[VAL_SESSION_COOKIE];
4086
4148
  if (typeof cookie === "string") {
4087
4149
  const decodedToken = decodeJwt(cookie, secret);
4088
4150
  if (!decodedToken) {
@@ -4100,7 +4162,7 @@ async function withAuth(secret, cookies, errorMessageType, handler) {
4100
4162
  status: 401,
4101
4163
  json: {
4102
4164
  message: "Session invalid or, most likely, expired. You will need to login again.",
4103
- details: verification.error
4165
+ details: fromError(verification.error).toString()
4104
4166
  }
4105
4167
  };
4106
4168
  }
@@ -4262,7 +4324,7 @@ function guessMimeTypeFromPath(filePath) {
4262
4324
 
4263
4325
  async function createValServer(valModules, route, opts, callbacks, formatter) {
4264
4326
  const valServerConfig = await initHandlerOptions(route, opts);
4265
- return new ValServer(valModules, {
4327
+ return ValServer(valModules, {
4266
4328
  formatter,
4267
4329
  ...valServerConfig
4268
4330
  }, callbacks);
@@ -4387,17 +4449,9 @@ async function readCommit(gitDir, branchName) {
4387
4449
  return undefined;
4388
4450
  }
4389
4451
  }
4390
- const {
4391
- VAL_SESSION_COOKIE,
4392
- VAL_STATE_COOKIE
4393
- } = Internal;
4394
- const TREE_PATH_PREFIX = "/tree/~";
4395
- const PATCHES_PATH_PREFIX = "/patches/~";
4396
- const FILES_PATH_PREFIX = "/files";
4397
4452
  function createValApiRouter(route, valServerPromise, convert) {
4398
4453
  const uiRequestHandler = createUIRequestHandler();
4399
4454
  return async req => {
4400
- var _req$method;
4401
4455
  const valServer = await valServerPromise;
4402
4456
  const url = new URL(req.url);
4403
4457
  if (!url.pathname.startsWith(route)) {
@@ -4411,115 +4465,213 @@ function createValApiRouter(route, valServerPromise, convert) {
4411
4465
  json: error
4412
4466
  });
4413
4467
  }
4414
- const method = (_req$method = req.method) === null || _req$method === void 0 ? void 0 : _req$method.toUpperCase();
4415
- function withTreePath(path, prefix) {
4416
- return async useTreePath => {
4417
- const pathIndex = path.indexOf("~");
4418
- if (path.startsWith(prefix) && pathIndex !== -1) {
4419
- return useTreePath(path.slice(pathIndex + 1));
4420
- } else {
4421
- if (prefix.indexOf("/~") === -1) {
4422
- return convert({
4423
- status: 500,
4424
- json: {
4425
- 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;
4426
4505
  }
4427
- });
4506
+ }
4507
+ break;
4428
4508
  }
4429
- return convert({
4430
- 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,
4431
4627
  json: {
4432
- 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
+ }
4433
4633
  }
4434
- });
4634
+ };
4435
4635
  }
4436
- };
4636
+ }
4637
+ return res;
4437
4638
  }
4438
- const path = url.pathname.slice(route.length);
4439
4639
  if (path.startsWith("/static")) {
4440
4640
  return convert(await uiRequestHandler(path.slice("/static".length), url.href));
4441
- } else if (path === "/session") {
4442
- return convert(await valServer.session(getCookies(req, [VAL_SESSION_COOKIE])));
4443
- } else if (path === "/authorize") {
4444
- return convert(await valServer.authorize({
4445
- redirect_to: url.searchParams.get("redirect_to") || undefined
4446
- }));
4447
- } else if (path === "/callback") {
4448
- return convert(await valServer.callback({
4449
- code: url.searchParams.get("code") || undefined,
4450
- state: url.searchParams.get("state") || undefined
4451
- }, getCookies(req, [VAL_STATE_COOKIE])));
4452
- } else if (path === "/logout") {
4453
- return convert(await valServer.logout());
4454
- } else if (path === "/enable") {
4455
- return convert(await valServer.enable({
4456
- redirect_to: url.searchParams.get("redirect_to") || undefined
4457
- }));
4458
- } else if (path === "/disable") {
4459
- return convert(await valServer.disable({
4460
- redirect_to: url.searchParams.get("redirect_to") || undefined
4461
- }));
4462
- } else if (method === "POST" && path === "/save") {
4463
- const body = await req.json();
4464
- return convert(await valServer.postSave(body, getCookies(req, [VAL_SESSION_COOKIE])));
4465
- // } else if (method === "POST" && path === "/validate") {
4466
- // const body = (await req.json()) as unknown;
4467
- // return convert(
4468
- // await valServer.postValidate(
4469
- // body,
4470
- // getCookies(req, [VAL_SESSION_COOKIE]),
4471
- // requestHeaders
4472
- // )
4473
- // );
4474
- } else if (method === "GET" && path === "/schema") {
4475
- return convert(await valServer.getSchema(getCookies(req, [VAL_SESSION_COOKIE])));
4476
- } else if (method === "PUT" && path.startsWith(TREE_PATH_PREFIX)) {
4477
- return withTreePath(path, TREE_PATH_PREFIX)(async treePath => convert(await valServer.putTree(await req.json(), treePath, {
4478
- patches_sha: url.searchParams.get("patches_sha") || undefined,
4479
- validate_all: url.searchParams.get("validate_all") || undefined,
4480
- validate_binary_files: url.searchParams.get("validate_binary_files") || undefined,
4481
- validate_sources: url.searchParams.get("validate_sources") || undefined
4482
- }, getCookies(req, [VAL_SESSION_COOKIE]))));
4483
- } else if (method === "GET" && path.startsWith(PATCHES_PATH_PREFIX)) {
4484
- return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.getPatches({
4485
- authors: url.searchParams.getAll("author"),
4486
- patchIds: url.searchParams.getAll("patch_id"),
4487
- omitPatch: url.searchParams.get("omit_patch") || undefined
4488
- }, getCookies(req, [VAL_SESSION_COOKIE]))));
4489
- } else if (method === "DELETE" && path.startsWith(PATCHES_PATH_PREFIX)) {
4490
- return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.deletePatches({
4491
- id: url.searchParams.getAll("id")
4492
- }, getCookies(req, [VAL_SESSION_COOKIE]))));
4493
- } else if (method === "GET" && path.startsWith(FILES_PATH_PREFIX)) {
4494
- const treePath = path.slice(FILES_PATH_PREFIX.length);
4495
- return convert(await valServer.getFiles(treePath, {
4496
- patch_id: url.searchParams.get("patch_id") || undefined
4497
- }));
4498
4641
  } else {
4499
- return convert({
4500
- status: 404,
4501
- json: {
4502
- message: "Not Found",
4503
- details: {
4504
- method,
4505
- path
4506
- }
4507
- }
4508
- });
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
+ }
4509
4658
  }
4510
4659
  };
4511
4660
  }
4512
4661
 
4513
4662
  // TODO: is this naive implementation is too naive?
4514
- function getCookies(req, names) {
4515
- var _req$headers$get;
4516
- return ((_req$headers$get = req.headers.get("Cookie")) === null || _req$headers$get === void 0 ? void 0 : _req$headers$get.split("; ").reduce((acc, cookie) => {
4517
- const [name, value] = cookie.split("=");
4518
- if (names.includes(name.trim())) {
4519
- acc[name.trim()] = decodeURIComponent(value.trim());
4520
- }
4521
- return acc;
4522
- }, {})) || {};
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);
4523
4675
  }
4524
4676
 
4525
4677
  /**