@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.
@@ -11,8 +11,8 @@ var fsPath = require('path');
11
11
  var fs = require('fs');
12
12
  var sucrase = require('sucrase');
13
13
  var ui = require('@valbuild/ui');
14
- var server = require('@valbuild/ui/server');
15
14
  var internal = require('@valbuild/shared/internal');
15
+ var server = require('@valbuild/ui/server');
16
16
  var crypto$1 = require('crypto');
17
17
  var z = require('zod');
18
18
  var sizeOf = require('image-size');
@@ -1279,10 +1279,10 @@ async function extractImageMetadata(filename, input) {
1279
1279
  let mimeType = null;
1280
1280
  if (imageSize.type) {
1281
1281
  const possibleMimeType = `image/${imageSize.type}`;
1282
- if (internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
1282
+ if (core.Internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
1283
1283
  mimeType = possibleMimeType;
1284
1284
  }
1285
- const filenameBasedLookup = internal.filenameToMimeType(filename);
1285
+ const filenameBasedLookup = core.Internal.filenameToMimeType(filename);
1286
1286
  if (filenameBasedLookup) {
1287
1287
  mimeType = filenameBasedLookup;
1288
1288
  }
@@ -1312,7 +1312,7 @@ function getSha256(mimeType, input) {
1312
1312
  `data:${mimeType};base64,${input.toString("base64")}`));
1313
1313
  }
1314
1314
  async function extractFileMetadata(filename, input) {
1315
- let mimeType = internal.filenameToMimeType(filename);
1315
+ let mimeType = core.Internal.filenameToMimeType(filename);
1316
1316
  if (!mimeType) {
1317
1317
  mimeType = "application/octet-stream";
1318
1318
  }
@@ -1520,10 +1520,14 @@ class ValOps {
1520
1520
  if (!errors[path]) {
1521
1521
  errors[path] = [];
1522
1522
  }
1523
- errors[path].push({
1523
+ errors[path].push(...patches.map(({
1524
+ patchId
1525
+ }) => ({
1526
+ patchId,
1524
1527
  invalidPath: true,
1528
+ skipped: true,
1525
1529
  error: new patch.PatchError(`Module at path: '${path}' not found`)
1526
- });
1530
+ })));
1527
1531
  }
1528
1532
  patchedSources[path] = sources[path];
1529
1533
  for (const {
@@ -1532,6 +1536,7 @@ class ValOps {
1532
1536
  if (errors[path]) {
1533
1537
  errors[path].push({
1534
1538
  patchId: patchId,
1539
+ skipped: true,
1535
1540
  error: new patch.PatchError(`Cannot apply patch: previous errors exists`)
1536
1541
  });
1537
1542
  } else {
@@ -1539,6 +1544,7 @@ class ValOps {
1539
1544
  if (!patchData) {
1540
1545
  errors[path] = [{
1541
1546
  patchId: patchId,
1547
+ skipped: false,
1542
1548
  error: new patch.PatchError(`Patch not found`)
1543
1549
  }];
1544
1550
  continue;
@@ -1569,6 +1575,7 @@ class ValOps {
1569
1575
  }
1570
1576
  errors[path].push({
1571
1577
  patchId: patchId,
1578
+ skipped: false,
1572
1579
  error: patchRes.error
1573
1580
  });
1574
1581
  } else {
@@ -2768,7 +2775,7 @@ const BasePatchResponse = z.z.object({
2768
2775
  });
2769
2776
  const GetPatches = z.z.object({
2770
2777
  patches: z.z.array(z.z.intersection(z.z.object({
2771
- patch: Patch
2778
+ patch: Patch.optional()
2772
2779
  }), BasePatchResponse)),
2773
2780
  errors: z.z.array(z.z.object({
2774
2781
  patchId: PatchId.optional(),
@@ -3241,251 +3248,59 @@ class ValOpsHttp extends ValOps {
3241
3248
  }
3242
3249
 
3243
3250
  /* eslint-disable @typescript-eslint/no-unused-vars */
3244
- class ValServer {
3245
- constructor(valModules, options, callbacks) {
3246
- this.valModules = valModules;
3247
- this.options = options;
3248
- this.callbacks = callbacks;
3249
- if (options.mode === "fs") {
3250
- this.serverOps = new ValOpsFS(options.cwd, valModules, {
3251
- formatter: options.formatter
3252
- });
3253
- } else if (options.mode === "http") {
3254
- this.serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
3255
- formatter: options.formatter,
3256
- root: options.root
3257
- });
3258
- } else {
3259
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3260
- throw new Error("Invalid mode: " + (options === null || options === void 0 ? void 0 : options.mode));
3261
- }
3262
- }
3263
-
3264
- //#region auth
3265
- async enable(query) {
3266
- const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
3267
- if (typeof redirectToRes !== "string") {
3268
- return redirectToRes;
3269
- }
3270
- await this.callbacks.onEnable(true);
3271
- return {
3272
- cookies: {
3273
- [internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
3274
- },
3275
- status: 302,
3276
- redirectTo: redirectToRes
3277
- };
3278
- }
3279
- async disable(query) {
3280
- const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
3281
- if (typeof redirectToRes !== "string") {
3282
- return redirectToRes;
3283
- }
3284
- await this.callbacks.onDisable(true);
3285
- return {
3286
- cookies: {
3287
- [internal.VAL_ENABLE_COOKIE_NAME]: {
3288
- value: "false"
3289
- }
3290
- },
3291
- status: 302,
3292
- redirectTo: redirectToRes
3293
- };
3294
- }
3295
- async authorize(query) {
3296
- if (typeof query.redirect_to !== "string") {
3297
- return {
3298
- status: 400,
3299
- json: {
3300
- message: "Missing redirect_to query param"
3301
- }
3302
- };
3303
- }
3304
- const token = crypto.randomUUID();
3305
- const redirectUrl = new URL(query.redirect_to);
3306
- const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
3307
- await this.callbacks.onEnable(true);
3308
- return {
3309
- cookies: {
3310
- [internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3311
- [internal.VAL_STATE_COOKIE]: {
3312
- value: createStateCookie({
3313
- redirect_to: query.redirect_to,
3314
- token
3315
- }),
3316
- options: {
3317
- httpOnly: true,
3318
- sameSite: "lax",
3319
- expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
3320
- }
3321
- }
3322
- },
3323
- status: 302,
3324
- redirectTo: appAuthorizeUrl
3325
- };
3251
+ const ValServer = (valModules, options, callbacks) => {
3252
+ let serverOps;
3253
+ if (options.mode === "fs") {
3254
+ serverOps = new ValOpsFS(options.cwd, valModules, {
3255
+ formatter: options.formatter
3256
+ });
3257
+ } else if (options.mode === "http") {
3258
+ serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
3259
+ formatter: options.formatter,
3260
+ root: options.root
3261
+ });
3262
+ } else {
3263
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3264
+ throw new Error("Invalid mode: " + (options === null || options === void 0 ? void 0 : options.mode));
3326
3265
  }
3327
- async callback(query, cookies) {
3328
- if (!this.options.project) {
3329
- return {
3330
- status: 302,
3331
- cookies: {
3332
- [internal.VAL_STATE_COOKIE]: {
3333
- value: null
3334
- }
3335
- },
3336
- redirectTo: this.getAppErrorUrl("Project is not set")
3337
- };
3338
- }
3339
- if (!this.options.valSecret) {
3340
- return {
3341
- status: 302,
3342
- cookies: {
3343
- [internal.VAL_STATE_COOKIE]: {
3344
- value: null
3345
- }
3346
- },
3347
- redirectTo: this.getAppErrorUrl("Secret is not set")
3348
- };
3349
- }
3350
- const {
3351
- success: callbackReqSuccess,
3352
- error: callbackReqError
3353
- } = verifyCallbackReq(cookies[internal.VAL_STATE_COOKIE], query);
3354
- if (callbackReqError !== null) {
3355
- return {
3356
- status: 302,
3357
- cookies: {
3358
- [internal.VAL_STATE_COOKIE]: {
3359
- value: null
3360
- }
3361
- },
3362
- redirectTo: this.getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`)
3363
- };
3364
- }
3365
- const data = await this.consumeCode(callbackReqSuccess.code);
3366
- if (data === null) {
3367
- return {
3368
- status: 302,
3369
- cookies: {
3370
- [internal.VAL_STATE_COOKIE]: {
3371
- value: null
3372
- }
3373
- },
3374
- redirectTo: this.getAppErrorUrl("Failed to exchange code for user")
3375
- };
3376
- }
3377
- const exp = getExpire();
3378
- const valSecret = this.options.valSecret;
3379
- if (!valSecret) {
3380
- return {
3381
- status: 302,
3382
- cookies: {
3383
- [internal.VAL_STATE_COOKIE]: {
3384
- value: null
3385
- }
3386
- },
3387
- redirectTo: this.getAppErrorUrl("Setup is not correct: secret is missing")
3388
- };
3266
+ const getAuthorizeUrl = (publicValApiRe, token) => {
3267
+ if (!options.project) {
3268
+ throw new Error("Project is not set");
3389
3269
  }
3390
- const cookie = encodeJwt({
3391
- ...data,
3392
- exp // this is the client side exp
3393
- }, valSecret);
3394
- return {
3395
- status: 302,
3396
- cookies: {
3397
- [internal.VAL_STATE_COOKIE]: {
3398
- value: null
3399
- },
3400
- [internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3401
- [internal.VAL_SESSION_COOKIE]: {
3402
- value: cookie,
3403
- options: {
3404
- httpOnly: true,
3405
- sameSite: "strict",
3406
- path: "/",
3407
- secure: true,
3408
- expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
3409
- }
3410
- }
3411
- },
3412
- redirectTo: callbackReqSuccess.redirect_uri || "/"
3413
- };
3414
- }
3415
- async session(cookies) {
3416
- if (this.serverOps instanceof ValOpsFS) {
3417
- return {
3418
- status: 200,
3419
- json: {
3420
- mode: "local",
3421
- enabled: await this.callbacks.isEnabled()
3422
- }
3423
- };
3270
+ if (!options.valBuildUrl) {
3271
+ throw new Error("Val build url is not set");
3424
3272
  }
3425
- if (!this.options.project) {
3426
- return {
3427
- status: 500,
3428
- json: {
3429
- message: "Project is not set"
3430
- }
3431
- };
3273
+ const url = new URL(`/auth/${options.project}/authorize`, options.valBuildUrl);
3274
+ url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRe}/callback`));
3275
+ url.searchParams.set("state", token);
3276
+ return url.toString();
3277
+ };
3278
+ const getAppErrorUrl = error => {
3279
+ if (!options.project) {
3280
+ throw new Error("Project is not set");
3432
3281
  }
3433
- if (!this.options.valSecret) {
3434
- return {
3435
- status: 500,
3436
- json: {
3437
- message: "Secret is not set"
3438
- }
3439
- };
3282
+ if (!options.valBuildUrl) {
3283
+ throw new Error("Val build url is not set");
3440
3284
  }
3441
- return withAuth(this.options.valSecret, cookies, "session", async data => {
3442
- if (!this.options.valBuildUrl) {
3443
- return {
3444
- status: 500,
3445
- json: {
3446
- message: "Val is not correctly setup. Build url is missing"
3447
- }
3448
- };
3449
- }
3450
- const url = new URL(`/api/val/${this.options.project}/auth/session`, this.options.valBuildUrl);
3451
- const fetchRes = await fetch(url, {
3452
- headers: getAuthHeaders(data.token, "application/json")
3453
- });
3454
- if (fetchRes.status === 200) {
3455
- return {
3456
- status: fetchRes.status,
3457
- json: {
3458
- mode: "proxy",
3459
- enabled: await this.callbacks.isEnabled(),
3460
- ...(await fetchRes.json())
3461
- }
3462
- };
3463
- } else {
3464
- return {
3465
- status: fetchRes.status,
3466
- json: {
3467
- message: "Failed to authorize",
3468
- ...(await fetchRes.json())
3469
- }
3470
- };
3471
- }
3472
- });
3473
- }
3474
- async consumeCode(code) {
3475
- if (!this.options.project) {
3285
+ const url = new URL(`/auth/${options.project}/authorize`, options.valBuildUrl);
3286
+ url.searchParams.set("error", encodeURIComponent(error));
3287
+ return url.toString();
3288
+ };
3289
+ const consumeCode = async code => {
3290
+ if (!options.project) {
3476
3291
  throw new Error("Project is not set");
3477
3292
  }
3478
- if (!this.options.valBuildUrl) {
3293
+ if (!options.valBuildUrl) {
3479
3294
  throw new Error("Val build url is not set");
3480
3295
  }
3481
- const url = new URL(`/api/val/${this.options.project}/auth/token`, this.options.valBuildUrl);
3296
+ const url = new URL(`/api/val/${options.project}/auth/token`, options.valBuildUrl);
3482
3297
  url.searchParams.set("code", encodeURIComponent(code));
3483
- if (!this.options.apiKey) {
3298
+ if (!options.apiKey) {
3484
3299
  return null;
3485
3300
  }
3486
3301
  return fetch(url, {
3487
3302
  method: "POST",
3488
- headers: getAuthHeaders(this.options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
3303
+ headers: getAuthHeaders(options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
3489
3304
  }).then(async res => {
3490
3305
  if (res.status === 200) {
3491
3306
  const token = await res.text();
@@ -3505,34 +3320,11 @@ class ValServer {
3505
3320
  console.debug("Failed to get user from code: ", err);
3506
3321
  return null;
3507
3322
  });
3508
- }
3509
- getAuthorizeUrl(publicValApiRe, token) {
3510
- if (!this.options.project) {
3511
- throw new Error("Project is not set");
3512
- }
3513
- if (!this.options.valBuildUrl) {
3514
- throw new Error("Val build url is not set");
3515
- }
3516
- const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
3517
- url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRe}/callback`));
3518
- url.searchParams.set("state", token);
3519
- return url.toString();
3520
- }
3521
- getAppErrorUrl(error) {
3522
- if (!this.options.project) {
3523
- throw new Error("Project is not set");
3524
- }
3525
- if (!this.options.valBuildUrl) {
3526
- throw new Error("Val build url is not set");
3527
- }
3528
- const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
3529
- url.searchParams.set("error", encodeURIComponent(error));
3530
- return url.toString();
3531
- }
3532
- getAuth(cookies) {
3323
+ };
3324
+ const getAuth = cookies => {
3533
3325
  const cookie = cookies[internal.VAL_SESSION_COOKIE];
3534
- if (!this.options.valSecret) {
3535
- if (this.serverOps instanceof ValOpsFS) {
3326
+ if (!options.valSecret) {
3327
+ if (serverOps instanceof ValOpsFS) {
3536
3328
  return {
3537
3329
  error: null,
3538
3330
  id: null
@@ -3544,9 +3336,9 @@ class ValServer {
3544
3336
  }
3545
3337
  }
3546
3338
  if (typeof cookie === "string") {
3547
- const decodedToken = decodeJwt(cookie, this.options.valSecret);
3339
+ const decodedToken = decodeJwt(cookie, options.valSecret);
3548
3340
  if (!decodedToken) {
3549
- if (this.serverOps instanceof ValOpsFS) {
3341
+ if (serverOps instanceof ValOpsFS) {
3550
3342
  return {
3551
3343
  error: null,
3552
3344
  id: null
@@ -3558,7 +3350,7 @@ class ValServer {
3558
3350
  }
3559
3351
  const verification = IntegratedServerJwtPayload.safeParse(decodedToken);
3560
3352
  if (!verification.success) {
3561
- if (this.serverOps instanceof ValOpsFS) {
3353
+ if (serverOps instanceof ValOpsFS) {
3562
3354
  return {
3563
3355
  error: null,
3564
3356
  id: null
@@ -3572,7 +3364,7 @@ class ValServer {
3572
3364
  id: verification.data.sub
3573
3365
  };
3574
3366
  } else {
3575
- if (this.serverOps instanceof ValOpsFS) {
3367
+ if (serverOps instanceof ValOpsFS) {
3576
3368
  return {
3577
3369
  error: null,
3578
3370
  id: null
@@ -3582,417 +3374,687 @@ class ValServer {
3582
3374
  error: "Login required: cookie not found"
3583
3375
  };
3584
3376
  }
3585
- }
3586
- async logout() {
3587
- return {
3588
- status: 200,
3589
- cookies: {
3590
- [internal.VAL_SESSION_COOKIE]: {
3591
- value: null
3592
- },
3593
- [internal.VAL_STATE_COOKIE]: {
3594
- value: null
3595
- }
3596
- }
3597
- };
3598
- }
3599
-
3600
- //#region patches
3601
- async getPatches(query, cookies) {
3602
- const auth = this.getAuth(cookies);
3603
- if (auth.error) {
3604
- return {
3605
- status: 401,
3606
- json: {
3607
- message: auth.error
3608
- }
3609
- };
3610
- }
3611
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3612
- return {
3613
- status: 401,
3614
- json: {
3615
- message: "Unauthorized"
3616
- }
3617
- };
3618
- }
3619
- const authors = query.authors;
3620
- const patches = await this.serverOps.fetchPatches({
3621
- authors,
3622
- patchIds: query.patchIds,
3623
- omitPatch: query.omitPatch === "true"
3624
- });
3625
- if (patches.errors && Object.keys(patches.errors).length > 0) {
3626
- console.error("Val: Failed to get patches", patches.errors);
3627
- return {
3628
- status: 500,
3629
- json: {
3630
- message: "Failed to get patches",
3631
- details: patches.errors
3632
- }
3633
- };
3634
- }
3635
- return {
3636
- status: 200,
3637
- json: patches
3638
- };
3639
- }
3640
- async deletePatches(query, cookies) {
3641
- const auth = this.getAuth(cookies);
3642
- if (auth.error) {
3643
- return {
3644
- status: 401,
3645
- json: {
3646
- message: auth.error
3647
- }
3648
- };
3649
- }
3650
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3651
- return {
3652
- status: 401,
3653
- json: {
3654
- message: "Unauthorized"
3655
- }
3656
- };
3657
- }
3658
- const ids = query.id;
3659
- const deleteRes = await this.serverOps.deletePatches(ids);
3660
- if (deleteRes.errors && Object.keys(deleteRes.errors).length > 0) {
3661
- console.error("Val: Failed to delete patches", deleteRes.errors);
3662
- return {
3663
- status: 500,
3664
- json: {
3665
- message: "Failed to delete patches",
3666
- details: deleteRes.errors
3667
- }
3668
- };
3669
- }
3670
- return {
3671
- status: 200,
3672
- json: ids
3673
- };
3674
- }
3675
-
3676
- //#region tree ops
3677
- async getSchema(cookies) {
3678
- const auth = this.getAuth(cookies);
3679
- if (auth.error) {
3680
- return {
3681
- status: 401,
3682
- json: {
3683
- message: auth.error
3684
- }
3685
- };
3686
- }
3687
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3688
- return {
3689
- status: 401,
3690
- json: {
3691
- message: "Unauthorized"
3692
- }
3693
- };
3694
- }
3695
- const moduleErrors = await this.serverOps.getModuleErrors();
3696
- if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
3697
- console.error("Val: Module errors", moduleErrors);
3698
- return {
3699
- status: 500,
3700
- json: {
3701
- message: "Val is not correctly setup. Check the val.modules file",
3702
- details: moduleErrors
3377
+ };
3378
+ return {
3379
+ //#region auth
3380
+ "/enable": {
3381
+ GET: async req => {
3382
+ const query = req.query;
3383
+ const redirectToRes = getRedirectUrl(query, options.valEnableRedirectUrl);
3384
+ if (typeof redirectToRes !== "string") {
3385
+ return redirectToRes;
3703
3386
  }
3704
- };
3705
- }
3706
- const schemaSha = await this.serverOps.getSchemaSha();
3707
- const schemas = await this.serverOps.getSchemas();
3708
- const serializedSchemas = {};
3709
- for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
3710
- const moduleFilePath = moduleFilePathS;
3711
- serializedSchemas[moduleFilePath] = schema.serialize();
3712
- }
3713
- return {
3714
- status: 200,
3715
- json: {
3716
- schemaSha,
3717
- schemas: serializedSchemas
3387
+ await callbacks.onEnable(true);
3388
+ return {
3389
+ cookies: {
3390
+ [internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
3391
+ },
3392
+ status: 302,
3393
+ redirectTo: redirectToRes
3394
+ };
3718
3395
  }
3719
- };
3720
- }
3721
- async putTree(body, treePath, query, cookies) {
3722
- var _bodyRes$data, _bodyRes$data2, _bodyRes$data3;
3723
- const auth = this.getAuth(cookies);
3724
- if (auth.error) {
3725
- return {
3726
- status: 401,
3727
- json: {
3728
- message: auth.error
3729
- }
3730
- };
3731
- }
3732
- if (this.serverOps instanceof ValOpsHttp && !("id" in auth)) {
3733
- return {
3734
- status: 401,
3735
- json: {
3736
- message: "Unauthorized"
3737
- }
3738
- };
3739
- }
3740
- // TODO: move
3741
- const PutTreeBody = z.z.object({
3742
- patchIds: z.z.array(z.z.string().refine(id => true // TODO:
3743
- )).optional(),
3744
- addPatch: z.z.object({
3745
- path: z.z.string().refine(path => true // TODO:
3746
- ),
3747
- patch: Patch
3748
- }).optional()
3749
- }).optional();
3750
- const moduleErrors = await this.serverOps.getModuleErrors();
3751
- if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
3752
- console.error("Val: Module errors", moduleErrors);
3753
- return {
3754
- status: 500,
3755
- json: {
3756
- message: "Val is not correctly setup. Check the val.modules file",
3757
- details: moduleErrors
3758
- }
3759
- };
3760
- }
3761
- const bodyRes = PutTreeBody.safeParse(body);
3762
- if (!bodyRes.success) {
3763
- return {
3764
- status: 400,
3765
- json: {
3766
- message: "Invalid body: " + zodValidationError.fromError(bodyRes.error).toString(),
3767
- details: bodyRes.error.errors
3768
- }
3769
- };
3770
- }
3771
- let tree;
3772
- let patchAnalysis = null;
3773
- let newPatchId = undefined;
3774
- 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) {
3775
- var _bodyRes$data4, _bodyRes$data5;
3776
- // TODO: validate patches_sha
3777
- const patchIds = (_bodyRes$data4 = bodyRes.data) === null || _bodyRes$data4 === void 0 ? void 0 : _bodyRes$data4.patchIds;
3778
- const patchOps = patchIds && patchIds.length > 0 ? await this.serverOps.fetchPatches({
3779
- patchIds,
3780
- omitPatch: false
3781
- }) : {
3782
- patches: {}
3783
- };
3784
- let patchErrors = undefined;
3785
- for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
3786
- const patchId = patchIdS;
3787
- if (!patchErrors) {
3788
- patchErrors = {};
3396
+ },
3397
+ "/disable": {
3398
+ GET: async req => {
3399
+ const query = req.query;
3400
+ const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
3401
+ if (typeof redirectToRes !== "string") {
3402
+ return redirectToRes;
3789
3403
  }
3790
- patchErrors[patchId] = {
3791
- message: error.message
3404
+ await callbacks.onDisable(true);
3405
+ return {
3406
+ cookies: {
3407
+ [internal.VAL_ENABLE_COOKIE_NAME]: {
3408
+ value: "false"
3409
+ }
3410
+ },
3411
+ status: 302,
3412
+ redirectTo: redirectToRes
3792
3413
  };
3793
3414
  }
3794
- if ((_bodyRes$data5 = bodyRes.data) !== null && _bodyRes$data5 !== void 0 && _bodyRes$data5.addPatch) {
3795
- const newPatchModuleFilePath = bodyRes.data.addPatch.path;
3796
- const newPatchOps = bodyRes.data.addPatch.patch;
3797
- const authorId = "id" in auth ? auth.id : null;
3798
- const createPatchRes = await this.serverOps.createPatch(newPatchModuleFilePath, newPatchOps, authorId);
3799
- if (createPatchRes.error) {
3415
+ },
3416
+ "/authorize": {
3417
+ GET: async req => {
3418
+ const query = req.query;
3419
+ if (typeof query.redirect_to !== "string") {
3800
3420
  return {
3801
- status: 500,
3421
+ status: 400,
3802
3422
  json: {
3803
- message: "Failed to create patch: " + createPatchRes.error.message,
3804
- details: createPatchRes.error
3423
+ message: "Missing redirect_to query param"
3805
3424
  }
3806
3425
  };
3807
3426
  }
3808
- // TODO: evaluate if we need this: seems wrong to delete patches that are not applied
3809
- // for (const fileRes of createPatchRes.files) {
3810
- // if (fileRes.error) {
3811
- // // clean up broken patch:
3812
- // await this.serverOps.deletePatches([createPatchRes.patchId]);
3813
- // return {
3814
- // status: 500,
3815
- // json: {
3816
- // message: "Failed to create patch",
3817
- // details: fileRes.error,
3818
- // },
3819
- // };
3820
- // }
3821
- // }
3822
- newPatchId = createPatchRes.patchId;
3823
- patchOps.patches[createPatchRes.patchId] = {
3824
- path: newPatchModuleFilePath,
3825
- patch: newPatchOps,
3826
- authorId,
3827
- createdAt: createPatchRes.createdAt,
3828
- appliedAt: null
3829
- };
3830
- }
3831
- // TODO: errors
3832
- patchAnalysis = this.serverOps.analyzePatches(patchOps.patches);
3833
- tree = {
3834
- ...(await this.serverOps.getTree({
3835
- ...patchAnalysis,
3836
- ...patchOps
3837
- }))
3838
- };
3839
- if (query.validate_all === "true") {
3840
- const allTree = await this.serverOps.getTree();
3841
- tree = {
3842
- sources: {
3843
- ...allTree.sources,
3844
- ...tree.sources
3427
+ const token = crypto.randomUUID();
3428
+ const redirectUrl = new URL(query.redirect_to);
3429
+ const appAuthorizeUrl = getAuthorizeUrl(`${redirectUrl.origin}/${options.route}`, token);
3430
+ await callbacks.onEnable(true);
3431
+ return {
3432
+ cookies: {
3433
+ [internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3434
+ [internal.VAL_STATE_COOKIE]: {
3435
+ value: createStateCookie({
3436
+ redirect_to: query.redirect_to,
3437
+ token
3438
+ }),
3439
+ options: {
3440
+ httpOnly: true,
3441
+ sameSite: "lax",
3442
+ expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
3443
+ }
3444
+ }
3845
3445
  },
3846
- errors: {
3847
- ...allTree.errors,
3848
- ...tree.errors
3849
- }
3850
- };
3851
- }
3852
- } else {
3853
- tree = await this.serverOps.getTree();
3854
- }
3855
- if (tree.errors && Object.keys(tree.errors).length > 0) {
3856
- console.error("Val: Failed to get tree", JSON.stringify(tree.errors));
3857
- }
3858
- if (query.validate_sources === "true" || query.validate_binary_files === "true") {
3859
- const schemas = await this.serverOps.getSchemas();
3860
- const sourcesValidation = await this.serverOps.validateSources(schemas, tree.sources);
3861
-
3862
- // TODO: send validation errors
3863
- if (query.validate_binary_files === "true") {
3864
- await this.serverOps.validateFiles(schemas, tree.sources, sourcesValidation.files);
3865
- }
3866
- }
3867
- const schemaSha = await this.serverOps.getSchemaSha();
3868
- const modules = {};
3869
- for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
3870
- const moduleFilePath = moduleFilePathS;
3871
- if (moduleFilePath.startsWith(treePath)) {
3872
- modules[moduleFilePath] = {
3873
- source: module,
3874
- patches: patchAnalysis ? {
3875
- applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
3876
- } : undefined
3446
+ status: 302,
3447
+ redirectTo: appAuthorizeUrl
3877
3448
  };
3878
3449
  }
3879
- }
3880
- return {
3881
- status: 200,
3882
- json: {
3883
- schemaSha,
3884
- modules,
3885
- newPatchId
3450
+ },
3451
+ "/callback": {
3452
+ GET: async req => {
3453
+ const cookies = req.cookies;
3454
+ const query = req.query;
3455
+ if (!options.project) {
3456
+ return {
3457
+ status: 302,
3458
+ cookies: {
3459
+ [internal.VAL_STATE_COOKIE]: {
3460
+ value: null
3461
+ }
3462
+ },
3463
+ redirectTo: getAppErrorUrl("Project is not set")
3464
+ };
3465
+ }
3466
+ if (!options.valSecret) {
3467
+ return {
3468
+ status: 302,
3469
+ cookies: {
3470
+ [internal.VAL_STATE_COOKIE]: {
3471
+ value: null
3472
+ }
3473
+ },
3474
+ redirectTo: getAppErrorUrl("Secret is not set")
3475
+ };
3476
+ }
3477
+ const {
3478
+ success: callbackReqSuccess,
3479
+ error: callbackReqError
3480
+ } = verifyCallbackReq(cookies[internal.VAL_STATE_COOKIE], query);
3481
+ if (callbackReqError !== null) {
3482
+ return {
3483
+ status: 302,
3484
+ cookies: {
3485
+ [internal.VAL_STATE_COOKIE]: {
3486
+ value: null
3487
+ }
3488
+ },
3489
+ redirectTo: getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`)
3490
+ };
3491
+ }
3492
+ const data = await consumeCode(callbackReqSuccess.code);
3493
+ if (data === null) {
3494
+ return {
3495
+ status: 302,
3496
+ cookies: {
3497
+ [internal.VAL_STATE_COOKIE]: {
3498
+ value: null
3499
+ }
3500
+ },
3501
+ redirectTo: getAppErrorUrl("Failed to exchange code for user")
3502
+ };
3503
+ }
3504
+ const exp = getExpire();
3505
+ const valSecret = options.valSecret;
3506
+ if (!valSecret) {
3507
+ return {
3508
+ status: 302,
3509
+ cookies: {
3510
+ [internal.VAL_STATE_COOKIE]: {
3511
+ value: null
3512
+ }
3513
+ },
3514
+ redirectTo: getAppErrorUrl("Setup is not correct: secret is missing")
3515
+ };
3516
+ }
3517
+ const cookie = encodeJwt({
3518
+ ...data,
3519
+ exp // this is the client side exp
3520
+ }, valSecret);
3521
+ return {
3522
+ status: 302,
3523
+ cookies: {
3524
+ [internal.VAL_STATE_COOKIE]: {
3525
+ value: null
3526
+ },
3527
+ [internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
3528
+ [internal.VAL_SESSION_COOKIE]: {
3529
+ value: cookie,
3530
+ options: {
3531
+ httpOnly: true,
3532
+ sameSite: "strict",
3533
+ path: "/",
3534
+ secure: true,
3535
+ expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
3536
+ }
3537
+ }
3538
+ },
3539
+ redirectTo: callbackReqSuccess.redirect_uri || "/"
3540
+ };
3886
3541
  }
3887
- };
3888
- }
3889
- async postSave(body, cookies) {
3890
- const auth = this.getAuth(cookies);
3891
- if (auth.error) {
3892
- return {
3893
- status: 401,
3894
- json: {
3895
- message: auth.error
3542
+ },
3543
+ "/session": {
3544
+ GET: async req => {
3545
+ const cookies = req.cookies;
3546
+ if (serverOps instanceof ValOpsFS) {
3547
+ return {
3548
+ status: 200,
3549
+ json: {
3550
+ mode: "local",
3551
+ enabled: await callbacks.isEnabled()
3552
+ }
3553
+ };
3896
3554
  }
3897
- };
3898
- }
3899
- const PostSaveBody = z.z.object({
3900
- patchIds: z.z.array(z.z.string().refine(id => true // TODO:
3901
- ))
3902
- });
3903
- const bodyRes = PostSaveBody.safeParse(body);
3904
- if (!bodyRes.success) {
3905
- return {
3906
- status: 400,
3907
- json: {
3908
- message: "Invalid body: " + zodValidationError.fromError(bodyRes.error).toString(),
3909
- details: bodyRes.error.errors
3555
+ if (!options.project) {
3556
+ return {
3557
+ status: 500,
3558
+ json: {
3559
+ message: "Project is not set"
3560
+ }
3561
+ };
3910
3562
  }
3911
- };
3912
- }
3913
- const {
3914
- patchIds
3915
- } = bodyRes.data;
3916
- const patches = await this.serverOps.fetchPatches({
3917
- patchIds,
3918
- omitPatch: false
3919
- });
3920
- const analysis = this.serverOps.analyzePatches(patches.patches);
3921
- const preparedCommit = await this.serverOps.prepare({
3922
- ...analysis,
3923
- ...patches
3924
- });
3925
- if (preparedCommit.hasErrors) {
3926
- console.error("Failed to create commit", {
3927
- sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
3928
- binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
3929
- });
3930
- return {
3931
- status: 400,
3932
- json: {
3933
- message: "Failed to create commit",
3934
- details: {
3935
- sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
3936
- binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
3563
+ if (!options.valSecret) {
3564
+ return {
3565
+ status: 500,
3566
+ json: {
3567
+ message: "Secret is not set"
3568
+ }
3569
+ };
3570
+ }
3571
+ return withAuth(options.valSecret, cookies, "session", async data => {
3572
+ if (!options.valBuildUrl) {
3573
+ return {
3574
+ status: 500,
3575
+ json: {
3576
+ message: "Val is not correctly setup. Build url is missing"
3577
+ }
3578
+ };
3579
+ }
3580
+ const url = new URL(`/api/val/${options.project}/auth/session`, options.valBuildUrl);
3581
+ const fetchRes = await fetch(url, {
3582
+ headers: getAuthHeaders(data.token, "application/json")
3583
+ });
3584
+ if (fetchRes.status === 200) {
3585
+ return {
3586
+ status: fetchRes.status,
3587
+ json: {
3588
+ mode: "proxy",
3589
+ enabled: await callbacks.isEnabled(),
3590
+ ...(await fetchRes.json())
3591
+ }
3592
+ };
3593
+ } else {
3594
+ return {
3595
+ status: fetchRes.status,
3596
+ json: {
3597
+ message: "Failed to authorize",
3598
+ ...(await fetchRes.json())
3599
+ }
3600
+ };
3601
+ }
3602
+ });
3603
+ }
3604
+ },
3605
+ "/logout": {
3606
+ GET: async () => {
3607
+ return {
3608
+ status: 200,
3609
+ cookies: {
3610
+ [internal.VAL_SESSION_COOKIE]: {
3611
+ value: null
3612
+ },
3613
+ [internal.VAL_STATE_COOKIE]: {
3614
+ value: null
3615
+ }
3937
3616
  }
3617
+ };
3618
+ }
3619
+ },
3620
+ //#region patches
3621
+ "/patches/~": {
3622
+ GET: async req => {
3623
+ const query = req.query;
3624
+ const cookies = req.cookies;
3625
+ const auth = getAuth(cookies);
3626
+ if (auth.error) {
3627
+ return {
3628
+ status: 401,
3629
+ json: {
3630
+ message: auth.error
3631
+ }
3632
+ };
3633
+ }
3634
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
3635
+ return {
3636
+ status: 401,
3637
+ json: {
3638
+ message: "Unauthorized"
3639
+ }
3640
+ };
3641
+ }
3642
+ const authors = query.author;
3643
+ const patches = await serverOps.fetchPatches({
3644
+ authors,
3645
+ patchIds: query.patch_id,
3646
+ omitPatch: query.omit_patch === true,
3647
+ moduleFilePaths: query.module_file_path
3648
+ });
3649
+ if (patches.error) {
3650
+ // Error is singular
3651
+ console.error("Val: Failed to get patches", patches.errors);
3652
+ return {
3653
+ status: 500,
3654
+ json: {
3655
+ message: patches.error.message,
3656
+ details: patches.error
3657
+ }
3658
+ };
3659
+ }
3660
+ if (patches.errors && Object.keys(patches.errors).length > 0) {
3661
+ // Errors is plural. Different property than above.
3662
+ console.error("Val: Failed to get patches", patches.errors);
3663
+ return {
3664
+ status: 500,
3665
+ json: {
3666
+ message: "Failed to get patches",
3667
+ details: patches.errors
3668
+ }
3669
+ };
3670
+ }
3671
+ return {
3672
+ status: 200,
3673
+ json: patches
3674
+ };
3675
+ },
3676
+ DELETE: async req => {
3677
+ const query = req.query;
3678
+ const cookies = req.cookies;
3679
+ const auth = getAuth(cookies);
3680
+ if (auth.error) {
3681
+ return {
3682
+ status: 401,
3683
+ json: {
3684
+ message: auth.error
3685
+ }
3686
+ };
3687
+ }
3688
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
3689
+ return {
3690
+ status: 401,
3691
+ json: {
3692
+ message: "Unauthorized"
3693
+ }
3694
+ };
3695
+ }
3696
+ const ids = query.id;
3697
+ const deleteRes = await serverOps.deletePatches(ids);
3698
+ if (deleteRes.errors && Object.keys(deleteRes.errors).length > 0) {
3699
+ console.error("Val: Failed to delete patches", deleteRes.errors);
3700
+ return {
3701
+ status: 500,
3702
+ json: {
3703
+ message: "Failed to delete patches",
3704
+ details: deleteRes.errors
3705
+ }
3706
+ };
3938
3707
  }
3939
- };
3940
- }
3941
- if (this.serverOps instanceof ValOpsFS) {
3942
- await this.serverOps.saveFiles(preparedCommit);
3943
- return {
3944
- status: 200,
3945
- json: {} // TODO:
3946
- };
3947
- } else if (this.serverOps instanceof ValOpsHttp) {
3948
- if (auth.error === undefined && auth.id) {
3949
- await this.serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
3950
3708
  return {
3951
3709
  status: 200,
3952
- json: {} // TODO:
3710
+ json: ids
3953
3711
  };
3954
3712
  }
3955
- return {
3956
- status: 401,
3957
- json: {
3958
- message: "Unauthorized"
3713
+ },
3714
+ //#region tree ops
3715
+ "/schema": {
3716
+ GET: async req => {
3717
+ const cookies = req.cookies;
3718
+ const auth = getAuth(cookies);
3719
+ if (auth.error) {
3720
+ return {
3721
+ status: 401,
3722
+ json: {
3723
+ message: auth.error
3724
+ }
3725
+ };
3959
3726
  }
3960
- };
3961
- } else {
3962
- throw new Error("Invalid server ops");
3963
- }
3964
- }
3727
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
3728
+ return {
3729
+ status: 401,
3730
+ json: {
3731
+ message: "Unauthorized"
3732
+ }
3733
+ };
3734
+ }
3735
+ const moduleErrors = await serverOps.getModuleErrors();
3736
+ if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
3737
+ console.error("Val: Module errors", moduleErrors);
3738
+ return {
3739
+ status: 500,
3740
+ json: {
3741
+ message: "Val is not correctly setup. Check the val.modules file",
3742
+ details: moduleErrors
3743
+ }
3744
+ };
3745
+ }
3746
+ const schemaSha = await serverOps.getSchemaSha();
3747
+ const schemas = await serverOps.getSchemas();
3748
+ const serializedSchemas = {};
3749
+ for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
3750
+ const moduleFilePath = moduleFilePathS;
3751
+ serializedSchemas[moduleFilePath] = schema.serialize();
3752
+ }
3753
+ return {
3754
+ status: 200,
3755
+ json: {
3756
+ schemaSha,
3757
+ schemas: serializedSchemas
3758
+ }
3759
+ };
3760
+ }
3761
+ },
3762
+ "/tree/~": {
3763
+ PUT: async req => {
3764
+ var _body$patchIds;
3765
+ const query = req.query;
3766
+ const cookies = req.cookies;
3767
+ const body = req.body;
3768
+ const treePath = req.path || "";
3769
+ const auth = getAuth(cookies);
3770
+ if (auth.error) {
3771
+ return {
3772
+ status: 401,
3773
+ json: {
3774
+ message: auth.error
3775
+ }
3776
+ };
3777
+ }
3778
+ if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
3779
+ return {
3780
+ status: 401,
3781
+ json: {
3782
+ message: "Unauthorized"
3783
+ }
3784
+ };
3785
+ }
3786
+ const moduleErrors = await serverOps.getModuleErrors();
3787
+ if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
3788
+ console.error("Val: Module errors", moduleErrors);
3789
+ return {
3790
+ status: 500,
3791
+ json: {
3792
+ message: "Val is not correctly setup. Check the val.modules file",
3793
+ details: moduleErrors
3794
+ }
3795
+ };
3796
+ }
3797
+ let tree;
3798
+ let patchAnalysis = null;
3799
+ let newPatchId = undefined;
3800
+ 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) {
3801
+ // TODO: validate patches_sha
3802
+ const patchIds = body === null || body === void 0 ? void 0 : body.patchIds;
3803
+ const patchOps = patchIds && patchIds.length > 0 ? await serverOps.fetchPatches({
3804
+ patchIds,
3805
+ omitPatch: false
3806
+ }) : {
3807
+ patches: {}
3808
+ };
3809
+ let patchErrors = undefined;
3810
+ for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
3811
+ const patchId = patchIdS;
3812
+ if (!patchErrors) {
3813
+ patchErrors = {};
3814
+ }
3815
+ patchErrors[patchId] = {
3816
+ message: error.message
3817
+ };
3818
+ }
3819
+ if (body !== null && body !== void 0 && body.addPatch) {
3820
+ const newPatchModuleFilePath = body.addPatch.path;
3821
+ const newPatchOps = body.addPatch.patch;
3822
+ const authorId = "id" in auth ? auth.id : null;
3823
+ const createPatchRes = await serverOps.createPatch(newPatchModuleFilePath, newPatchOps, authorId);
3824
+ if (createPatchRes.error) {
3825
+ return {
3826
+ status: 500,
3827
+ json: {
3828
+ message: "Failed to create patch: " + createPatchRes.error.message,
3829
+ details: createPatchRes.error
3830
+ }
3831
+ };
3832
+ }
3833
+ // TODO: evaluate if we need this: seems wrong to delete patches that are not applied
3834
+ // for (const fileRes of createPatchRes.files) {
3835
+ // if (fileRes.error) {
3836
+ // // clean up broken patch:
3837
+ // await this.serverOps.deletePatches([createPatchRes.patchId]);
3838
+ // return {
3839
+ // status: 500,
3840
+ // json: {
3841
+ // message: "Failed to create patch",
3842
+ // details: fileRes.error,
3843
+ // },
3844
+ // };
3845
+ // }
3846
+ // }
3847
+ newPatchId = createPatchRes.patchId;
3848
+ patchOps.patches[createPatchRes.patchId] = {
3849
+ path: newPatchModuleFilePath,
3850
+ patch: newPatchOps,
3851
+ authorId,
3852
+ createdAt: createPatchRes.createdAt,
3853
+ appliedAt: null
3854
+ };
3855
+ }
3856
+ // TODO: errors
3857
+ patchAnalysis = serverOps.analyzePatches(patchOps.patches);
3858
+ tree = {
3859
+ ...(await serverOps.getTree({
3860
+ ...patchAnalysis,
3861
+ ...patchOps
3862
+ }))
3863
+ };
3864
+ if (query.validate_all) {
3865
+ const allTree = await serverOps.getTree();
3866
+ tree = {
3867
+ sources: {
3868
+ ...allTree.sources,
3869
+ ...tree.sources
3870
+ },
3871
+ errors: {
3872
+ ...allTree.errors,
3873
+ ...tree.errors
3874
+ }
3875
+ };
3876
+ }
3877
+ } else {
3878
+ tree = await serverOps.getTree();
3879
+ }
3880
+ if (tree.errors && Object.keys(tree.errors).length > 0) {
3881
+ console.error("Val: Failed to get tree", JSON.stringify(tree.errors));
3882
+ const res = {
3883
+ status: 400,
3884
+ json: {
3885
+ type: "patch-error",
3886
+ errors: Object.fromEntries(Object.entries(tree.errors).map(([key, value]) => [key, value.map(error => ({
3887
+ patchId: error.patchId,
3888
+ skipped: error.skipped,
3889
+ error: {
3890
+ message: error.error.message
3891
+ }
3892
+ }))])),
3893
+ message: "One or more patches failed to be applied"
3894
+ }
3895
+ };
3896
+ return res;
3897
+ }
3898
+ if (query.validate_sources || query.validate_binary_files) {
3899
+ const schemas = await serverOps.getSchemas();
3900
+ const sourcesValidation = await serverOps.validateSources(schemas, tree.sources);
3965
3901
 
3966
- //#region files
3967
- async getFiles(filePath, query) {
3968
- // NOTE: no auth here since you would need the patch_id to get something that is not published.
3969
- // For everything that is published, well they are already public so no auth required there...
3970
- // We could imagine adding auth just to be a 200% certain,
3971
- // 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, ...).
3972
- // So: 1) patch ids are not possible to guess (but possible to brute force)
3973
- // 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)
3974
- // 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
3975
- // Thus: attack surface + ease of attack + benefit = low probability of attack
3976
- // If we couldn't argue that patch ids are secret enough, then this would be a problem.
3977
- let fileBuffer;
3978
- if (query.patch_id) {
3979
- fileBuffer = await this.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
3980
- } else {
3981
- fileBuffer = await this.serverOps.getBinaryFile(filePath);
3982
- }
3983
- if (fileBuffer) {
3984
- return {
3985
- status: 200,
3986
- body: bufferToReadableStream(fileBuffer)
3987
- };
3988
- } else {
3989
- return {
3990
- status: 404,
3991
- json: {
3992
- message: "File not found"
3902
+ // TODO: send validation errors
3903
+ if (query.validate_binary_files) {
3904
+ await serverOps.validateFiles(schemas, tree.sources, sourcesValidation.files);
3905
+ }
3993
3906
  }
3994
- };
3907
+ const schemaSha = await serverOps.getSchemaSha();
3908
+ const modules = {};
3909
+ for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
3910
+ const moduleFilePath = moduleFilePathS;
3911
+ if (moduleFilePath.startsWith(treePath)) {
3912
+ modules[moduleFilePath] = {
3913
+ source: module,
3914
+ patches: patchAnalysis && patchAnalysis.patchesByModule[moduleFilePath] ? {
3915
+ applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
3916
+ } : undefined
3917
+ };
3918
+ }
3919
+ }
3920
+ const res = {
3921
+ status: 200,
3922
+ json: {
3923
+ schemaSha,
3924
+ modules,
3925
+ newPatchId
3926
+ }
3927
+ };
3928
+ return res;
3929
+ }
3930
+ },
3931
+ "/save": {
3932
+ POST: async req => {
3933
+ const cookies = req.cookies;
3934
+ const body = req.body;
3935
+ const auth = getAuth(cookies);
3936
+ if (auth.error) {
3937
+ return {
3938
+ status: 401,
3939
+ json: {
3940
+ message: auth.error
3941
+ }
3942
+ };
3943
+ }
3944
+ const PostSaveBody = z.z.object({
3945
+ patchIds: z.z.array(z.z.string().refine(id => true // TODO:
3946
+ ))
3947
+ });
3948
+ const bodyRes = PostSaveBody.safeParse(body);
3949
+ if (!bodyRes.success) {
3950
+ return {
3951
+ status: 400,
3952
+ json: {
3953
+ message: "Invalid body: " + zodValidationError.fromError(bodyRes.error).toString(),
3954
+ details: bodyRes.error.errors
3955
+ }
3956
+ };
3957
+ }
3958
+ const {
3959
+ patchIds
3960
+ } = bodyRes.data;
3961
+ const patches = await serverOps.fetchPatches({
3962
+ patchIds,
3963
+ omitPatch: false
3964
+ });
3965
+ const analysis = serverOps.analyzePatches(patches.patches);
3966
+ const preparedCommit = await serverOps.prepare({
3967
+ ...analysis,
3968
+ ...patches
3969
+ });
3970
+ if (preparedCommit.hasErrors) {
3971
+ console.error("Failed to create commit", {
3972
+ sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
3973
+ binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
3974
+ });
3975
+ return {
3976
+ status: 400,
3977
+ json: {
3978
+ message: "Failed to create commit",
3979
+ details: {
3980
+ sourceFilePatchErrors: Object.fromEntries(Object.entries(preparedCommit.sourceFilePatchErrors).map(([key, errors]) => [key, errors.map(e => ({
3981
+ message: formatPatchSourceError(e)
3982
+ }))])),
3983
+ binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
3984
+ }
3985
+ }
3986
+ };
3987
+ }
3988
+ if (serverOps instanceof ValOpsFS) {
3989
+ await serverOps.saveFiles(preparedCommit);
3990
+ return {
3991
+ status: 200,
3992
+ json: {} // TODO:
3993
+ };
3994
+ } else if (serverOps instanceof ValOpsHttp) {
3995
+ if (auth.error === undefined && auth.id) {
3996
+ await serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
3997
+ return {
3998
+ status: 200,
3999
+ json: {} // TODO:
4000
+ };
4001
+ }
4002
+ return {
4003
+ status: 401,
4004
+ json: {
4005
+ message: "Unauthorized"
4006
+ }
4007
+ };
4008
+ } else {
4009
+ throw new Error("Invalid server ops");
4010
+ }
4011
+ }
4012
+ },
4013
+ //#region files
4014
+ "/files": {
4015
+ GET: async req => {
4016
+ const query = req.query;
4017
+ const filePath = req.path;
4018
+ // NOTE: no auth here since you would need the patch_id to get something that is not published.
4019
+ // For everything that is published, well they are already public so no auth required there...
4020
+ // We could imagine adding auth just to be a 200% certain,
4021
+ // 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, ...).
4022
+ // So: 1) patch ids are not possible to guess (but possible to brute force)
4023
+ // 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)
4024
+ // 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
4025
+ // Thus: attack surface + ease of attack + benefit = low probability of attack
4026
+ // If we couldn't argue that patch ids are secret enough, then this would be a problem.
4027
+ let fileBuffer;
4028
+ if (query.patch_id) {
4029
+ fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
4030
+ } else {
4031
+ fileBuffer = await serverOps.getBinaryFile(filePath);
4032
+ }
4033
+ if (fileBuffer) {
4034
+ return {
4035
+ status: 200,
4036
+ body: bufferToReadableStream(fileBuffer)
4037
+ };
4038
+ } else {
4039
+ return {
4040
+ status: 404,
4041
+ json: {
4042
+ message: "File not found"
4043
+ }
4044
+ };
4045
+ }
4046
+ }
3995
4047
  }
4048
+ };
4049
+ };
4050
+ function formatPatchSourceError(error) {
4051
+ if ("message" in error) {
4052
+ return error.message;
4053
+ } else if (Array.isArray(error)) {
4054
+ return error.map(formatPatchSourceError).join("\n");
4055
+ } else {
4056
+ const _exhaustiveCheck = error;
4057
+ return "Unknown patch source error: " + JSON.stringify(_exhaustiveCheck);
3996
4058
  }
3997
4059
  }
3998
4060
  function verifyCallbackReq(stateCookie, queryParams) {
@@ -4130,7 +4192,7 @@ async function withAuth(secret, cookies, errorMessageType, handler) {
4130
4192
  status: 401,
4131
4193
  json: {
4132
4194
  message: "Session invalid or, most likely, expired. You will need to login again.",
4133
- details: verification.error
4195
+ details: zodValidationError.fromError(verification.error).toString()
4134
4196
  }
4135
4197
  };
4136
4198
  }
@@ -4292,7 +4354,7 @@ function guessMimeTypeFromPath(filePath) {
4292
4354
 
4293
4355
  async function createValServer(valModules, route, opts, callbacks, formatter) {
4294
4356
  const valServerConfig = await initHandlerOptions(route, opts);
4295
- return new ValServer(valModules, {
4357
+ return ValServer(valModules, {
4296
4358
  formatter,
4297
4359
  ...valServerConfig
4298
4360
  }, callbacks);
@@ -4417,17 +4479,9 @@ async function readCommit(gitDir, branchName) {
4417
4479
  return undefined;
4418
4480
  }
4419
4481
  }
4420
- const {
4421
- VAL_SESSION_COOKIE,
4422
- VAL_STATE_COOKIE
4423
- } = core.Internal;
4424
- const TREE_PATH_PREFIX = "/tree/~";
4425
- const PATCHES_PATH_PREFIX = "/patches/~";
4426
- const FILES_PATH_PREFIX = "/files";
4427
4482
  function createValApiRouter(route, valServerPromise, convert) {
4428
4483
  const uiRequestHandler = server.createUIRequestHandler();
4429
4484
  return async req => {
4430
- var _req$method;
4431
4485
  const valServer = await valServerPromise;
4432
4486
  const url = new URL(req.url);
4433
4487
  if (!url.pathname.startsWith(route)) {
@@ -4441,115 +4495,213 @@ function createValApiRouter(route, valServerPromise, convert) {
4441
4495
  json: error
4442
4496
  });
4443
4497
  }
4444
- const method = (_req$method = req.method) === null || _req$method === void 0 ? void 0 : _req$method.toUpperCase();
4445
- function withTreePath(path, prefix) {
4446
- return async useTreePath => {
4447
- const pathIndex = path.indexOf("~");
4448
- if (path.startsWith(prefix) && pathIndex !== -1) {
4449
- return useTreePath(path.slice(pathIndex + 1));
4450
- } else {
4451
- if (prefix.indexOf("/~") === -1) {
4452
- return convert({
4453
- status: 500,
4454
- json: {
4455
- message: `Route is incorrectly formed: ${prefix}!`
4498
+ const path = url.pathname.slice(route.length);
4499
+ const groupQueryParams = arr => {
4500
+ const map = {};
4501
+ for (const [key, value] of arr) {
4502
+ const list = map[key] || [];
4503
+ list.push(value);
4504
+ map[key] = list;
4505
+ }
4506
+ return map;
4507
+ };
4508
+ async function getValServerResponse(reqApiRoutePath, req) {
4509
+ var _req$method, _anyApi$route, _anyValServer$route;
4510
+ const anyApi =
4511
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4512
+ internal.Api;
4513
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4514
+ const anyValServer = valServer;
4515
+ const method = (_req$method = req.method) === null || _req$method === void 0 ? void 0 : _req$method.toUpperCase();
4516
+ let route = null;
4517
+ let path = undefined;
4518
+ for (const routeDef of Object.keys(internal.Api)) {
4519
+ if (routeDef === reqApiRoutePath) {
4520
+ route = routeDef;
4521
+ break;
4522
+ }
4523
+ if (reqApiRoutePath.startsWith(routeDef)) {
4524
+ var _anyApi$routeDef;
4525
+ 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;
4526
+ if (reqDefinition) {
4527
+ route = routeDef;
4528
+ if (reqDefinition.path) {
4529
+ const subPath = reqApiRoutePath.slice(routeDef.length);
4530
+ const pathRes = reqDefinition.path.safeParse(subPath);
4531
+ if (!pathRes.success) {
4532
+ return zodErrorResult(pathRes.error, `invalid path: '${subPath}' endpoint: '${routeDef}'`);
4533
+ } else {
4534
+ path = pathRes.data;
4456
4535
  }
4457
- });
4536
+ }
4537
+ break;
4458
4538
  }
4459
- return convert({
4460
- status: 404,
4539
+ }
4540
+ }
4541
+ if (!route) {
4542
+ return {
4543
+ status: 404,
4544
+ json: {
4545
+ message: "Route not found. Valid routes are: " + Object.keys(internal.Api),
4546
+ details: {
4547
+ route,
4548
+ method
4549
+ }
4550
+ }
4551
+ };
4552
+ }
4553
+ const apiEndpoint = anyApi === null || anyApi === void 0 || (_anyApi$route = anyApi[route]) === null || _anyApi$route === void 0 ? void 0 : _anyApi$route[method];
4554
+ const reqDefinition = apiEndpoint === null || apiEndpoint === void 0 ? void 0 : apiEndpoint.req;
4555
+ if (!reqDefinition) {
4556
+ return {
4557
+ status: 404,
4558
+ json: {
4559
+ message: `Requested method ${method} on route ${route} is not valid. Valid methods are: ${Object.keys(anyApi[route]).join(", ")}`,
4560
+ details: {
4561
+ route,
4562
+ method
4563
+ }
4564
+ }
4565
+ };
4566
+ }
4567
+ const endpointImpl = anyValServer === null || anyValServer === void 0 || (_anyValServer$route = anyValServer[route]) === null || _anyValServer$route === void 0 ? void 0 : _anyValServer$route[method];
4568
+ if (!endpointImpl) {
4569
+ return {
4570
+ status: 500,
4571
+ json: {
4572
+ message: "Missing server implementation of route with method. This might be caused by a mismatch between Val package versions.",
4573
+ details: {
4574
+ valid: {
4575
+ route: {
4576
+ server: Object.keys(anyValServer || {}),
4577
+ api: Object.keys(anyApi || {})
4578
+ },
4579
+ method: {
4580
+ server: Object.keys((anyValServer === null || anyValServer === void 0 ? void 0 : anyValServer[route]) || {}),
4581
+ api: Object.keys((anyApi === null || anyApi === void 0 ? void 0 : anyApi[route]) || {})
4582
+ }
4583
+ },
4584
+ route,
4585
+ method
4586
+ }
4587
+ }
4588
+ };
4589
+ }
4590
+ const bodyRes = reqDefinition.body ? reqDefinition.body.safeParse(await req.json()) : {
4591
+ success: true,
4592
+ data: {}
4593
+ };
4594
+ if (!bodyRes.success) {
4595
+ return zodErrorResult(bodyRes.error, "invalid body data");
4596
+ }
4597
+ const cookiesRes = reqDefinition.cookies ? getCookies(req, reqDefinition.cookies) : {
4598
+ success: true,
4599
+ data: {}
4600
+ };
4601
+ if (!cookiesRes.success) {
4602
+ return zodErrorResult(cookiesRes.error, "invalid cookies");
4603
+ }
4604
+ const actualQueryParams = groupQueryParams(Array.from(url.searchParams.entries()));
4605
+ let query = {};
4606
+ if (reqDefinition.query) {
4607
+ // This is code is particularly heavy, however
4608
+ // @see ValidQueryParamTypes in ApiRouter.ts where we explain what we want to support
4609
+ // We prioritized a declarative ApiRouter, so this code is what we ended up with for better of worse
4610
+ const queryRules = {};
4611
+ for (const [key, zodRule] of Object.entries(reqDefinition.query)) {
4612
+ let innerType = zodRule;
4613
+ let isOptional = false;
4614
+ let isArray = false;
4615
+ // extract inner types:
4616
+ if (innerType instanceof z.z.ZodOptional) {
4617
+ isOptional = true;
4618
+ innerType = innerType.unwrap();
4619
+ }
4620
+ if (innerType instanceof z.z.ZodArray) {
4621
+ isArray = true;
4622
+ innerType = innerType.element;
4623
+ }
4624
+ // convert boolean to union of literals true and false so we can parse it as a string
4625
+ if (innerType instanceof z.z.ZodBoolean) {
4626
+ innerType = z.z.union([z.z.literal("true"), z.z.literal("false")]).transform(arg => Boolean(arg));
4627
+ }
4628
+ // re-build rules:
4629
+ let arrayCompatibleRule = innerType;
4630
+ arrayCompatibleRule = z.z.array(innerType); // we always want to parse an array because we group the query params by into an array
4631
+ if (isOptional) {
4632
+ arrayCompatibleRule = arrayCompatibleRule.optional();
4633
+ }
4634
+ if (!isArray) {
4635
+ arrayCompatibleRule = arrayCompatibleRule.transform(arg => arg && arg[0]);
4636
+ }
4637
+ queryRules[key] = arrayCompatibleRule;
4638
+ }
4639
+ const queryRes = z.z.object(queryRules).safeParse(actualQueryParams);
4640
+ if (!queryRes.success) {
4641
+ return zodErrorResult(queryRes.error, `invalid query params: (${JSON.stringify(actualQueryParams)})`);
4642
+ }
4643
+ query = queryRes.data;
4644
+ }
4645
+ const res = await endpointImpl({
4646
+ body: bodyRes.data,
4647
+ cookies: cookiesRes.data,
4648
+ query,
4649
+ path
4650
+ });
4651
+ const resDef = apiEndpoint.res;
4652
+ if (resDef) {
4653
+ const responseResult = resDef.safeParse(res);
4654
+ if (!responseResult.success) {
4655
+ return {
4656
+ status: 500,
4461
4657
  json: {
4462
- message: `Malformed ${prefix} path! Expected: '${prefix}'`
4658
+ message: "Could not validate response. This is likely a bug in Val server.",
4659
+ details: {
4660
+ response: res,
4661
+ errors: formatZodErrorString(responseResult.error)
4662
+ }
4463
4663
  }
4464
- });
4664
+ };
4465
4665
  }
4466
- };
4666
+ }
4667
+ return res;
4467
4668
  }
4468
- const path = url.pathname.slice(route.length);
4469
4669
  if (path.startsWith("/static")) {
4470
4670
  return convert(await uiRequestHandler(path.slice("/static".length), url.href));
4471
- } else if (path === "/session") {
4472
- return convert(await valServer.session(getCookies(req, [VAL_SESSION_COOKIE])));
4473
- } else if (path === "/authorize") {
4474
- return convert(await valServer.authorize({
4475
- redirect_to: url.searchParams.get("redirect_to") || undefined
4476
- }));
4477
- } else if (path === "/callback") {
4478
- return convert(await valServer.callback({
4479
- code: url.searchParams.get("code") || undefined,
4480
- state: url.searchParams.get("state") || undefined
4481
- }, getCookies(req, [VAL_STATE_COOKIE])));
4482
- } else if (path === "/logout") {
4483
- return convert(await valServer.logout());
4484
- } else if (path === "/enable") {
4485
- return convert(await valServer.enable({
4486
- redirect_to: url.searchParams.get("redirect_to") || undefined
4487
- }));
4488
- } else if (path === "/disable") {
4489
- return convert(await valServer.disable({
4490
- redirect_to: url.searchParams.get("redirect_to") || undefined
4491
- }));
4492
- } else if (method === "POST" && path === "/save") {
4493
- const body = await req.json();
4494
- return convert(await valServer.postSave(body, getCookies(req, [VAL_SESSION_COOKIE])));
4495
- // } else if (method === "POST" && path === "/validate") {
4496
- // const body = (await req.json()) as unknown;
4497
- // return convert(
4498
- // await valServer.postValidate(
4499
- // body,
4500
- // getCookies(req, [VAL_SESSION_COOKIE]),
4501
- // requestHeaders
4502
- // )
4503
- // );
4504
- } else if (method === "GET" && path === "/schema") {
4505
- return convert(await valServer.getSchema(getCookies(req, [VAL_SESSION_COOKIE])));
4506
- } else if (method === "PUT" && path.startsWith(TREE_PATH_PREFIX)) {
4507
- return withTreePath(path, TREE_PATH_PREFIX)(async treePath => convert(await valServer.putTree(await req.json(), treePath, {
4508
- patches_sha: url.searchParams.get("patches_sha") || undefined,
4509
- validate_all: url.searchParams.get("validate_all") || undefined,
4510
- validate_binary_files: url.searchParams.get("validate_binary_files") || undefined,
4511
- validate_sources: url.searchParams.get("validate_sources") || undefined
4512
- }, getCookies(req, [VAL_SESSION_COOKIE]))));
4513
- } else if (method === "GET" && path.startsWith(PATCHES_PATH_PREFIX)) {
4514
- return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.getPatches({
4515
- authors: url.searchParams.getAll("author"),
4516
- patchIds: url.searchParams.getAll("patch_id"),
4517
- omitPatch: url.searchParams.get("omit_patch") || undefined
4518
- }, getCookies(req, [VAL_SESSION_COOKIE]))));
4519
- } else if (method === "DELETE" && path.startsWith(PATCHES_PATH_PREFIX)) {
4520
- return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.deletePatches({
4521
- id: url.searchParams.getAll("id")
4522
- }, getCookies(req, [VAL_SESSION_COOKIE]))));
4523
- } else if (method === "GET" && path.startsWith(FILES_PATH_PREFIX)) {
4524
- const treePath = path.slice(FILES_PATH_PREFIX.length);
4525
- return convert(await valServer.getFiles(treePath, {
4526
- patch_id: url.searchParams.get("patch_id") || undefined
4527
- }));
4528
4671
  } else {
4529
- return convert({
4530
- status: 404,
4531
- json: {
4532
- message: "Not Found",
4533
- details: {
4534
- method,
4535
- path
4536
- }
4537
- }
4538
- });
4672
+ return convert(await getValServerResponse(path, req));
4673
+ }
4674
+ };
4675
+ }
4676
+ function formatZodErrorString(error) {
4677
+ const errors = zodValidationError.fromZodError(error).toString();
4678
+ return errors.length > 640 ? `${errors.slice(0, 640)}...` : errors;
4679
+ }
4680
+ function zodErrorResult(error, message) {
4681
+ return {
4682
+ status: 400,
4683
+ json: {
4684
+ message: "Bad Request: " + message,
4685
+ details: {
4686
+ errors: formatZodErrorString(error)
4687
+ }
4539
4688
  }
4540
4689
  };
4541
4690
  }
4542
4691
 
4543
4692
  // TODO: is this naive implementation is too naive?
4544
- function getCookies(req, names) {
4545
- var _req$headers$get;
4546
- return ((_req$headers$get = req.headers.get("Cookie")) === null || _req$headers$get === void 0 ? void 0 : _req$headers$get.split("; ").reduce((acc, cookie) => {
4547
- const [name, value] = cookie.split("=");
4548
- if (names.includes(name.trim())) {
4549
- acc[name.trim()] = decodeURIComponent(value.trim());
4550
- }
4551
- return acc;
4552
- }, {})) || {};
4693
+ function getCookies(req, cookiesDef) {
4694
+ var _req$headers;
4695
+ const input = {};
4696
+ 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("; ");
4697
+ for (const name of Object.keys(cookiesDef)) {
4698
+ const cookie = cookieParts === null || cookieParts === void 0 ? void 0 : cookieParts.find(cookie => cookie.startsWith(`${name}=`));
4699
+ const value = cookie ? decodeURIComponent(cookie === null || cookie === void 0 ? void 0 : cookie.split("=")[1]) : undefined;
4700
+ if (value) {
4701
+ input[name.trim()] = value;
4702
+ }
4703
+ }
4704
+ return z.z.object(cookiesDef).safeParse(input);
4553
4705
  }
4554
4706
 
4555
4707
  /**