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