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