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