@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.
- package/dist/declarations/src/ValServer.d.ts +12 -63
- package/dist/declarations/src/index.d.ts +2 -1
- package/dist/valbuild-server.cjs.dev.js +911 -759
- package/dist/valbuild-server.cjs.prod.js +911 -759
- package/dist/valbuild-server.esm.js +915 -763
- package/package.json +5 -4
- /package/dist/declarations/src/{createValApiRouter.d.ts → ValRouter.d.ts} +0 -0
@@ -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 (
|
1282
|
+
if (core.Internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
|
1283
1283
|
mimeType = possibleMimeType;
|
1284
1284
|
}
|
1285
|
-
const filenameBasedLookup =
|
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 =
|
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
|
-
|
3245
|
-
|
3246
|
-
|
3247
|
-
|
3248
|
-
|
3249
|
-
|
3250
|
-
|
3251
|
-
|
3252
|
-
|
3253
|
-
|
3254
|
-
|
3255
|
-
|
3256
|
-
|
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
|
-
|
3328
|
-
if (!
|
3329
|
-
|
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
|
-
|
3391
|
-
|
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
|
-
|
3426
|
-
|
3427
|
-
|
3428
|
-
|
3429
|
-
|
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 (!
|
3434
|
-
|
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
|
-
|
3442
|
-
|
3443
|
-
|
3444
|
-
|
3445
|
-
|
3446
|
-
|
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 (!
|
3293
|
+
if (!options.valBuildUrl) {
|
3479
3294
|
throw new Error("Val build url is not set");
|
3480
3295
|
}
|
3481
|
-
const url = new URL(`/api/val/${
|
3296
|
+
const url = new URL(`/api/val/${options.project}/auth/token`, options.valBuildUrl);
|
3482
3297
|
url.searchParams.set("code", encodeURIComponent(code));
|
3483
|
-
if (!
|
3298
|
+
if (!options.apiKey) {
|
3484
3299
|
return null;
|
3485
3300
|
}
|
3486
3301
|
return fetch(url, {
|
3487
3302
|
method: "POST",
|
3488
|
-
headers: getAuthHeaders(
|
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
|
-
|
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 (!
|
3535
|
-
if (
|
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,
|
3339
|
+
const decodedToken = decodeJwt(cookie, options.valSecret);
|
3548
3340
|
if (!decodedToken) {
|
3549
|
-
if (
|
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 (
|
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 (
|
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
|
-
|
3587
|
-
|
3588
|
-
|
3589
|
-
|
3590
|
-
|
3591
|
-
|
3592
|
-
|
3593
|
-
|
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
|
-
|
3707
|
-
|
3708
|
-
|
3709
|
-
|
3710
|
-
|
3711
|
-
|
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
|
-
|
3722
|
-
|
3723
|
-
|
3724
|
-
|
3725
|
-
|
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
|
-
|
3791
|
-
|
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
|
-
|
3795
|
-
|
3796
|
-
|
3797
|
-
const
|
3798
|
-
|
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:
|
3421
|
+
status: 400,
|
3802
3422
|
json: {
|
3803
|
-
message: "
|
3804
|
-
details: createPatchRes.error
|
3423
|
+
message: "Missing redirect_to query param"
|
3805
3424
|
}
|
3806
3425
|
};
|
3807
3426
|
}
|
3808
|
-
|
3809
|
-
|
3810
|
-
|
3811
|
-
|
3812
|
-
|
3813
|
-
|
3814
|
-
|
3815
|
-
|
3816
|
-
|
3817
|
-
|
3818
|
-
|
3819
|
-
|
3820
|
-
|
3821
|
-
|
3822
|
-
|
3823
|
-
|
3824
|
-
|
3825
|
-
|
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
|
-
|
3847
|
-
|
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
|
-
|
3881
|
-
|
3882
|
-
|
3883
|
-
|
3884
|
-
|
3885
|
-
|
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
|
-
|
3890
|
-
|
3891
|
-
|
3892
|
-
|
3893
|
-
|
3894
|
-
|
3895
|
-
|
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
|
-
|
3900
|
-
|
3901
|
-
|
3902
|
-
|
3903
|
-
|
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
|
-
|
3914
|
-
|
3915
|
-
|
3916
|
-
|
3917
|
-
|
3918
|
-
|
3919
|
-
|
3920
|
-
|
3921
|
-
|
3922
|
-
|
3923
|
-
|
3924
|
-
|
3925
|
-
|
3926
|
-
|
3927
|
-
|
3928
|
-
|
3929
|
-
|
3930
|
-
|
3931
|
-
|
3932
|
-
|
3933
|
-
|
3934
|
-
|
3935
|
-
|
3936
|
-
|
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:
|
3710
|
+
json: ids
|
3953
3711
|
};
|
3954
3712
|
}
|
3955
|
-
|
3956
|
-
|
3957
|
-
|
3958
|
-
|
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
|
-
|
3962
|
-
|
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
|
-
|
3967
|
-
|
3968
|
-
|
3969
|
-
|
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
|
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
|
4445
|
-
|
4446
|
-
|
4447
|
-
|
4448
|
-
|
4449
|
-
|
4450
|
-
|
4451
|
-
|
4452
|
-
|
4453
|
-
|
4454
|
-
|
4455
|
-
|
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
|
-
|
4460
|
-
|
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:
|
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
|
-
|
4531
|
-
|
4532
|
-
|
4533
|
-
|
4534
|
-
|
4535
|
-
|
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,
|
4545
|
-
var _req$headers
|
4546
|
-
|
4547
|
-
|
4548
|
-
|
4549
|
-
|
4550
|
-
|
4551
|
-
|
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
|
/**
|