@valbuild/server 0.63.5 → 0.65.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/ValFS.d.ts +0 -1
- package/dist/declarations/src/ValFSHost.d.ts +0 -1
- package/dist/declarations/src/{createValApiRouter.d.ts → ValRouter.d.ts} +1 -1
- package/dist/declarations/src/ValServer.d.ts +15 -64
- package/dist/declarations/src/index.d.ts +2 -1
- package/dist/valbuild-server.cjs.d.ts +2 -2
- package/dist/valbuild-server.cjs.dev.js +1594 -885
- package/dist/valbuild-server.cjs.prod.js +1594 -885
- package/dist/valbuild-server.esm.js +1598 -889
- package/package.json +6 -4
- package/dist/valbuild-server.cjs.d.ts.map +0 -1
@@ -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');
|
@@ -1273,56 +1273,6 @@ function encodeJwt(payload, sessionKey) {
|
|
1273
1273
|
return `${jwtHeaderBase64}.${payloadBase64}.${crypto__default["default"].createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
|
1274
1274
|
}
|
1275
1275
|
|
1276
|
-
const textEncoder$2 = new TextEncoder();
|
1277
|
-
async function extractImageMetadata(filename, input) {
|
1278
|
-
const imageSize = sizeOf__default["default"](input);
|
1279
|
-
let mimeType = null;
|
1280
|
-
if (imageSize.type) {
|
1281
|
-
const possibleMimeType = `image/${imageSize.type}`;
|
1282
|
-
if (internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
|
1283
|
-
mimeType = possibleMimeType;
|
1284
|
-
}
|
1285
|
-
const filenameBasedLookup = internal.filenameToMimeType(filename);
|
1286
|
-
if (filenameBasedLookup) {
|
1287
|
-
mimeType = filenameBasedLookup;
|
1288
|
-
}
|
1289
|
-
}
|
1290
|
-
if (!mimeType) {
|
1291
|
-
mimeType = "application/octet-stream";
|
1292
|
-
}
|
1293
|
-
let {
|
1294
|
-
width,
|
1295
|
-
height
|
1296
|
-
} = imageSize;
|
1297
|
-
if (!width || !height) {
|
1298
|
-
width = 0;
|
1299
|
-
height = 0;
|
1300
|
-
}
|
1301
|
-
const sha256 = getSha256(mimeType, input);
|
1302
|
-
return {
|
1303
|
-
width,
|
1304
|
-
height,
|
1305
|
-
sha256,
|
1306
|
-
mimeType
|
1307
|
-
};
|
1308
|
-
}
|
1309
|
-
function getSha256(mimeType, input) {
|
1310
|
-
return core.Internal.getSHA256Hash(textEncoder$2.encode(
|
1311
|
-
// TODO: we should probably store the mimetype in the metadata and reuse it here
|
1312
|
-
`data:${mimeType};base64,${input.toString("base64")}`));
|
1313
|
-
}
|
1314
|
-
async function extractFileMetadata(filename, input) {
|
1315
|
-
let mimeType = internal.filenameToMimeType(filename);
|
1316
|
-
if (!mimeType) {
|
1317
|
-
mimeType = "application/octet-stream";
|
1318
|
-
}
|
1319
|
-
const sha256 = getSha256(mimeType, input);
|
1320
|
-
return {
|
1321
|
-
sha256,
|
1322
|
-
mimeType
|
1323
|
-
};
|
1324
|
-
}
|
1325
|
-
|
1326
1276
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
1327
1277
|
const textEncoder$1 = new TextEncoder();
|
1328
1278
|
const jsonOps = new patch.JSONOps();
|
@@ -1351,15 +1301,20 @@ class ValOps {
|
|
1351
1301
|
this.modulesErrors = null;
|
1352
1302
|
}
|
1353
1303
|
hash(input) {
|
1354
|
-
|
1355
|
-
if (typeof input === "string") {
|
1356
|
-
str = input;
|
1357
|
-
} else {
|
1358
|
-
str = JSON.stringify(input);
|
1359
|
-
}
|
1360
|
-
return core.Internal.getSHA256Hash(textEncoder$1.encode(str));
|
1304
|
+
return core.Internal.getSHA256Hash(textEncoder$1.encode(input));
|
1361
1305
|
}
|
1362
1306
|
|
1307
|
+
// #region stat
|
1308
|
+
/**
|
1309
|
+
* Get the status from Val
|
1310
|
+
*
|
1311
|
+
* This works differently in ValOpsFS and ValOpsHttp:
|
1312
|
+
* - In ValOpsFS (for dev mode) works using long-polling operations since we cannot use WebSockets in the host Next.js server and we do not want to hammer the server with requests (though we could argue that it would be ok in dev, it is not up to our standards as a kick-ass CMS).
|
1313
|
+
* - In ValOpsHttp (in production) it returns a WebSocket URL so that the client can connect directly.
|
1314
|
+
*
|
1315
|
+
* The reason we do not use long polling in production is that Vercel (a very likely host for Next.js), bills by wall time and long polling would therefore be very expensive.
|
1316
|
+
*/
|
1317
|
+
|
1363
1318
|
// #region initTree
|
1364
1319
|
async initTree() {
|
1365
1320
|
if (this.baseSha === null || this.schemaSha === null || this.sources === null || this.schemas === null || this.modulesErrors === null) {
|
@@ -1416,17 +1371,25 @@ class ValOps {
|
|
1416
1371
|
addModuleError(`source in ${path} is undefined`, moduleIdx, path);
|
1417
1372
|
return;
|
1418
1373
|
}
|
1374
|
+
let serializedSchema;
|
1375
|
+
try {
|
1376
|
+
serializedSchema = schema.serialize();
|
1377
|
+
} catch (e) {
|
1378
|
+
const message = e instanceof Error ? e.message : JSON.stringify(e);
|
1379
|
+
addModuleError(`Could not serialize module: '${path}'. Error: ${message}`, moduleIdx, path);
|
1380
|
+
return;
|
1381
|
+
}
|
1419
1382
|
const pathM = path;
|
1420
1383
|
currentSources[pathM] = source;
|
1421
1384
|
currentSchemas[pathM] = schema;
|
1422
1385
|
// make sure the checks above is enough that this does not fail - even if val modules are not set up correctly
|
1423
|
-
baseSha
|
1386
|
+
baseSha = this.hash(baseSha + JSON.stringify({
|
1424
1387
|
path,
|
1425
|
-
schema:
|
1388
|
+
schema: serializedSchema,
|
1426
1389
|
source,
|
1427
1390
|
modulesErrors: currentModulesErrors
|
1428
|
-
});
|
1429
|
-
schemaSha
|
1391
|
+
}));
|
1392
|
+
schemaSha = this.hash(schemaSha + JSON.stringify(serializedSchema));
|
1430
1393
|
});
|
1431
1394
|
}
|
1432
1395
|
this.sources = currentSources;
|
@@ -1520,10 +1483,14 @@ class ValOps {
|
|
1520
1483
|
if (!errors[path]) {
|
1521
1484
|
errors[path] = [];
|
1522
1485
|
}
|
1523
|
-
errors[path].push({
|
1486
|
+
errors[path].push(...patches.map(({
|
1487
|
+
patchId
|
1488
|
+
}) => ({
|
1489
|
+
patchId,
|
1524
1490
|
invalidPath: true,
|
1491
|
+
skipped: true,
|
1525
1492
|
error: new patch.PatchError(`Module at path: '${path}' not found`)
|
1526
|
-
});
|
1493
|
+
})));
|
1527
1494
|
}
|
1528
1495
|
patchedSources[path] = sources[path];
|
1529
1496
|
for (const {
|
@@ -1532,6 +1499,7 @@ class ValOps {
|
|
1532
1499
|
if (errors[path]) {
|
1533
1500
|
errors[path].push({
|
1534
1501
|
patchId: patchId,
|
1502
|
+
skipped: true,
|
1535
1503
|
error: new patch.PatchError(`Cannot apply patch: previous errors exists`)
|
1536
1504
|
});
|
1537
1505
|
} else {
|
@@ -1539,6 +1507,7 @@ class ValOps {
|
|
1539
1507
|
if (!patchData) {
|
1540
1508
|
errors[path] = [{
|
1541
1509
|
patchId: patchId,
|
1510
|
+
skipped: false,
|
1542
1511
|
error: new patch.PatchError(`Patch not found`)
|
1543
1512
|
}];
|
1544
1513
|
continue;
|
@@ -1569,6 +1538,7 @@ class ValOps {
|
|
1569
1538
|
}
|
1570
1539
|
errors[path].push({
|
1571
1540
|
patchId: patchId,
|
1541
|
+
skipped: false,
|
1572
1542
|
error: patchRes.error
|
1573
1543
|
});
|
1574
1544
|
} else {
|
@@ -1615,25 +1585,27 @@ class ValOps {
|
|
1615
1585
|
}
|
1616
1586
|
for (const [sourcePathS, validationErrors] of Object.entries(res)) {
|
1617
1587
|
const sourcePath = sourcePathS;
|
1618
|
-
|
1619
|
-
|
1620
|
-
if (
|
1621
|
-
|
1622
|
-
|
1623
|
-
|
1624
|
-
|
1625
|
-
|
1626
|
-
|
1627
|
-
|
1628
|
-
|
1629
|
-
errors[path]
|
1630
|
-
|
1631
|
-
|
1632
|
-
|
1633
|
-
|
1634
|
-
errors[path].validations[sourcePath]
|
1588
|
+
if (validationErrors) {
|
1589
|
+
for (const validationError of validationErrors) {
|
1590
|
+
if (isOnlyFileCheckValidationError(validationError)) {
|
1591
|
+
if (files[sourcePath]) {
|
1592
|
+
throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
|
1593
|
+
}
|
1594
|
+
const value = validationError.value;
|
1595
|
+
if (isFileSource(value)) {
|
1596
|
+
files[sourcePath] = value;
|
1597
|
+
}
|
1598
|
+
} else {
|
1599
|
+
if (!errors[path]) {
|
1600
|
+
errors[path] = {
|
1601
|
+
validations: {}
|
1602
|
+
};
|
1603
|
+
}
|
1604
|
+
if (!errors[path].validations[sourcePath]) {
|
1605
|
+
errors[path].validations[sourcePath] = [];
|
1606
|
+
}
|
1607
|
+
errors[path].validations[sourcePath].push(validationError);
|
1635
1608
|
}
|
1636
|
-
errors[path].validations[sourcePath].push(validationError);
|
1637
1609
|
}
|
1638
1610
|
}
|
1639
1611
|
}
|
@@ -1821,8 +1793,22 @@ class ValOps {
|
|
1821
1793
|
const patchRes = patch.applyPatch(tsSourceFile, tsOps, sourceFileOps);
|
1822
1794
|
if (fp.result.isErr(patchRes)) {
|
1823
1795
|
if (Array.isArray(patchRes.error)) {
|
1796
|
+
for (const error of patchRes.error) {
|
1797
|
+
console.error("Could not patch", JSON.stringify({
|
1798
|
+
path,
|
1799
|
+
patchId,
|
1800
|
+
error,
|
1801
|
+
sourceFileOps
|
1802
|
+
}, null, 2));
|
1803
|
+
}
|
1824
1804
|
errors.push(...patchRes.error);
|
1825
1805
|
} else {
|
1806
|
+
console.error("Could not patch", JSON.stringify({
|
1807
|
+
path,
|
1808
|
+
patchId,
|
1809
|
+
error: patchRes.error,
|
1810
|
+
sourceFileOps
|
1811
|
+
}, null, 2));
|
1826
1812
|
errors.push(patchRes.error);
|
1827
1813
|
}
|
1828
1814
|
triedPatches.push(patchId);
|
@@ -1916,16 +1902,23 @@ class ValOps {
|
|
1916
1902
|
}
|
1917
1903
|
|
1918
1904
|
// #region createPatch
|
1919
|
-
async createPatch(path, patch$1, authorId) {
|
1920
|
-
const
|
1921
|
-
|
1922
|
-
|
1923
|
-
|
1924
|
-
|
1905
|
+
async createPatch(path, patchAnalysis, patch$1, authorId) {
|
1906
|
+
const initTree = await this.initTree();
|
1907
|
+
const schemas = initTree.schemas;
|
1908
|
+
const moduleErrors = initTree.moduleErrors;
|
1909
|
+
let sources = initTree.sources;
|
1910
|
+
if (patchAnalysis) {
|
1911
|
+
const tree = await this.getTree(patchAnalysis);
|
1912
|
+
sources = {
|
1913
|
+
...sources,
|
1914
|
+
...tree.sources
|
1915
|
+
};
|
1916
|
+
}
|
1925
1917
|
const source = sources[path];
|
1926
1918
|
const schema = schemas[path];
|
1927
1919
|
const moduleError = moduleErrors.find(e => e.path === path);
|
1928
1920
|
if (moduleError) {
|
1921
|
+
console.error(`Cannot patch. Module at path: '${path}' has fatal errors: "${moduleError.message}"`);
|
1929
1922
|
return {
|
1930
1923
|
error: {
|
1931
1924
|
message: `Cannot patch. Module at path: '${path}' has fatal errors: ` + moduleErrors.map(m => `"${m.message}"`).join(" and ")
|
@@ -1933,6 +1926,7 @@ class ValOps {
|
|
1933
1926
|
};
|
1934
1927
|
}
|
1935
1928
|
if (!source) {
|
1929
|
+
console.error(`Cannot patch. Module source at path: '${path}' does not exist`);
|
1936
1930
|
return {
|
1937
1931
|
error: {
|
1938
1932
|
message: `Cannot patch. Module source at path: '${path}' does not exist`
|
@@ -1940,6 +1934,7 @@ class ValOps {
|
|
1940
1934
|
};
|
1941
1935
|
}
|
1942
1936
|
if (!schema) {
|
1937
|
+
console.error(`Cannot patch. Module schema at path: '${path}' does not exist`);
|
1943
1938
|
return {
|
1944
1939
|
error: {
|
1945
1940
|
message: `Cannot patch. Module schema at path: '${path}' does not exist`
|
@@ -1957,10 +1952,12 @@ class ValOps {
|
|
1957
1952
|
filePath
|
1958
1953
|
} = op;
|
1959
1954
|
if (files[filePath]) {
|
1955
|
+
console.error(`Cannot have multiple files with same path in same patch. Path: ${filePath}`);
|
1960
1956
|
files[filePath] = {
|
1961
1957
|
error: new patch.PatchError("Cannot have multiple files with same path in same patch")
|
1962
1958
|
};
|
1963
1959
|
} else if (typeof value !== "string") {
|
1960
|
+
console.error(`Value is not a string. Path: ${filePath}. Value: ${value}`);
|
1964
1961
|
files[filePath] = {
|
1965
1962
|
error: new patch.PatchError("Value is not a string")
|
1966
1963
|
};
|
@@ -1976,15 +1973,14 @@ class ValOps {
|
|
1976
1973
|
path: op.path,
|
1977
1974
|
filePath,
|
1978
1975
|
nestedFilePath: op.nestedFilePath,
|
1979
|
-
value:
|
1980
|
-
sha256
|
1981
|
-
}
|
1976
|
+
value: sha256
|
1982
1977
|
});
|
1983
1978
|
}
|
1984
1979
|
}
|
1985
1980
|
}
|
1986
1981
|
const saveRes = await this.saveSourceFilePatch(path, sourceFileOps, authorId);
|
1987
1982
|
if (saveRes.error) {
|
1983
|
+
console.error(`Could not save source file patch at path: '${path}'. Error: ${saveRes.error.message}`);
|
1988
1984
|
return {
|
1989
1985
|
error: saveRes.error
|
1990
1986
|
};
|
@@ -2008,17 +2004,20 @@ class ValOps {
|
|
2008
2004
|
? "image" : schemaAtPath instanceof core.FileSchema ? "file" : schemaAtPath.serialize().type;
|
2009
2005
|
} catch (e) {
|
2010
2006
|
if (e instanceof Error) {
|
2007
|
+
console.error(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`);
|
2011
2008
|
return {
|
2012
2009
|
filePath,
|
2013
2010
|
error: new patch.PatchError(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`)
|
2014
2011
|
};
|
2015
2012
|
}
|
2013
|
+
console.error(`Could not resolve file type at: ${modulePath}. Unknown error.`);
|
2016
2014
|
return {
|
2017
2015
|
filePath,
|
2018
2016
|
error: new patch.PatchError(`Could not resolve file type at: ${modulePath}. Unknown error.`)
|
2019
2017
|
};
|
2020
2018
|
}
|
2021
2019
|
if (type !== "image" && type !== "file") {
|
2020
|
+
console.error("Unknown file type (resolved from schema): " + type);
|
2022
2021
|
return {
|
2023
2022
|
filePath,
|
2024
2023
|
error: new patch.PatchError("Unknown file type (resolved from schema): " + type)
|
@@ -2026,6 +2025,7 @@ class ValOps {
|
|
2026
2025
|
}
|
2027
2026
|
const mimeType = getMimeTypeFromBase64(data.value);
|
2028
2027
|
if (!mimeType) {
|
2028
|
+
console.error("Could not get mimeType from base 64 encoded value");
|
2029
2029
|
return {
|
2030
2030
|
filePath,
|
2031
2031
|
error: new patch.PatchError("Could not get mimeType from base 64 encoded value. First chars were: " + data.value.slice(0, 20))
|
@@ -2033,6 +2033,7 @@ class ValOps {
|
|
2033
2033
|
}
|
2034
2034
|
const buffer = bufferFromDataUrl(data.value);
|
2035
2035
|
if (!buffer) {
|
2036
|
+
console.error("Could not create buffer from base 64 encoded value");
|
2036
2037
|
return {
|
2037
2038
|
filePath,
|
2038
2039
|
error: new patch.PatchError("Could not create buffer from base 64 encoded value")
|
@@ -2040,6 +2041,7 @@ class ValOps {
|
|
2040
2041
|
}
|
2041
2042
|
const metadataOps = createMetadataFromBuffer(type, mimeType, buffer);
|
2042
2043
|
if (metadataOps.errors) {
|
2044
|
+
console.error(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`);
|
2043
2045
|
return {
|
2044
2046
|
filePath,
|
2045
2047
|
error: new patch.PatchError(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`)
|
@@ -2061,6 +2063,14 @@ class ValOps {
|
|
2061
2063
|
};
|
2062
2064
|
}
|
2063
2065
|
}));
|
2066
|
+
const errors = saveFileRes.filter(f => !!f.error);
|
2067
|
+
if (errors.length > 0) {
|
2068
|
+
return {
|
2069
|
+
error: {
|
2070
|
+
message: "Could not save patch: " + errors.map(e => e.error.message).join(", ")
|
2071
|
+
}
|
2072
|
+
};
|
2073
|
+
}
|
2064
2074
|
return {
|
2065
2075
|
patchId,
|
2066
2076
|
files: saveFileRes,
|
@@ -2085,14 +2095,13 @@ function isFileSource(value) {
|
|
2085
2095
|
}
|
2086
2096
|
function getFieldsForType(type) {
|
2087
2097
|
if (type === "file") {
|
2088
|
-
return ["
|
2098
|
+
return ["mimeType"];
|
2089
2099
|
} else if (type === "image") {
|
2090
|
-
return ["
|
2100
|
+
return ["mimeType", "height", "width"];
|
2091
2101
|
}
|
2092
2102
|
throw new Error("Unknown type: " + type);
|
2093
2103
|
}
|
2094
2104
|
function createMetadataFromBuffer(type, mimeType, buffer) {
|
2095
|
-
const sha256 = getSha256(mimeType, buffer);
|
2096
2105
|
const errors = [];
|
2097
2106
|
let availableMetadata;
|
2098
2107
|
if (type === "image") {
|
@@ -2101,7 +2110,7 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
|
|
2101
2110
|
height,
|
2102
2111
|
type
|
2103
2112
|
} = sizeOf__default["default"](buffer);
|
2104
|
-
const normalizedType = type === "jpg" ? "jpeg" : type;
|
2113
|
+
const normalizedType = type === "jpg" ? "jpeg" : type === "svg" ? "svg+xml" : type;
|
2105
2114
|
if (type !== undefined && `image/${normalizedType}` !== mimeType) {
|
2106
2115
|
return {
|
2107
2116
|
errors: [{
|
@@ -2110,14 +2119,12 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
|
|
2110
2119
|
};
|
2111
2120
|
}
|
2112
2121
|
availableMetadata = {
|
2113
|
-
sha256: sha256,
|
2114
2122
|
mimeType,
|
2115
2123
|
height,
|
2116
2124
|
width
|
2117
2125
|
};
|
2118
2126
|
} else {
|
2119
2127
|
availableMetadata = {
|
2120
|
-
sha256: sha256,
|
2121
2128
|
mimeType
|
2122
2129
|
};
|
2123
2130
|
}
|
@@ -2237,9 +2244,7 @@ const OperationT = z__default["default"].discriminatedUnion("op", [z__default["d
|
|
2237
2244
|
path: z__default["default"].array(z__default["default"].string()),
|
2238
2245
|
filePath: z__default["default"].string(),
|
2239
2246
|
nestedFilePath: z__default["default"].array(z__default["default"].string()).optional(),
|
2240
|
-
value: z__default["default"].
|
2241
|
-
sha256: z__default["default"].string()
|
2242
|
-
})])
|
2247
|
+
value: z__default["default"].string()
|
2243
2248
|
}).strict()]);
|
2244
2249
|
const Patch = z__default["default"].array(OperationT);
|
2245
2250
|
|
@@ -2253,6 +2258,161 @@ class ValOpsFS extends ValOps {
|
|
2253
2258
|
async onInit() {
|
2254
2259
|
// do nothing
|
2255
2260
|
}
|
2261
|
+
async getStat(params) {
|
2262
|
+
// In ValOpsFS, we don't have a websocket server to listen to file changes so we use long-polling.
|
2263
|
+
// If a file that Val depends on changes, we break the connection and tell the client to request again to get the latest values.
|
2264
|
+
try {
|
2265
|
+
var _this$options, _this$options2;
|
2266
|
+
const currentBaseSha = await this.getBaseSha();
|
2267
|
+
const currentSchemaSha = await this.getSchemaSha();
|
2268
|
+
const moduleFilePaths = Object.keys(await this.getSchemas());
|
2269
|
+
const patchData = await this.readPatches();
|
2270
|
+
const patches = [];
|
2271
|
+
// TODO: use proper patch sequences when available:
|
2272
|
+
for (const [patchId] of Object.entries(patchData.patches).sort(([, a], [, b]) => {
|
2273
|
+
return a.createdAt.localeCompare(b.createdAt, undefined);
|
2274
|
+
})) {
|
2275
|
+
patches.push(patchId);
|
2276
|
+
}
|
2277
|
+
// something changed: return immediately
|
2278
|
+
const didChange = !params || currentBaseSha !== params.baseSha || currentSchemaSha !== params.schemaSha || patches.length !== params.patches.length || patches.some((p, i) => p !== params.patches[i]);
|
2279
|
+
if (didChange) {
|
2280
|
+
return {
|
2281
|
+
type: "did-change",
|
2282
|
+
baseSha: currentBaseSha,
|
2283
|
+
schemaSha: currentSchemaSha,
|
2284
|
+
patches
|
2285
|
+
};
|
2286
|
+
}
|
2287
|
+
let fsWatcher = null;
|
2288
|
+
let stopPolling = false;
|
2289
|
+
const didDirectoryChangeUsingPolling = (dir, interval, setHandle) => {
|
2290
|
+
const mtimeInDir = {};
|
2291
|
+
if (fs__default["default"].existsSync(dir)) {
|
2292
|
+
for (const file of fs__default["default"].readdirSync(dir)) {
|
2293
|
+
mtimeInDir[file] = fs__default["default"].statSync(fsPath__namespace["default"].join(dir, file)).mtime.getTime();
|
2294
|
+
}
|
2295
|
+
}
|
2296
|
+
return new Promise(resolve => {
|
2297
|
+
const go = resolve => {
|
2298
|
+
const start = Date.now();
|
2299
|
+
if (fs__default["default"].existsSync(dir)) {
|
2300
|
+
const subDirs = fs__default["default"].readdirSync(dir);
|
2301
|
+
// amount of files changed
|
2302
|
+
if (subDirs.length !== Object.keys(mtimeInDir).length) {
|
2303
|
+
resolve("request-again");
|
2304
|
+
}
|
2305
|
+
for (const file of fs__default["default"].readdirSync(dir)) {
|
2306
|
+
const mtime = fs__default["default"].statSync(fsPath__namespace["default"].join(dir, file)).mtime.getTime();
|
2307
|
+
if (mtime !== mtimeInDir[file]) {
|
2308
|
+
resolve("request-again");
|
2309
|
+
}
|
2310
|
+
}
|
2311
|
+
} else {
|
2312
|
+
// dir had files, but now is deleted
|
2313
|
+
if (Object.keys(mtimeInDir).length > 0) {
|
2314
|
+
resolve("request-again");
|
2315
|
+
}
|
2316
|
+
}
|
2317
|
+
if (Date.now() - start > interval) {
|
2318
|
+
console.warn("Val: polling interval of patches exceeded");
|
2319
|
+
}
|
2320
|
+
if (stopPolling) {
|
2321
|
+
return;
|
2322
|
+
}
|
2323
|
+
setHandle(setTimeout(() => go(resolve), interval));
|
2324
|
+
};
|
2325
|
+
setHandle(setTimeout(() => go(resolve), interval));
|
2326
|
+
});
|
2327
|
+
};
|
2328
|
+
const didFilesChangeUsingPolling = (files, interval, setHandle) => {
|
2329
|
+
const mtimes = {};
|
2330
|
+
for (const file of files) {
|
2331
|
+
if (fs__default["default"].existsSync(file)) {
|
2332
|
+
mtimes[file] = fs__default["default"].statSync(file).mtime.getTime();
|
2333
|
+
} else {
|
2334
|
+
mtimes[file] = -1;
|
2335
|
+
}
|
2336
|
+
}
|
2337
|
+
return new Promise(resolve => {
|
2338
|
+
const go = resolve => {
|
2339
|
+
const start = Date.now();
|
2340
|
+
for (const file of files) {
|
2341
|
+
const mtime = fs__default["default"].existsSync(file) ? fs__default["default"].statSync(file).mtime.getTime() : -1;
|
2342
|
+
if (mtime !== mtimes[file]) {
|
2343
|
+
resolve("request-again");
|
2344
|
+
}
|
2345
|
+
}
|
2346
|
+
if (Date.now() - start > interval) {
|
2347
|
+
console.warn("Val: polling interval of files exceeded");
|
2348
|
+
}
|
2349
|
+
setHandle(setTimeout(() => go(resolve), interval));
|
2350
|
+
};
|
2351
|
+
if (stopPolling) {
|
2352
|
+
return;
|
2353
|
+
}
|
2354
|
+
setHandle(setTimeout(() => go(resolve), interval));
|
2355
|
+
});
|
2356
|
+
};
|
2357
|
+
const statFilePollingInterval = ((_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.statFilePollingInterval) || 250; // relatively low interval, but there would typically not be that many files (less than 1000 at the very least) - hopefully if we have customers with more files than that, we also have devs working on Val that easily can fix this :) Besides this is just the default
|
2358
|
+
const disableFilePolling = ((_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.disableFilePolling) || false;
|
2359
|
+
let patchesDirHandle;
|
2360
|
+
let valFilesIntervalHandle;
|
2361
|
+
const type = await Promise.race([
|
2362
|
+
// we poll the patches directory for changes since fs.watch does not work reliably on all system (in particular on WSL) and just checking the patches dir is relatively cheap
|
2363
|
+
disableFilePolling ? new Promise(() => {}) : didDirectoryChangeUsingPolling(this.getPatchesDir(), statFilePollingInterval, handle => {
|
2364
|
+
patchesDirHandle = handle;
|
2365
|
+
}),
|
2366
|
+
// we poll the files that Val depends on for changes
|
2367
|
+
disableFilePolling ? new Promise(() => {}) : didFilesChangeUsingPolling([fsPath__namespace["default"].join(this.rootDir, "val.config.ts"), fsPath__namespace["default"].join(this.rootDir, "val.modules.ts"), fsPath__namespace["default"].join(this.rootDir, "val.config.js"), fsPath__namespace["default"].join(this.rootDir, "val.modules.js"), ...moduleFilePaths.map(p => fsPath__namespace["default"].join(this.rootDir, p))], statFilePollingInterval, handle => {
|
2368
|
+
valFilesIntervalHandle = handle;
|
2369
|
+
}), new Promise(resolve => {
|
2370
|
+
fsWatcher = fs__default["default"].watch(this.rootDir, {
|
2371
|
+
recursive: true
|
2372
|
+
}, (eventType, filename) => {
|
2373
|
+
if (!filename) {
|
2374
|
+
return;
|
2375
|
+
}
|
2376
|
+
const isChange = filename.startsWith(this.getPatchesDir().slice(this.rootDir.length + 1)) || filename.endsWith(".val.ts") || filename.endsWith(".val.js") || filename.endsWith("val.config.ts") || filename.endsWith("val.config.js") || filename.endsWith("val.modules.ts") || filename.endsWith("val.modules.js");
|
2377
|
+
if (isChange) {
|
2378
|
+
// a file that Val depends on just changed or a patch was created, break connection and request stat again to get the new values
|
2379
|
+
resolve("request-again");
|
2380
|
+
}
|
2381
|
+
});
|
2382
|
+
}), new Promise(resolve => {
|
2383
|
+
var _this$options3;
|
2384
|
+
return setTimeout(() => resolve("no-change"), ((_this$options3 = this.options) === null || _this$options3 === void 0 ? void 0 : _this$options3.statPollingInterval) || 20000);
|
2385
|
+
})]).finally(() => {
|
2386
|
+
if (fsWatcher) {
|
2387
|
+
fsWatcher.close();
|
2388
|
+
}
|
2389
|
+
stopPolling = true;
|
2390
|
+
clearInterval(patchesDirHandle);
|
2391
|
+
clearInterval(valFilesIntervalHandle);
|
2392
|
+
});
|
2393
|
+
return {
|
2394
|
+
type,
|
2395
|
+
baseSha: currentBaseSha,
|
2396
|
+
schemaSha: currentSchemaSha,
|
2397
|
+
patches
|
2398
|
+
};
|
2399
|
+
} catch (err) {
|
2400
|
+
if (err instanceof Error) {
|
2401
|
+
return {
|
2402
|
+
type: "error",
|
2403
|
+
error: {
|
2404
|
+
message: err.message
|
2405
|
+
}
|
2406
|
+
};
|
2407
|
+
}
|
2408
|
+
return {
|
2409
|
+
type: "error",
|
2410
|
+
error: {
|
2411
|
+
message: "Unknown error (getStat)"
|
2412
|
+
}
|
2413
|
+
};
|
2414
|
+
}
|
2415
|
+
}
|
2256
2416
|
async readPatches(includes) {
|
2257
2417
|
const patchesCacheDir = this.getPatchesDir();
|
2258
2418
|
let patchJsonFiles = [];
|
@@ -2261,8 +2421,8 @@ class ValOpsFS extends ValOps {
|
|
2261
2421
|
}
|
2262
2422
|
const patches = {};
|
2263
2423
|
const errors = {};
|
2264
|
-
const
|
2265
|
-
for (const patchIdNum of
|
2424
|
+
const parsedPatchIds = patchJsonFiles.map(file => parseInt(fsPath__namespace["default"].basename(fsPath__namespace["default"].dirname(file)), 10)).sort();
|
2425
|
+
for (const patchIdNum of parsedPatchIds) {
|
2266
2426
|
if (Number.isNaN(patchIdNum)) {
|
2267
2427
|
throw new Error("Could not parse patch id from file name. Files found: " + patchJsonFiles.join(", "));
|
2268
2428
|
}
|
@@ -2303,6 +2463,12 @@ class ValOpsFS extends ValOps {
|
|
2303
2463
|
errors: allErrors,
|
2304
2464
|
patches: allPatches
|
2305
2465
|
} = await this.readPatches(filters.patchIds);
|
2466
|
+
if (allErrors && Object.keys(allErrors).length > 0) {
|
2467
|
+
for (const [patchId, error] of Object.entries(allErrors)) {
|
2468
|
+
console.error("Error reading patch", patchId, error);
|
2469
|
+
errors[patchId] = error;
|
2470
|
+
}
|
2471
|
+
}
|
2306
2472
|
for (const [patchIdS, patch] of Object.entries(allPatches)) {
|
2307
2473
|
const patchId = patchIdS;
|
2308
2474
|
if (filters.authors && !(patch.authorId === null || filters.authors.includes(patch.authorId))) {
|
@@ -2318,10 +2484,6 @@ class ValOpsFS extends ValOps {
|
|
2318
2484
|
authorId: patch.authorId,
|
2319
2485
|
appliedAt: patch.appliedAt
|
2320
2486
|
};
|
2321
|
-
const error = allErrors && allErrors[patchId];
|
2322
|
-
if (error) {
|
2323
|
-
errors[patchId] = error;
|
2324
|
-
}
|
2325
2487
|
}
|
2326
2488
|
if (errors && Object.keys(errors).length > 0) {
|
2327
2489
|
return {
|
@@ -2665,11 +2827,11 @@ class ValOpsFS extends ValOps {
|
|
2665
2827
|
getPatchDir(patchId) {
|
2666
2828
|
return fsPath__namespace["default"].join(this.getPatchesDir(), patchId);
|
2667
2829
|
}
|
2668
|
-
getBinaryFilePath(
|
2669
|
-
return fsPath__namespace["default"].join(this.getPatchDir(patchId), "files",
|
2830
|
+
getBinaryFilePath(filePath, patchId) {
|
2831
|
+
return fsPath__namespace["default"].join(this.getPatchDir(patchId), "files", filePath, fsPath__namespace["default"].basename(filePath));
|
2670
2832
|
}
|
2671
|
-
getBinaryFileMetadataPath(
|
2672
|
-
return fsPath__namespace["default"].join(this.getPatchDir(patchId), "files",
|
2833
|
+
getBinaryFileMetadataPath(filePath, patchId) {
|
2834
|
+
return fsPath__namespace["default"].join(this.getPatchDir(patchId), "files", filePath, "metadata.json");
|
2673
2835
|
}
|
2674
2836
|
getPatchFilePath(patchId) {
|
2675
2837
|
return fsPath__namespace["default"].join(this.getPatchDir(patchId), "patch.json");
|
@@ -2742,12 +2904,10 @@ const BaseSha = z.z.string().refine(s => !!s); // TODO: validate
|
|
2742
2904
|
const AuthorId = z.z.string().refine(s => !!s); // TODO: validate
|
2743
2905
|
const ModuleFilePath = z.z.string().refine(s => !!s); // TODO: validate
|
2744
2906
|
const Metadata = z.z.union([z.z.object({
|
2745
|
-
sha256: z.z.string(),
|
2746
2907
|
mimeType: z.z.string(),
|
2747
2908
|
width: z.z.number(),
|
2748
2909
|
height: z.z.number()
|
2749
2910
|
}), z.z.object({
|
2750
|
-
sha256: z.z.string(),
|
2751
2911
|
mimeType: z.z.string()
|
2752
2912
|
})]);
|
2753
2913
|
const MetadataRes = z.z.object({
|
@@ -2768,7 +2928,7 @@ const BasePatchResponse = z.z.object({
|
|
2768
2928
|
});
|
2769
2929
|
const GetPatches = z.z.object({
|
2770
2930
|
patches: z.z.array(z.z.intersection(z.z.object({
|
2771
|
-
patch: Patch
|
2931
|
+
patch: Patch.optional()
|
2772
2932
|
}), BasePatchResponse)),
|
2773
2933
|
errors: z.z.array(z.z.object({
|
2774
2934
|
patchId: PatchId.optional(),
|
@@ -2819,7 +2979,9 @@ const CommitResponse = z.z.object({
|
|
2819
2979
|
branch: z.z.string()
|
2820
2980
|
});
|
2821
2981
|
class ValOpsHttp extends ValOps {
|
2822
|
-
constructor(hostUrl, project, commitSha,
|
2982
|
+
constructor(hostUrl, project, commitSha,
|
2983
|
+
// TODO: CommitSha
|
2984
|
+
branch, apiKey, valModules, options) {
|
2823
2985
|
super(valModules, options);
|
2824
2986
|
this.hostUrl = hostUrl;
|
2825
2987
|
this.project = project;
|
@@ -2833,7 +2995,150 @@ class ValOpsHttp extends ValOps {
|
|
2833
2995
|
async onInit() {
|
2834
2996
|
// TODO: unused for now. Implement or remove
|
2835
2997
|
}
|
2998
|
+
async getStat(params) {
|
2999
|
+
if (!(params !== null && params !== void 0 && params.profileId)) {
|
3000
|
+
return {
|
3001
|
+
type: "error",
|
3002
|
+
error: {
|
3003
|
+
message: "No profileId provided"
|
3004
|
+
}
|
3005
|
+
};
|
3006
|
+
}
|
3007
|
+
const currentBaseSha = await this.getBaseSha();
|
3008
|
+
const currentSchemaSha = await this.getSchemaSha();
|
3009
|
+
const patchData = await this.fetchPatches({
|
3010
|
+
omitPatch: true,
|
3011
|
+
authors: undefined,
|
3012
|
+
patchIds: undefined,
|
3013
|
+
moduleFilePaths: undefined
|
3014
|
+
});
|
3015
|
+
const patches = [];
|
3016
|
+
// TODO: use proper patch sequences when available:
|
3017
|
+
for (const [patchId] of Object.entries(patchData.patches).sort(([, a], [, b]) => {
|
3018
|
+
return a.createdAt.localeCompare(b.createdAt, undefined);
|
3019
|
+
})) {
|
3020
|
+
patches.push(patchId);
|
3021
|
+
}
|
3022
|
+
const webSocketNonceRes = await this.getWebSocketNonce(params.profileId);
|
3023
|
+
if (webSocketNonceRes.status === "error") {
|
3024
|
+
return {
|
3025
|
+
type: "error",
|
3026
|
+
error: webSocketNonceRes.error
|
3027
|
+
};
|
3028
|
+
}
|
3029
|
+
const {
|
3030
|
+
nonce,
|
3031
|
+
url
|
3032
|
+
} = webSocketNonceRes.data;
|
3033
|
+
return {
|
3034
|
+
type: "use-websocket",
|
3035
|
+
url,
|
3036
|
+
nonce,
|
3037
|
+
baseSha: currentBaseSha,
|
3038
|
+
schemaSha: currentSchemaSha,
|
3039
|
+
patches,
|
3040
|
+
commitSha: this.commitSha
|
3041
|
+
};
|
3042
|
+
}
|
3043
|
+
async getWebSocketNonce(profileId) {
|
3044
|
+
return fetch(`${this.hostUrl}/v1/${this.project}/websocket/nonces`, {
|
3045
|
+
method: "POST",
|
3046
|
+
body: JSON.stringify({
|
3047
|
+
branch: this.branch,
|
3048
|
+
profileId
|
3049
|
+
}),
|
3050
|
+
headers: {
|
3051
|
+
...this.authHeaders,
|
3052
|
+
"Content-Type": "application/json"
|
3053
|
+
}
|
3054
|
+
}).then(async res => {
|
3055
|
+
if (res.ok) {
|
3056
|
+
const json = await res.json();
|
3057
|
+
if (typeof json.nonce !== "string" || typeof json.url !== "string") {
|
3058
|
+
return {
|
3059
|
+
status: "error",
|
3060
|
+
error: {
|
3061
|
+
message: "Invalid nonce response: " + JSON.stringify(json)
|
3062
|
+
}
|
3063
|
+
};
|
3064
|
+
}
|
3065
|
+
if (!json.url.startsWith("ws://") && !json.url.startsWith("wss://")) {
|
3066
|
+
return {
|
3067
|
+
status: "error",
|
3068
|
+
error: {
|
3069
|
+
message: "Invalid websocket url: " + json.url
|
3070
|
+
}
|
3071
|
+
};
|
3072
|
+
}
|
3073
|
+
return {
|
3074
|
+
status: "success",
|
3075
|
+
data: {
|
3076
|
+
nonce: json.nonce,
|
3077
|
+
url: json.url
|
3078
|
+
}
|
3079
|
+
};
|
3080
|
+
}
|
3081
|
+
return {
|
3082
|
+
status: "error",
|
3083
|
+
error: {
|
3084
|
+
message: "Could not get nonce. HTTP error: " + res.status + " " + res.statusText
|
3085
|
+
}
|
3086
|
+
};
|
3087
|
+
}).catch(e => {
|
3088
|
+
console.error("Could not get nonce (connection error?):", e instanceof Error ? e.message : e.toString());
|
3089
|
+
return {
|
3090
|
+
status: "error",
|
3091
|
+
error: {
|
3092
|
+
message: "Could not get nonce. Error: " + (e instanceof Error ? e.message : e.toString())
|
3093
|
+
}
|
3094
|
+
};
|
3095
|
+
});
|
3096
|
+
}
|
2836
3097
|
async fetchPatches(filters) {
|
3098
|
+
// Split patchIds into chunks to avoid too long query strings
|
3099
|
+
// NOTE: fetching patches results are cached, so this should reduce the pressure on the server
|
3100
|
+
const chunkSize = 100;
|
3101
|
+
const patchIds = filters.patchIds || [];
|
3102
|
+
const patchIdChunks = [];
|
3103
|
+
for (let i = 0; i < patchIds.length; i += chunkSize) {
|
3104
|
+
patchIdChunks.push(patchIds.slice(i, i + chunkSize));
|
3105
|
+
}
|
3106
|
+
let allPatches = {};
|
3107
|
+
let allErrors = {};
|
3108
|
+
if (patchIds === undefined || patchIds.length === 0) {
|
3109
|
+
return this.fetchPatchesInternal({
|
3110
|
+
patchIds: patchIds,
|
3111
|
+
authors: filters.authors,
|
3112
|
+
moduleFilePaths: filters.moduleFilePaths,
|
3113
|
+
omitPatch: filters.omitPatch
|
3114
|
+
});
|
3115
|
+
}
|
3116
|
+
for (const res of await Promise.all(patchIdChunks.map(patchIdChunk => this.fetchPatchesInternal({
|
3117
|
+
patchIds: patchIdChunk,
|
3118
|
+
authors: filters.authors,
|
3119
|
+
moduleFilePaths: filters.moduleFilePaths,
|
3120
|
+
omitPatch: filters.omitPatch
|
3121
|
+
})))) {
|
3122
|
+
if ("error" in res) {
|
3123
|
+
return res;
|
3124
|
+
}
|
3125
|
+
allPatches = {
|
3126
|
+
...allPatches,
|
3127
|
+
...res.patches
|
3128
|
+
};
|
3129
|
+
if (res.errors) {
|
3130
|
+
allErrors = {
|
3131
|
+
...allErrors,
|
3132
|
+
...res.errors
|
3133
|
+
};
|
3134
|
+
}
|
3135
|
+
}
|
3136
|
+
return {
|
3137
|
+
patches: allPatches,
|
3138
|
+
errors: Object.keys(allErrors).length > 0 ? allErrors : undefined
|
3139
|
+
};
|
3140
|
+
}
|
3141
|
+
async fetchPatchesInternal(filters) {
|
2837
3142
|
const params = [];
|
2838
3143
|
params.push(["branch", this.branch]);
|
2839
3144
|
if (filters.patchIds) {
|
@@ -3082,7 +3387,7 @@ class ValOpsHttp extends ValOps {
|
|
3082
3387
|
if (!file) {
|
3083
3388
|
return null;
|
3084
3389
|
}
|
3085
|
-
return
|
3390
|
+
return bufferFromDataUrl(file.value) ?? null;
|
3086
3391
|
}
|
3087
3392
|
async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
|
3088
3393
|
const params = new URLSearchParams();
|
@@ -3190,6 +3495,7 @@ class ValOpsHttp extends ValOps {
|
|
3190
3495
|
}
|
3191
3496
|
async commit(prepared, message, committer, newBranch) {
|
3192
3497
|
try {
|
3498
|
+
var _res$headers$get;
|
3193
3499
|
const existingBranch = this.branch;
|
3194
3500
|
const res = await fetch(`${this.hostUrl}/v1/${this.project}/commit`, {
|
3195
3501
|
method: "POST",
|
@@ -3225,6 +3531,22 @@ class ValOpsHttp extends ValOps {
|
|
3225
3531
|
}
|
3226
3532
|
};
|
3227
3533
|
}
|
3534
|
+
if ((_res$headers$get = res.headers.get("Content-Type")) !== null && _res$headers$get !== void 0 && _res$headers$get.includes("application/json")) {
|
3535
|
+
const json = await res.json();
|
3536
|
+
if (json.isNotFastForward) {
|
3537
|
+
return {
|
3538
|
+
isNotFastForward: true,
|
3539
|
+
error: {
|
3540
|
+
message: "Could not commit. Not a fast-forward commit"
|
3541
|
+
}
|
3542
|
+
};
|
3543
|
+
}
|
3544
|
+
return {
|
3545
|
+
error: {
|
3546
|
+
message: json.message
|
3547
|
+
}
|
3548
|
+
};
|
3549
|
+
}
|
3228
3550
|
return {
|
3229
3551
|
error: {
|
3230
3552
|
message: "Could not commit. HTTP error: " + res.status + " " + res.statusText
|
@@ -3241,251 +3563,61 @@ class ValOpsHttp extends ValOps {
|
|
3241
3563
|
}
|
3242
3564
|
|
3243
3565
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
3244
|
-
|
3245
|
-
|
3246
|
-
|
3247
|
-
|
3248
|
-
|
3249
|
-
|
3250
|
-
|
3251
|
-
|
3252
|
-
|
3253
|
-
|
3254
|
-
|
3255
|
-
|
3256
|
-
|
3257
|
-
|
3258
|
-
|
3259
|
-
|
3260
|
-
throw new Error("Invalid mode: " + (options === null || options === void 0 ? void 0 : options.mode));
|
3261
|
-
}
|
3566
|
+
const ValServer = (valModules, options, callbacks) => {
|
3567
|
+
let serverOps;
|
3568
|
+
if (options.mode === "fs") {
|
3569
|
+
serverOps = new ValOpsFS(options.cwd, valModules, {
|
3570
|
+
formatter: options.formatter,
|
3571
|
+
config: options.config
|
3572
|
+
});
|
3573
|
+
} else if (options.mode === "http") {
|
3574
|
+
serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
|
3575
|
+
formatter: options.formatter,
|
3576
|
+
root: options.root,
|
3577
|
+
config: options.config
|
3578
|
+
});
|
3579
|
+
} else {
|
3580
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
3581
|
+
throw new Error("Invalid mode: " + (options === null || options === void 0 ? void 0 : options.mode));
|
3262
3582
|
}
|
3263
|
-
|
3264
|
-
|
3265
|
-
|
3266
|
-
const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
|
3267
|
-
if (typeof redirectToRes !== "string") {
|
3268
|
-
return redirectToRes;
|
3583
|
+
const getAuthorizeUrl = (publicValApiRe, token) => {
|
3584
|
+
if (!options.project) {
|
3585
|
+
throw new Error("Project is not set");
|
3269
3586
|
}
|
3270
|
-
|
3271
|
-
|
3272
|
-
cookies: {
|
3273
|
-
[internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
|
3274
|
-
},
|
3275
|
-
status: 302,
|
3276
|
-
redirectTo: redirectToRes
|
3277
|
-
};
|
3278
|
-
}
|
3279
|
-
async disable(query) {
|
3280
|
-
const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
|
3281
|
-
if (typeof redirectToRes !== "string") {
|
3282
|
-
return redirectToRes;
|
3587
|
+
if (!options.valBuildUrl) {
|
3588
|
+
throw new Error("Val build url is not set");
|
3283
3589
|
}
|
3284
|
-
|
3285
|
-
|
3286
|
-
|
3287
|
-
|
3288
|
-
|
3289
|
-
|
3290
|
-
|
3291
|
-
|
3292
|
-
redirectTo: redirectToRes
|
3293
|
-
};
|
3294
|
-
}
|
3295
|
-
async authorize(query) {
|
3296
|
-
if (typeof query.redirect_to !== "string") {
|
3297
|
-
return {
|
3298
|
-
status: 400,
|
3299
|
-
json: {
|
3300
|
-
message: "Missing redirect_to query param"
|
3301
|
-
}
|
3302
|
-
};
|
3590
|
+
const url = new URL(`/auth/${options.project}/authorize`, options.valBuildUrl);
|
3591
|
+
url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRe}/callback`));
|
3592
|
+
url.searchParams.set("state", token);
|
3593
|
+
return url.toString();
|
3594
|
+
};
|
3595
|
+
const getAppErrorUrl = error => {
|
3596
|
+
if (!options.project) {
|
3597
|
+
throw new Error("Project is not set");
|
3303
3598
|
}
|
3304
|
-
|
3305
|
-
|
3306
|
-
const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
|
3307
|
-
await this.callbacks.onEnable(true);
|
3308
|
-
return {
|
3309
|
-
cookies: {
|
3310
|
-
[internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
|
3311
|
-
[internal.VAL_STATE_COOKIE]: {
|
3312
|
-
value: createStateCookie({
|
3313
|
-
redirect_to: query.redirect_to,
|
3314
|
-
token
|
3315
|
-
}),
|
3316
|
-
options: {
|
3317
|
-
httpOnly: true,
|
3318
|
-
sameSite: "lax",
|
3319
|
-
expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
|
3320
|
-
}
|
3321
|
-
}
|
3322
|
-
},
|
3323
|
-
status: 302,
|
3324
|
-
redirectTo: appAuthorizeUrl
|
3325
|
-
};
|
3326
|
-
}
|
3327
|
-
async callback(query, cookies) {
|
3328
|
-
if (!this.options.project) {
|
3329
|
-
return {
|
3330
|
-
status: 302,
|
3331
|
-
cookies: {
|
3332
|
-
[internal.VAL_STATE_COOKIE]: {
|
3333
|
-
value: null
|
3334
|
-
}
|
3335
|
-
},
|
3336
|
-
redirectTo: this.getAppErrorUrl("Project is not set")
|
3337
|
-
};
|
3338
|
-
}
|
3339
|
-
if (!this.options.valSecret) {
|
3340
|
-
return {
|
3341
|
-
status: 302,
|
3342
|
-
cookies: {
|
3343
|
-
[internal.VAL_STATE_COOKIE]: {
|
3344
|
-
value: null
|
3345
|
-
}
|
3346
|
-
},
|
3347
|
-
redirectTo: this.getAppErrorUrl("Secret is not set")
|
3348
|
-
};
|
3349
|
-
}
|
3350
|
-
const {
|
3351
|
-
success: callbackReqSuccess,
|
3352
|
-
error: callbackReqError
|
3353
|
-
} = verifyCallbackReq(cookies[internal.VAL_STATE_COOKIE], query);
|
3354
|
-
if (callbackReqError !== null) {
|
3355
|
-
return {
|
3356
|
-
status: 302,
|
3357
|
-
cookies: {
|
3358
|
-
[internal.VAL_STATE_COOKIE]: {
|
3359
|
-
value: null
|
3360
|
-
}
|
3361
|
-
},
|
3362
|
-
redirectTo: this.getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`)
|
3363
|
-
};
|
3364
|
-
}
|
3365
|
-
const data = await this.consumeCode(callbackReqSuccess.code);
|
3366
|
-
if (data === null) {
|
3367
|
-
return {
|
3368
|
-
status: 302,
|
3369
|
-
cookies: {
|
3370
|
-
[internal.VAL_STATE_COOKIE]: {
|
3371
|
-
value: null
|
3372
|
-
}
|
3373
|
-
},
|
3374
|
-
redirectTo: this.getAppErrorUrl("Failed to exchange code for user")
|
3375
|
-
};
|
3376
|
-
}
|
3377
|
-
const exp = getExpire();
|
3378
|
-
const valSecret = this.options.valSecret;
|
3379
|
-
if (!valSecret) {
|
3380
|
-
return {
|
3381
|
-
status: 302,
|
3382
|
-
cookies: {
|
3383
|
-
[internal.VAL_STATE_COOKIE]: {
|
3384
|
-
value: null
|
3385
|
-
}
|
3386
|
-
},
|
3387
|
-
redirectTo: this.getAppErrorUrl("Setup is not correct: secret is missing")
|
3388
|
-
};
|
3389
|
-
}
|
3390
|
-
const cookie = encodeJwt({
|
3391
|
-
...data,
|
3392
|
-
exp // this is the client side exp
|
3393
|
-
}, valSecret);
|
3394
|
-
return {
|
3395
|
-
status: 302,
|
3396
|
-
cookies: {
|
3397
|
-
[internal.VAL_STATE_COOKIE]: {
|
3398
|
-
value: null
|
3399
|
-
},
|
3400
|
-
[internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
|
3401
|
-
[internal.VAL_SESSION_COOKIE]: {
|
3402
|
-
value: cookie,
|
3403
|
-
options: {
|
3404
|
-
httpOnly: true,
|
3405
|
-
sameSite: "strict",
|
3406
|
-
path: "/",
|
3407
|
-
secure: true,
|
3408
|
-
expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
|
3409
|
-
}
|
3410
|
-
}
|
3411
|
-
},
|
3412
|
-
redirectTo: callbackReqSuccess.redirect_uri || "/"
|
3413
|
-
};
|
3414
|
-
}
|
3415
|
-
async session(cookies) {
|
3416
|
-
if (this.serverOps instanceof ValOpsFS) {
|
3417
|
-
return {
|
3418
|
-
status: 200,
|
3419
|
-
json: {
|
3420
|
-
mode: "local",
|
3421
|
-
enabled: await this.callbacks.isEnabled()
|
3422
|
-
}
|
3423
|
-
};
|
3424
|
-
}
|
3425
|
-
if (!this.options.project) {
|
3426
|
-
return {
|
3427
|
-
status: 500,
|
3428
|
-
json: {
|
3429
|
-
message: "Project is not set"
|
3430
|
-
}
|
3431
|
-
};
|
3432
|
-
}
|
3433
|
-
if (!this.options.valSecret) {
|
3434
|
-
return {
|
3435
|
-
status: 500,
|
3436
|
-
json: {
|
3437
|
-
message: "Secret is not set"
|
3438
|
-
}
|
3439
|
-
};
|
3599
|
+
if (!options.valBuildUrl) {
|
3600
|
+
throw new Error("Val build url is not set");
|
3440
3601
|
}
|
3441
|
-
|
3442
|
-
|
3443
|
-
|
3444
|
-
|
3445
|
-
|
3446
|
-
|
3447
|
-
}
|
3448
|
-
};
|
3449
|
-
}
|
3450
|
-
const url = new URL(`/api/val/${this.options.project}/auth/session`, this.options.valBuildUrl);
|
3451
|
-
const fetchRes = await fetch(url, {
|
3452
|
-
headers: getAuthHeaders(data.token, "application/json")
|
3453
|
-
});
|
3454
|
-
if (fetchRes.status === 200) {
|
3455
|
-
return {
|
3456
|
-
status: fetchRes.status,
|
3457
|
-
json: {
|
3458
|
-
mode: "proxy",
|
3459
|
-
enabled: await this.callbacks.isEnabled(),
|
3460
|
-
...(await fetchRes.json())
|
3461
|
-
}
|
3462
|
-
};
|
3463
|
-
} else {
|
3464
|
-
return {
|
3465
|
-
status: fetchRes.status,
|
3466
|
-
json: {
|
3467
|
-
message: "Failed to authorize",
|
3468
|
-
...(await fetchRes.json())
|
3469
|
-
}
|
3470
|
-
};
|
3471
|
-
}
|
3472
|
-
});
|
3473
|
-
}
|
3474
|
-
async consumeCode(code) {
|
3475
|
-
if (!this.options.project) {
|
3602
|
+
const url = new URL(`/auth/${options.project}/authorize`, options.valBuildUrl);
|
3603
|
+
url.searchParams.set("error", encodeURIComponent(error));
|
3604
|
+
return url.toString();
|
3605
|
+
};
|
3606
|
+
const consumeCode = async code => {
|
3607
|
+
if (!options.project) {
|
3476
3608
|
throw new Error("Project is not set");
|
3477
3609
|
}
|
3478
|
-
if (!
|
3610
|
+
if (!options.valBuildUrl) {
|
3479
3611
|
throw new Error("Val build url is not set");
|
3480
3612
|
}
|
3481
|
-
const url = new URL(`/api/val/${
|
3613
|
+
const url = new URL(`/api/val/${options.project}/auth/token`, options.valBuildUrl);
|
3482
3614
|
url.searchParams.set("code", encodeURIComponent(code));
|
3483
|
-
if (!
|
3615
|
+
if (!options.apiKey) {
|
3484
3616
|
return null;
|
3485
3617
|
}
|
3486
3618
|
return fetch(url, {
|
3487
3619
|
method: "POST",
|
3488
|
-
headers: getAuthHeaders(
|
3620
|
+
headers: getAuthHeaders(options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
|
3489
3621
|
}).then(async res => {
|
3490
3622
|
if (res.status === 200) {
|
3491
3623
|
const token = await res.text();
|
@@ -3505,34 +3637,11 @@ class ValServer {
|
|
3505
3637
|
console.debug("Failed to get user from code: ", err);
|
3506
3638
|
return null;
|
3507
3639
|
});
|
3508
|
-
}
|
3509
|
-
|
3510
|
-
if (!this.options.project) {
|
3511
|
-
throw new Error("Project is not set");
|
3512
|
-
}
|
3513
|
-
if (!this.options.valBuildUrl) {
|
3514
|
-
throw new Error("Val build url is not set");
|
3515
|
-
}
|
3516
|
-
const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
|
3517
|
-
url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRe}/callback`));
|
3518
|
-
url.searchParams.set("state", token);
|
3519
|
-
return url.toString();
|
3520
|
-
}
|
3521
|
-
getAppErrorUrl(error) {
|
3522
|
-
if (!this.options.project) {
|
3523
|
-
throw new Error("Project is not set");
|
3524
|
-
}
|
3525
|
-
if (!this.options.valBuildUrl) {
|
3526
|
-
throw new Error("Val build url is not set");
|
3527
|
-
}
|
3528
|
-
const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
|
3529
|
-
url.searchParams.set("error", encodeURIComponent(error));
|
3530
|
-
return url.toString();
|
3531
|
-
}
|
3532
|
-
getAuth(cookies) {
|
3640
|
+
};
|
3641
|
+
const getAuth = cookies => {
|
3533
3642
|
const cookie = cookies[internal.VAL_SESSION_COOKIE];
|
3534
|
-
if (!
|
3535
|
-
if (
|
3643
|
+
if (!options.valSecret) {
|
3644
|
+
if (serverOps instanceof ValOpsFS) {
|
3536
3645
|
return {
|
3537
3646
|
error: null,
|
3538
3647
|
id: null
|
@@ -3544,9 +3653,9 @@ class ValServer {
|
|
3544
3653
|
}
|
3545
3654
|
}
|
3546
3655
|
if (typeof cookie === "string") {
|
3547
|
-
const decodedToken = decodeJwt(cookie,
|
3656
|
+
const decodedToken = decodeJwt(cookie, options.valSecret);
|
3548
3657
|
if (!decodedToken) {
|
3549
|
-
if (
|
3658
|
+
if (serverOps instanceof ValOpsFS) {
|
3550
3659
|
return {
|
3551
3660
|
error: null,
|
3552
3661
|
id: null
|
@@ -3558,7 +3667,7 @@ class ValServer {
|
|
3558
3667
|
}
|
3559
3668
|
const verification = IntegratedServerJwtPayload.safeParse(decodedToken);
|
3560
3669
|
if (!verification.success) {
|
3561
|
-
if (
|
3670
|
+
if (serverOps instanceof ValOpsFS) {
|
3562
3671
|
return {
|
3563
3672
|
error: null,
|
3564
3673
|
id: null
|
@@ -3572,7 +3681,7 @@ class ValServer {
|
|
3572
3681
|
id: verification.data.sub
|
3573
3682
|
};
|
3574
3683
|
} else {
|
3575
|
-
if (
|
3684
|
+
if (serverOps instanceof ValOpsFS) {
|
3576
3685
|
return {
|
3577
3686
|
error: null,
|
3578
3687
|
id: null
|
@@ -3582,417 +3691,880 @@ class ValServer {
|
|
3582
3691
|
error: "Login required: cookie not found"
|
3583
3692
|
};
|
3584
3693
|
}
|
3585
|
-
}
|
3586
|
-
async
|
3694
|
+
};
|
3695
|
+
const authorize = async redirectTo => {
|
3696
|
+
const token = crypto.randomUUID();
|
3697
|
+
const redirectUrl = new URL(redirectTo);
|
3698
|
+
const appAuthorizeUrl = getAuthorizeUrl(`${redirectUrl.origin}/${options.route}`, token);
|
3699
|
+
await callbacks.onEnable(true);
|
3587
3700
|
return {
|
3588
|
-
status: 200,
|
3589
3701
|
cookies: {
|
3590
|
-
[internal.
|
3591
|
-
value: null
|
3592
|
-
},
|
3702
|
+
[internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
|
3593
3703
|
[internal.VAL_STATE_COOKIE]: {
|
3594
|
-
value:
|
3704
|
+
value: createStateCookie({
|
3705
|
+
redirect_to: redirectTo,
|
3706
|
+
token
|
3707
|
+
}),
|
3708
|
+
options: {
|
3709
|
+
httpOnly: true,
|
3710
|
+
sameSite: "lax",
|
3711
|
+
expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
|
3712
|
+
}
|
3595
3713
|
}
|
3596
|
-
}
|
3714
|
+
},
|
3715
|
+
status: 302,
|
3716
|
+
redirectTo: appAuthorizeUrl
|
3597
3717
|
};
|
3598
|
-
}
|
3599
|
-
|
3600
|
-
|
3601
|
-
|
3602
|
-
|
3603
|
-
|
3604
|
-
|
3605
|
-
|
3606
|
-
|
3607
|
-
|
3718
|
+
};
|
3719
|
+
return {
|
3720
|
+
"/draft/enable": {
|
3721
|
+
GET: async req => {
|
3722
|
+
const cookies = req.cookies;
|
3723
|
+
const auth = getAuth(cookies);
|
3724
|
+
if (auth.error) {
|
3725
|
+
return {
|
3726
|
+
status: 401,
|
3727
|
+
json: {
|
3728
|
+
message: auth.error
|
3729
|
+
}
|
3730
|
+
};
|
3608
3731
|
}
|
3609
|
-
|
3610
|
-
|
3611
|
-
|
3612
|
-
|
3613
|
-
status: 401,
|
3614
|
-
json: {
|
3615
|
-
message: "Unauthorized"
|
3732
|
+
const query = req.query;
|
3733
|
+
const redirectToRes = getRedirectUrl(query, options.valEnableRedirectUrl);
|
3734
|
+
if (typeof redirectToRes !== "string") {
|
3735
|
+
return redirectToRes;
|
3616
3736
|
}
|
3617
|
-
|
3618
|
-
|
3619
|
-
|
3620
|
-
|
3621
|
-
|
3622
|
-
|
3623
|
-
|
3624
|
-
|
3625
|
-
|
3626
|
-
|
3627
|
-
|
3628
|
-
|
3629
|
-
|
3630
|
-
|
3631
|
-
|
3737
|
+
await callbacks.onEnable(true);
|
3738
|
+
return {
|
3739
|
+
status: 302,
|
3740
|
+
redirectTo: redirectToRes
|
3741
|
+
};
|
3742
|
+
}
|
3743
|
+
},
|
3744
|
+
"/draft/disable": {
|
3745
|
+
GET: async req => {
|
3746
|
+
const cookies = req.cookies;
|
3747
|
+
const auth = getAuth(cookies);
|
3748
|
+
if (auth.error) {
|
3749
|
+
return {
|
3750
|
+
status: 401,
|
3751
|
+
json: {
|
3752
|
+
message: auth.error
|
3753
|
+
}
|
3754
|
+
};
|
3632
3755
|
}
|
3633
|
-
|
3634
|
-
|
3635
|
-
|
3636
|
-
|
3637
|
-
json: patches
|
3638
|
-
};
|
3639
|
-
}
|
3640
|
-
async deletePatches(query, cookies) {
|
3641
|
-
const auth = this.getAuth(cookies);
|
3642
|
-
if (auth.error) {
|
3643
|
-
return {
|
3644
|
-
status: 401,
|
3645
|
-
json: {
|
3646
|
-
message: auth.error
|
3756
|
+
const query = req.query;
|
3757
|
+
const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
|
3758
|
+
if (typeof redirectToRes !== "string") {
|
3759
|
+
return redirectToRes;
|
3647
3760
|
}
|
3648
|
-
|
3649
|
-
|
3650
|
-
|
3651
|
-
|
3652
|
-
|
3653
|
-
|
3654
|
-
|
3761
|
+
await callbacks.onDisable(true);
|
3762
|
+
return {
|
3763
|
+
status: 302,
|
3764
|
+
redirectTo: redirectToRes
|
3765
|
+
};
|
3766
|
+
}
|
3767
|
+
},
|
3768
|
+
"/draft/stat": {
|
3769
|
+
GET: async req => {
|
3770
|
+
const cookies = req.cookies;
|
3771
|
+
const auth = getAuth(cookies);
|
3772
|
+
if (auth.error) {
|
3773
|
+
return {
|
3774
|
+
status: 401,
|
3775
|
+
json: {
|
3776
|
+
message: auth.error
|
3777
|
+
}
|
3778
|
+
};
|
3655
3779
|
}
|
3656
|
-
|
3657
|
-
|
3658
|
-
|
3659
|
-
|
3660
|
-
|
3661
|
-
|
3662
|
-
|
3663
|
-
|
3664
|
-
|
3665
|
-
|
3666
|
-
|
3780
|
+
return {
|
3781
|
+
status: 200,
|
3782
|
+
json: {
|
3783
|
+
draftMode: await callbacks.isEnabled()
|
3784
|
+
}
|
3785
|
+
};
|
3786
|
+
}
|
3787
|
+
},
|
3788
|
+
"/enable": {
|
3789
|
+
GET: async req => {
|
3790
|
+
const cookies = req.cookies;
|
3791
|
+
const auth = getAuth(cookies);
|
3792
|
+
const query = req.query;
|
3793
|
+
const redirectToRes = getRedirectUrl(query, options.valEnableRedirectUrl);
|
3794
|
+
if (auth.error) {
|
3795
|
+
if (typeof redirectToRes === "string") {
|
3796
|
+
return authorize(redirectToRes);
|
3797
|
+
}
|
3798
|
+
return {
|
3799
|
+
status: 401,
|
3800
|
+
json: {
|
3801
|
+
message: auth.error
|
3802
|
+
}
|
3803
|
+
};
|
3667
3804
|
}
|
3668
|
-
|
3669
|
-
|
3670
|
-
return {
|
3671
|
-
status: 200,
|
3672
|
-
json: ids
|
3673
|
-
};
|
3674
|
-
}
|
3675
|
-
|
3676
|
-
//#region tree ops
|
3677
|
-
async getSchema(cookies) {
|
3678
|
-
const auth = this.getAuth(cookies);
|
3679
|
-
if (auth.error) {
|
3680
|
-
return {
|
3681
|
-
status: 401,
|
3682
|
-
json: {
|
3683
|
-
message: auth.error
|
3805
|
+
if (typeof redirectToRes !== "string") {
|
3806
|
+
return redirectToRes;
|
3684
3807
|
}
|
3685
|
-
|
3686
|
-
|
3687
|
-
|
3688
|
-
|
3689
|
-
|
3690
|
-
|
3691
|
-
|
3808
|
+
await callbacks.onEnable(true);
|
3809
|
+
return {
|
3810
|
+
cookies: {
|
3811
|
+
[internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
|
3812
|
+
},
|
3813
|
+
status: 302,
|
3814
|
+
redirectTo: redirectToRes
|
3815
|
+
};
|
3816
|
+
}
|
3817
|
+
},
|
3818
|
+
"/disable": {
|
3819
|
+
GET: async req => {
|
3820
|
+
const cookies = req.cookies;
|
3821
|
+
const auth = getAuth(cookies);
|
3822
|
+
if (auth.error) {
|
3823
|
+
return {
|
3824
|
+
status: 401,
|
3825
|
+
json: {
|
3826
|
+
message: auth.error
|
3827
|
+
}
|
3828
|
+
};
|
3692
3829
|
}
|
3693
|
-
|
3694
|
-
|
3695
|
-
|
3696
|
-
|
3697
|
-
console.error("Val: Module errors", moduleErrors);
|
3698
|
-
return {
|
3699
|
-
status: 500,
|
3700
|
-
json: {
|
3701
|
-
message: "Val is not correctly setup. Check the val.modules file",
|
3702
|
-
details: moduleErrors
|
3830
|
+
const query = req.query;
|
3831
|
+
const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
|
3832
|
+
if (typeof redirectToRes !== "string") {
|
3833
|
+
return redirectToRes;
|
3703
3834
|
}
|
3704
|
-
|
3705
|
-
|
3706
|
-
|
3707
|
-
|
3708
|
-
|
3709
|
-
|
3710
|
-
|
3711
|
-
|
3712
|
-
|
3713
|
-
|
3714
|
-
status: 200,
|
3715
|
-
json: {
|
3716
|
-
schemaSha,
|
3717
|
-
schemas: serializedSchemas
|
3835
|
+
await callbacks.onDisable(true);
|
3836
|
+
return {
|
3837
|
+
cookies: {
|
3838
|
+
[internal.VAL_ENABLE_COOKIE_NAME]: {
|
3839
|
+
value: "false"
|
3840
|
+
}
|
3841
|
+
},
|
3842
|
+
status: 302,
|
3843
|
+
redirectTo: redirectToRes
|
3844
|
+
};
|
3718
3845
|
}
|
3719
|
-
}
|
3720
|
-
|
3721
|
-
|
3722
|
-
|
3723
|
-
|
3724
|
-
|
3725
|
-
|
3726
|
-
|
3727
|
-
|
3728
|
-
|
3846
|
+
},
|
3847
|
+
//#region auth
|
3848
|
+
"/authorize": {
|
3849
|
+
GET: async req => {
|
3850
|
+
const query = req.query;
|
3851
|
+
if (typeof query.redirect_to !== "string") {
|
3852
|
+
return {
|
3853
|
+
status: 400,
|
3854
|
+
json: {
|
3855
|
+
message: "Missing redirect_to query param"
|
3856
|
+
}
|
3857
|
+
};
|
3729
3858
|
}
|
3730
|
-
|
3731
|
-
|
3732
|
-
|
3733
|
-
|
3734
|
-
|
3735
|
-
|
3736
|
-
|
3859
|
+
const redirectTo = query.redirect_to;
|
3860
|
+
return authorize(redirectTo);
|
3861
|
+
}
|
3862
|
+
},
|
3863
|
+
"/callback": {
|
3864
|
+
GET: async req => {
|
3865
|
+
const cookies = req.cookies;
|
3866
|
+
const query = req.query;
|
3867
|
+
if (!options.project) {
|
3868
|
+
return {
|
3869
|
+
status: 302,
|
3870
|
+
cookies: {
|
3871
|
+
[internal.VAL_STATE_COOKIE]: {
|
3872
|
+
value: null
|
3873
|
+
}
|
3874
|
+
},
|
3875
|
+
redirectTo: getAppErrorUrl("Project is not set")
|
3876
|
+
};
|
3737
3877
|
}
|
3738
|
-
|
3739
|
-
|
3740
|
-
|
3741
|
-
|
3742
|
-
|
3743
|
-
|
3744
|
-
|
3745
|
-
|
3746
|
-
|
3747
|
-
|
3748
|
-
}).optional()
|
3749
|
-
}).optional();
|
3750
|
-
const moduleErrors = await this.serverOps.getModuleErrors();
|
3751
|
-
if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
|
3752
|
-
console.error("Val: Module errors", moduleErrors);
|
3753
|
-
return {
|
3754
|
-
status: 500,
|
3755
|
-
json: {
|
3756
|
-
message: "Val is not correctly setup. Check the val.modules file",
|
3757
|
-
details: moduleErrors
|
3878
|
+
if (!options.valSecret) {
|
3879
|
+
return {
|
3880
|
+
status: 302,
|
3881
|
+
cookies: {
|
3882
|
+
[internal.VAL_STATE_COOKIE]: {
|
3883
|
+
value: null
|
3884
|
+
}
|
3885
|
+
},
|
3886
|
+
redirectTo: getAppErrorUrl("Secret is not set")
|
3887
|
+
};
|
3758
3888
|
}
|
3759
|
-
|
3760
|
-
|
3761
|
-
|
3762
|
-
|
3763
|
-
|
3764
|
-
|
3765
|
-
|
3766
|
-
|
3767
|
-
|
3889
|
+
const {
|
3890
|
+
success: callbackReqSuccess,
|
3891
|
+
error: callbackReqError
|
3892
|
+
} = verifyCallbackReq(cookies[internal.VAL_STATE_COOKIE], query);
|
3893
|
+
if (callbackReqError !== null) {
|
3894
|
+
return {
|
3895
|
+
status: 302,
|
3896
|
+
cookies: {
|
3897
|
+
[internal.VAL_STATE_COOKIE]: {
|
3898
|
+
value: null
|
3899
|
+
}
|
3900
|
+
},
|
3901
|
+
redirectTo: getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`)
|
3902
|
+
};
|
3903
|
+
}
|
3904
|
+
const data = await consumeCode(callbackReqSuccess.code);
|
3905
|
+
if (data === null) {
|
3906
|
+
return {
|
3907
|
+
status: 302,
|
3908
|
+
cookies: {
|
3909
|
+
[internal.VAL_STATE_COOKIE]: {
|
3910
|
+
value: null
|
3911
|
+
}
|
3912
|
+
},
|
3913
|
+
redirectTo: getAppErrorUrl("Failed to exchange code for user")
|
3914
|
+
};
|
3915
|
+
}
|
3916
|
+
const exp = getExpire();
|
3917
|
+
const valSecret = options.valSecret;
|
3918
|
+
if (!valSecret) {
|
3919
|
+
return {
|
3920
|
+
status: 302,
|
3921
|
+
cookies: {
|
3922
|
+
[internal.VAL_STATE_COOKIE]: {
|
3923
|
+
value: null
|
3924
|
+
}
|
3925
|
+
},
|
3926
|
+
redirectTo: getAppErrorUrl("Setup is not correct: secret is missing")
|
3927
|
+
};
|
3928
|
+
}
|
3929
|
+
const cookie = encodeJwt({
|
3930
|
+
...data,
|
3931
|
+
exp // this is the client side exp
|
3932
|
+
}, valSecret);
|
3933
|
+
return {
|
3934
|
+
status: 302,
|
3935
|
+
cookies: {
|
3936
|
+
[internal.VAL_STATE_COOKIE]: {
|
3937
|
+
value: null
|
3938
|
+
},
|
3939
|
+
[internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
|
3940
|
+
[internal.VAL_SESSION_COOKIE]: {
|
3941
|
+
value: cookie,
|
3942
|
+
options: {
|
3943
|
+
httpOnly: true,
|
3944
|
+
sameSite: "strict",
|
3945
|
+
path: "/",
|
3946
|
+
secure: true,
|
3947
|
+
expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
|
3948
|
+
}
|
3949
|
+
}
|
3950
|
+
},
|
3951
|
+
redirectTo: callbackReqSuccess.redirect_uri || "/"
|
3952
|
+
};
|
3953
|
+
}
|
3954
|
+
},
|
3955
|
+
"/session": {
|
3956
|
+
GET: async req => {
|
3957
|
+
const cookies = req.cookies;
|
3958
|
+
if (serverOps instanceof ValOpsFS) {
|
3959
|
+
return {
|
3960
|
+
status: 200,
|
3961
|
+
json: {
|
3962
|
+
mode: "local",
|
3963
|
+
enabled: await callbacks.isEnabled()
|
3964
|
+
}
|
3965
|
+
};
|
3966
|
+
}
|
3967
|
+
if (!options.project) {
|
3968
|
+
return {
|
3969
|
+
status: 500,
|
3970
|
+
json: {
|
3971
|
+
message: "Project is not set"
|
3972
|
+
}
|
3973
|
+
};
|
3974
|
+
}
|
3975
|
+
if (!options.valSecret) {
|
3976
|
+
return {
|
3977
|
+
status: 500,
|
3978
|
+
json: {
|
3979
|
+
message: "Secret is not set"
|
3980
|
+
}
|
3981
|
+
};
|
3982
|
+
}
|
3983
|
+
return withAuth(options.valSecret, cookies, "session", async data => {
|
3984
|
+
if (!options.valBuildUrl) {
|
3985
|
+
return {
|
3986
|
+
status: 500,
|
3987
|
+
json: {
|
3988
|
+
message: "Val is not correctly setup. Build url is missing"
|
3989
|
+
}
|
3990
|
+
};
|
3991
|
+
}
|
3992
|
+
const url = new URL(`/api/val/${options.project}/auth/session`, options.valBuildUrl);
|
3993
|
+
const fetchRes = await fetch(url, {
|
3994
|
+
headers: getAuthHeaders(data.token, "application/json")
|
3995
|
+
});
|
3996
|
+
if (fetchRes.status === 200) {
|
3997
|
+
return {
|
3998
|
+
status: fetchRes.status,
|
3999
|
+
json: {
|
4000
|
+
mode: "proxy",
|
4001
|
+
enabled: await callbacks.isEnabled(),
|
4002
|
+
...(await fetchRes.json())
|
4003
|
+
}
|
4004
|
+
};
|
4005
|
+
} else {
|
4006
|
+
return {
|
4007
|
+
status: fetchRes.status,
|
4008
|
+
json: {
|
4009
|
+
message: "Failed to authorize",
|
4010
|
+
...(await fetchRes.json())
|
4011
|
+
}
|
4012
|
+
};
|
4013
|
+
}
|
4014
|
+
});
|
4015
|
+
}
|
4016
|
+
},
|
4017
|
+
"/logout": {
|
4018
|
+
GET: async () => {
|
4019
|
+
return {
|
4020
|
+
status: 200,
|
4021
|
+
cookies: {
|
4022
|
+
[internal.VAL_SESSION_COOKIE]: {
|
4023
|
+
value: null
|
4024
|
+
},
|
4025
|
+
[internal.VAL_STATE_COOKIE]: {
|
4026
|
+
value: null
|
4027
|
+
}
|
4028
|
+
}
|
4029
|
+
};
|
4030
|
+
}
|
4031
|
+
},
|
4032
|
+
//#region stat
|
4033
|
+
"/stat": {
|
4034
|
+
POST: async req => {
|
4035
|
+
const cookies = req.cookies;
|
4036
|
+
const auth = getAuth(cookies);
|
4037
|
+
if (auth.error) {
|
4038
|
+
return {
|
4039
|
+
status: 401,
|
4040
|
+
json: {
|
4041
|
+
message: auth.error
|
4042
|
+
}
|
4043
|
+
};
|
4044
|
+
}
|
4045
|
+
if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
|
4046
|
+
return {
|
4047
|
+
status: 401,
|
4048
|
+
json: {
|
4049
|
+
message: "Unauthorized"
|
4050
|
+
}
|
4051
|
+
};
|
4052
|
+
}
|
4053
|
+
const currentStat = await serverOps.getStat({
|
4054
|
+
...req.body,
|
4055
|
+
profileId: "id" in auth ? auth.id : undefined
|
4056
|
+
});
|
4057
|
+
if (currentStat.type === "error") {
|
4058
|
+
return {
|
4059
|
+
status: 500,
|
4060
|
+
json: currentStat.error
|
4061
|
+
};
|
4062
|
+
}
|
4063
|
+
return {
|
4064
|
+
status: 200,
|
4065
|
+
json: {
|
4066
|
+
...currentStat,
|
4067
|
+
config: options.config
|
4068
|
+
}
|
4069
|
+
};
|
4070
|
+
}
|
4071
|
+
},
|
4072
|
+
//#region patches
|
4073
|
+
"/patches/~": {
|
4074
|
+
GET: async req => {
|
4075
|
+
const query = req.query;
|
4076
|
+
const cookies = req.cookies;
|
4077
|
+
const auth = getAuth(cookies);
|
4078
|
+
if (auth.error) {
|
4079
|
+
return {
|
4080
|
+
status: 401,
|
4081
|
+
json: {
|
4082
|
+
message: auth.error
|
4083
|
+
}
|
4084
|
+
};
|
4085
|
+
}
|
4086
|
+
if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
|
4087
|
+
return {
|
4088
|
+
status: 401,
|
4089
|
+
json: {
|
4090
|
+
message: "Unauthorized"
|
4091
|
+
}
|
4092
|
+
};
|
4093
|
+
}
|
4094
|
+
const authors = query.author;
|
4095
|
+
const patches = await serverOps.fetchPatches({
|
4096
|
+
authors,
|
4097
|
+
patchIds: query.patch_id,
|
4098
|
+
omitPatch: query.omit_patch === true,
|
4099
|
+
moduleFilePaths: query.module_file_path
|
4100
|
+
});
|
4101
|
+
if (patches.error) {
|
4102
|
+
// Error is singular
|
4103
|
+
console.error("Val: Failed to get patches", patches.errors);
|
4104
|
+
return {
|
4105
|
+
status: 500,
|
4106
|
+
json: {
|
4107
|
+
message: patches.error.message,
|
4108
|
+
details: patches.error
|
4109
|
+
}
|
4110
|
+
};
|
4111
|
+
}
|
4112
|
+
if (patches.errors && Object.keys(patches.errors).length > 0) {
|
4113
|
+
// Errors is plural. Different property than above.
|
4114
|
+
console.error("Val: Failed to get patches", patches.errors);
|
4115
|
+
return {
|
4116
|
+
status: 500,
|
4117
|
+
json: {
|
4118
|
+
message: "Failed to get patches",
|
4119
|
+
details: patches.errors
|
4120
|
+
}
|
4121
|
+
};
|
4122
|
+
}
|
4123
|
+
return {
|
4124
|
+
status: 200,
|
4125
|
+
json: patches
|
4126
|
+
};
|
4127
|
+
},
|
4128
|
+
DELETE: async req => {
|
4129
|
+
const query = req.query;
|
4130
|
+
const cookies = req.cookies;
|
4131
|
+
const auth = getAuth(cookies);
|
4132
|
+
if (auth.error) {
|
4133
|
+
return {
|
4134
|
+
status: 401,
|
4135
|
+
json: {
|
4136
|
+
message: auth.error
|
4137
|
+
}
|
4138
|
+
};
|
4139
|
+
}
|
4140
|
+
if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
|
4141
|
+
return {
|
4142
|
+
status: 401,
|
4143
|
+
json: {
|
4144
|
+
message: "Unauthorized"
|
4145
|
+
}
|
4146
|
+
};
|
4147
|
+
}
|
4148
|
+
const ids = query.id;
|
4149
|
+
const deleteRes = await serverOps.deletePatches(ids);
|
4150
|
+
if (deleteRes.errors && Object.keys(deleteRes.errors).length > 0) {
|
4151
|
+
console.error("Val: Failed to delete patches", deleteRes.errors);
|
4152
|
+
return {
|
4153
|
+
status: 500,
|
4154
|
+
json: {
|
4155
|
+
message: "Failed to delete patches",
|
4156
|
+
details: deleteRes.errors
|
4157
|
+
}
|
4158
|
+
};
|
4159
|
+
}
|
4160
|
+
return {
|
4161
|
+
status: 200,
|
4162
|
+
json: ids
|
4163
|
+
};
|
4164
|
+
}
|
4165
|
+
},
|
4166
|
+
//#region schema
|
4167
|
+
"/schema": {
|
4168
|
+
GET: async req => {
|
4169
|
+
const cookies = req.cookies;
|
4170
|
+
const auth = getAuth(cookies);
|
4171
|
+
if (auth.error) {
|
4172
|
+
return {
|
4173
|
+
status: 401,
|
4174
|
+
json: {
|
4175
|
+
message: auth.error
|
4176
|
+
}
|
4177
|
+
};
|
4178
|
+
}
|
4179
|
+
if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
|
4180
|
+
return {
|
4181
|
+
status: 401,
|
4182
|
+
json: {
|
4183
|
+
message: "Unauthorized"
|
4184
|
+
}
|
4185
|
+
};
|
4186
|
+
}
|
4187
|
+
const moduleErrors = await serverOps.getModuleErrors();
|
4188
|
+
if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
|
4189
|
+
console.error("Val: Module errors", moduleErrors);
|
4190
|
+
return {
|
4191
|
+
status: 500,
|
4192
|
+
json: {
|
4193
|
+
message: `Got errors while fetching modules: ${moduleErrors.filter(error => error).map(error => error.message).join(", ")}`,
|
4194
|
+
details: moduleErrors
|
4195
|
+
}
|
4196
|
+
};
|
4197
|
+
}
|
4198
|
+
const schemaSha = await serverOps.getSchemaSha();
|
4199
|
+
const schemas = await serverOps.getSchemas();
|
4200
|
+
const serializedSchemas = {};
|
4201
|
+
try {
|
4202
|
+
for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
|
4203
|
+
const moduleFilePath = moduleFilePathS;
|
4204
|
+
serializedSchemas[moduleFilePath] = schema.serialize();
|
4205
|
+
}
|
4206
|
+
} catch (e) {
|
4207
|
+
console.error("Val: Failed to serialize schemas", e);
|
4208
|
+
return {
|
4209
|
+
status: 500,
|
4210
|
+
json: {
|
4211
|
+
message: "Failed to serialize schemas",
|
4212
|
+
details: [{
|
4213
|
+
message: e instanceof Error ? e.message : JSON.stringify(e)
|
4214
|
+
}]
|
4215
|
+
}
|
4216
|
+
};
|
4217
|
+
}
|
4218
|
+
return {
|
4219
|
+
status: 200,
|
4220
|
+
json: {
|
4221
|
+
schemaSha,
|
4222
|
+
schemas: serializedSchemas
|
4223
|
+
}
|
4224
|
+
};
|
4225
|
+
}
|
4226
|
+
},
|
4227
|
+
// #region sources
|
4228
|
+
"/sources": {
|
4229
|
+
PUT: async req => {
|
4230
|
+
var _body$patchIds;
|
4231
|
+
const query = req.query;
|
4232
|
+
const cookies = req.cookies;
|
4233
|
+
const body = req.body;
|
4234
|
+
const treePath = req.path || "";
|
4235
|
+
const auth = getAuth(cookies);
|
4236
|
+
if (auth.error) {
|
4237
|
+
return {
|
4238
|
+
status: 401,
|
4239
|
+
json: {
|
4240
|
+
message: auth.error
|
4241
|
+
}
|
4242
|
+
};
|
4243
|
+
}
|
4244
|
+
if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
|
4245
|
+
return {
|
4246
|
+
status: 401,
|
4247
|
+
json: {
|
4248
|
+
message: "Unauthorized"
|
4249
|
+
}
|
4250
|
+
};
|
4251
|
+
}
|
4252
|
+
const moduleErrors = await serverOps.getModuleErrors();
|
4253
|
+
if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
|
4254
|
+
console.error("Val: Module errors", moduleErrors);
|
4255
|
+
return {
|
4256
|
+
status: 500,
|
4257
|
+
json: {
|
4258
|
+
message: "Val is not correctly setup. Check the val.modules file",
|
4259
|
+
details: moduleErrors
|
4260
|
+
}
|
4261
|
+
};
|
4262
|
+
}
|
4263
|
+
let tree;
|
4264
|
+
let patchAnalysis = null;
|
4265
|
+
let newPatchIds = undefined;
|
4266
|
+
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.addPatches) {
|
4267
|
+
// TODO: validate patches_sha
|
4268
|
+
const patchIds = body === null || body === void 0 ? void 0 : body.patchIds;
|
4269
|
+
const patchOps = patchIds && patchIds.length > 0 ? await serverOps.fetchPatches({
|
4270
|
+
patchIds,
|
4271
|
+
omitPatch: false
|
4272
|
+
}) : {
|
4273
|
+
patches: {}
|
4274
|
+
};
|
4275
|
+
if (patchOps.error) {
|
4276
|
+
return {
|
4277
|
+
status: 400,
|
4278
|
+
json: {
|
4279
|
+
message: "Failed to fetch patches: " + patchOps.error.message,
|
4280
|
+
details: []
|
4281
|
+
}
|
4282
|
+
};
|
4283
|
+
}
|
4284
|
+
let patchErrors = undefined;
|
4285
|
+
for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
|
4286
|
+
const patchId = patchIdS;
|
4287
|
+
if (!patchErrors) {
|
4288
|
+
patchErrors = {};
|
4289
|
+
}
|
4290
|
+
patchErrors[patchId] = {
|
4291
|
+
message: error.message
|
4292
|
+
};
|
4293
|
+
}
|
4294
|
+
// TODO: errors
|
4295
|
+
patchAnalysis = serverOps.analyzePatches(patchOps.patches);
|
4296
|
+
if (body !== null && body !== void 0 && body.addPatches) {
|
4297
|
+
for (const addPatch of body.addPatches) {
|
4298
|
+
const newPatchModuleFilePath = addPatch.path;
|
4299
|
+
const newPatchOps = addPatch.patch;
|
4300
|
+
const authorId = "id" in auth ? auth.id : null;
|
4301
|
+
const createPatchRes = await serverOps.createPatch(newPatchModuleFilePath, {
|
4302
|
+
...patchAnalysis,
|
4303
|
+
...patchOps
|
4304
|
+
}, newPatchOps, authorId);
|
4305
|
+
if (createPatchRes.error) {
|
4306
|
+
return {
|
4307
|
+
status: 500,
|
4308
|
+
json: {
|
4309
|
+
message: "Failed to create patch: " + createPatchRes.error.message,
|
4310
|
+
details: createPatchRes.error
|
4311
|
+
}
|
4312
|
+
};
|
4313
|
+
}
|
4314
|
+
if (!newPatchIds) {
|
4315
|
+
newPatchIds = [createPatchRes.patchId];
|
4316
|
+
} else {
|
4317
|
+
newPatchIds.push(createPatchRes.patchId);
|
4318
|
+
}
|
4319
|
+
patchOps.patches[createPatchRes.patchId] = {
|
4320
|
+
path: newPatchModuleFilePath,
|
4321
|
+
patch: newPatchOps,
|
4322
|
+
authorId,
|
4323
|
+
createdAt: createPatchRes.createdAt,
|
4324
|
+
appliedAt: null
|
4325
|
+
};
|
4326
|
+
patchAnalysis.patchesByModule[newPatchModuleFilePath] = [...(patchAnalysis.patchesByModule[newPatchModuleFilePath] || []), {
|
4327
|
+
patchId: createPatchRes.patchId
|
4328
|
+
}];
|
4329
|
+
}
|
4330
|
+
}
|
4331
|
+
tree = {
|
4332
|
+
...(await serverOps.getTree({
|
4333
|
+
...patchAnalysis,
|
4334
|
+
...patchOps
|
4335
|
+
}))
|
4336
|
+
};
|
4337
|
+
if (query.validate_all) {
|
4338
|
+
const allTree = await serverOps.getTree();
|
4339
|
+
tree = {
|
4340
|
+
sources: {
|
4341
|
+
...allTree.sources,
|
4342
|
+
...tree.sources
|
4343
|
+
},
|
4344
|
+
errors: {
|
4345
|
+
...allTree.errors,
|
4346
|
+
...tree.errors
|
4347
|
+
}
|
4348
|
+
};
|
4349
|
+
}
|
4350
|
+
} else {
|
4351
|
+
tree = await serverOps.getTree();
|
3768
4352
|
}
|
3769
|
-
|
3770
|
-
|
3771
|
-
|
3772
|
-
let patchAnalysis = null;
|
3773
|
-
let newPatchId = undefined;
|
3774
|
-
if ((_bodyRes$data = bodyRes.data) !== null && _bodyRes$data !== void 0 && _bodyRes$data.patchIds && ((_bodyRes$data2 = bodyRes.data) === null || _bodyRes$data2 === void 0 || (_bodyRes$data2 = _bodyRes$data2.patchIds) === null || _bodyRes$data2 === void 0 ? void 0 : _bodyRes$data2.length) > 0 || (_bodyRes$data3 = bodyRes.data) !== null && _bodyRes$data3 !== void 0 && _bodyRes$data3.addPatch) {
|
3775
|
-
var _bodyRes$data4, _bodyRes$data5;
|
3776
|
-
// TODO: validate patches_sha
|
3777
|
-
const patchIds = (_bodyRes$data4 = bodyRes.data) === null || _bodyRes$data4 === void 0 ? void 0 : _bodyRes$data4.patchIds;
|
3778
|
-
const patchOps = patchIds && patchIds.length > 0 ? await this.serverOps.fetchPatches({
|
3779
|
-
patchIds,
|
3780
|
-
omitPatch: false
|
3781
|
-
}) : {
|
3782
|
-
patches: {}
|
3783
|
-
};
|
3784
|
-
let patchErrors = undefined;
|
3785
|
-
for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
|
3786
|
-
const patchId = patchIdS;
|
3787
|
-
if (!patchErrors) {
|
3788
|
-
patchErrors = {};
|
3789
|
-
}
|
3790
|
-
patchErrors[patchId] = {
|
3791
|
-
message: error.message
|
4353
|
+
let sourcesValidation = {
|
4354
|
+
errors: {},
|
4355
|
+
files: {}
|
3792
4356
|
};
|
3793
|
-
|
3794
|
-
|
3795
|
-
|
3796
|
-
|
3797
|
-
|
3798
|
-
|
3799
|
-
|
3800
|
-
|
3801
|
-
|
4357
|
+
if (query.validate_sources || query.validate_binary_files) {
|
4358
|
+
const schemas = await serverOps.getSchemas();
|
4359
|
+
sourcesValidation = await serverOps.validateSources(schemas, tree.sources);
|
4360
|
+
|
4361
|
+
// TODO: send validation errors
|
4362
|
+
if (query.validate_binary_files) {
|
4363
|
+
await serverOps.validateFiles(schemas, tree.sources, sourcesValidation.files);
|
4364
|
+
}
|
4365
|
+
}
|
4366
|
+
const schemaSha = await serverOps.getSchemaSha();
|
4367
|
+
const modules = {};
|
4368
|
+
for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
|
4369
|
+
const moduleFilePath = moduleFilePathS;
|
4370
|
+
if (moduleFilePath.startsWith(treePath)) {
|
4371
|
+
var _sourcesValidation$er;
|
4372
|
+
modules[moduleFilePath] = {
|
4373
|
+
source: module,
|
4374
|
+
patches: patchAnalysis && patchAnalysis.patchesByModule[moduleFilePath] ? {
|
4375
|
+
applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
|
4376
|
+
} : undefined,
|
4377
|
+
validationErrors: (_sourcesValidation$er = sourcesValidation.errors[moduleFilePath]) === null || _sourcesValidation$er === void 0 ? void 0 : _sourcesValidation$er.validations
|
4378
|
+
};
|
4379
|
+
}
|
4380
|
+
}
|
4381
|
+
if (tree.errors && Object.keys(tree.errors).length > 0) {
|
4382
|
+
const res = {
|
4383
|
+
status: 400,
|
3802
4384
|
json: {
|
3803
|
-
|
3804
|
-
|
4385
|
+
type: "patch-error",
|
4386
|
+
schemaSha,
|
4387
|
+
modules,
|
4388
|
+
errors: Object.fromEntries(Object.entries(tree.errors).map(([key, value]) => [key, value.map(error => ({
|
4389
|
+
patchId: error.patchId,
|
4390
|
+
skipped: error.skipped,
|
4391
|
+
error: {
|
4392
|
+
message: error.error.message
|
4393
|
+
}
|
4394
|
+
}))])),
|
4395
|
+
message: "One or more patches failed to be applied"
|
3805
4396
|
}
|
3806
4397
|
};
|
4398
|
+
return res;
|
3807
4399
|
}
|
3808
|
-
|
3809
|
-
|
3810
|
-
|
3811
|
-
|
3812
|
-
|
3813
|
-
|
3814
|
-
// status: 500,
|
3815
|
-
// json: {
|
3816
|
-
// message: "Failed to create patch",
|
3817
|
-
// details: fileRes.error,
|
3818
|
-
// },
|
3819
|
-
// };
|
3820
|
-
// }
|
3821
|
-
// }
|
3822
|
-
newPatchId = createPatchRes.patchId;
|
3823
|
-
patchOps.patches[createPatchRes.patchId] = {
|
3824
|
-
path: newPatchModuleFilePath,
|
3825
|
-
patch: newPatchOps,
|
3826
|
-
authorId,
|
3827
|
-
createdAt: createPatchRes.createdAt,
|
3828
|
-
appliedAt: null
|
3829
|
-
};
|
3830
|
-
}
|
3831
|
-
// TODO: errors
|
3832
|
-
patchAnalysis = this.serverOps.analyzePatches(patchOps.patches);
|
3833
|
-
tree = {
|
3834
|
-
...(await this.serverOps.getTree({
|
3835
|
-
...patchAnalysis,
|
3836
|
-
...patchOps
|
3837
|
-
}))
|
3838
|
-
};
|
3839
|
-
if (query.validate_all === "true") {
|
3840
|
-
const allTree = await this.serverOps.getTree();
|
3841
|
-
tree = {
|
3842
|
-
sources: {
|
3843
|
-
...allTree.sources,
|
3844
|
-
...tree.sources
|
3845
|
-
},
|
3846
|
-
errors: {
|
3847
|
-
...allTree.errors,
|
3848
|
-
...tree.errors
|
4400
|
+
const res = {
|
4401
|
+
status: 200,
|
4402
|
+
json: {
|
4403
|
+
schemaSha,
|
4404
|
+
modules,
|
4405
|
+
newPatchIds
|
3849
4406
|
}
|
3850
4407
|
};
|
4408
|
+
return res;
|
3851
4409
|
}
|
3852
|
-
}
|
3853
|
-
|
3854
|
-
|
3855
|
-
|
3856
|
-
|
3857
|
-
|
3858
|
-
|
3859
|
-
|
3860
|
-
|
3861
|
-
|
3862
|
-
|
3863
|
-
|
3864
|
-
|
3865
|
-
}
|
3866
|
-
}
|
3867
|
-
const schemaSha = await this.serverOps.getSchemaSha();
|
3868
|
-
const modules = {};
|
3869
|
-
for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
|
3870
|
-
const moduleFilePath = moduleFilePathS;
|
3871
|
-
if (moduleFilePath.startsWith(treePath)) {
|
3872
|
-
modules[moduleFilePath] = {
|
3873
|
-
source: module,
|
3874
|
-
patches: patchAnalysis ? {
|
3875
|
-
applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
|
3876
|
-
} : undefined
|
3877
|
-
};
|
3878
|
-
}
|
3879
|
-
}
|
3880
|
-
return {
|
3881
|
-
status: 200,
|
3882
|
-
json: {
|
3883
|
-
schemaSha,
|
3884
|
-
modules,
|
3885
|
-
newPatchId
|
3886
|
-
}
|
3887
|
-
};
|
3888
|
-
}
|
3889
|
-
async postSave(body, cookies) {
|
3890
|
-
const auth = this.getAuth(cookies);
|
3891
|
-
if (auth.error) {
|
3892
|
-
return {
|
3893
|
-
status: 401,
|
3894
|
-
json: {
|
3895
|
-
message: auth.error
|
4410
|
+
},
|
4411
|
+
"/save": {
|
4412
|
+
POST: async req => {
|
4413
|
+
const cookies = req.cookies;
|
4414
|
+
const body = req.body;
|
4415
|
+
const auth = getAuth(cookies);
|
4416
|
+
if (auth.error) {
|
4417
|
+
return {
|
4418
|
+
status: 401,
|
4419
|
+
json: {
|
4420
|
+
message: auth.error
|
4421
|
+
}
|
4422
|
+
};
|
3896
4423
|
}
|
3897
|
-
|
3898
|
-
|
3899
|
-
|
3900
|
-
|
3901
|
-
|
3902
|
-
|
3903
|
-
|
3904
|
-
|
3905
|
-
|
3906
|
-
|
3907
|
-
|
3908
|
-
|
3909
|
-
|
4424
|
+
const PostSaveBody = z.z.object({
|
4425
|
+
patchIds: z.z.array(z.z.string().refine(id => true // TODO:
|
4426
|
+
))
|
4427
|
+
});
|
4428
|
+
const bodyRes = PostSaveBody.safeParse(body);
|
4429
|
+
if (!bodyRes.success) {
|
4430
|
+
return {
|
4431
|
+
status: 400,
|
4432
|
+
json: {
|
4433
|
+
message: "Invalid body: " + zodValidationError.fromError(bodyRes.error).toString(),
|
4434
|
+
details: bodyRes.error.errors
|
4435
|
+
}
|
4436
|
+
};
|
3910
4437
|
}
|
3911
|
-
|
3912
|
-
|
3913
|
-
|
3914
|
-
|
3915
|
-
|
3916
|
-
|
3917
|
-
|
3918
|
-
|
3919
|
-
|
3920
|
-
|
3921
|
-
|
3922
|
-
|
3923
|
-
|
3924
|
-
|
3925
|
-
if (preparedCommit.hasErrors) {
|
3926
|
-
console.error("Failed to create commit", {
|
3927
|
-
sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
|
3928
|
-
binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
|
3929
|
-
});
|
3930
|
-
return {
|
3931
|
-
status: 400,
|
3932
|
-
json: {
|
3933
|
-
message: "Failed to create commit",
|
3934
|
-
details: {
|
4438
|
+
const {
|
4439
|
+
patchIds
|
4440
|
+
} = bodyRes.data;
|
4441
|
+
const patches = await serverOps.fetchPatches({
|
4442
|
+
patchIds,
|
4443
|
+
omitPatch: false
|
4444
|
+
});
|
4445
|
+
const analysis = serverOps.analyzePatches(patches.patches);
|
4446
|
+
const preparedCommit = await serverOps.prepare({
|
4447
|
+
...analysis,
|
4448
|
+
...patches
|
4449
|
+
});
|
4450
|
+
if (preparedCommit.hasErrors) {
|
4451
|
+
console.error("Failed to create commit", JSON.stringify({
|
3935
4452
|
sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
|
3936
4453
|
binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
|
4454
|
+
}, null, 2));
|
4455
|
+
return {
|
4456
|
+
status: 400,
|
4457
|
+
json: {
|
4458
|
+
message: "Failed to create commit",
|
4459
|
+
details: {
|
4460
|
+
sourceFilePatchErrors: Object.fromEntries(Object.entries(preparedCommit.sourceFilePatchErrors).map(([key, errors]) => [key, errors.map(e => ({
|
4461
|
+
message: formatPatchSourceError(e)
|
4462
|
+
}))])),
|
4463
|
+
binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
|
4464
|
+
}
|
4465
|
+
}
|
4466
|
+
};
|
4467
|
+
}
|
4468
|
+
if (serverOps instanceof ValOpsFS) {
|
4469
|
+
await serverOps.saveFiles(preparedCommit);
|
4470
|
+
await serverOps.deletePatches(patchIds);
|
4471
|
+
return {
|
4472
|
+
status: 200,
|
4473
|
+
json: {} // TODO:
|
4474
|
+
};
|
4475
|
+
} else if (serverOps instanceof ValOpsHttp) {
|
4476
|
+
if (auth.error === undefined && auth.id) {
|
4477
|
+
const commitRes = await serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
|
4478
|
+
if (commitRes.error) {
|
4479
|
+
console.error("Failed to commit", commitRes.error);
|
4480
|
+
if ("isNotFastForward" in commitRes && commitRes.isNotFastForward) {
|
4481
|
+
return {
|
4482
|
+
status: 409,
|
4483
|
+
json: {
|
4484
|
+
isNotFastForward: true,
|
4485
|
+
message: "Cannot commit: this is not the latest version of this branch"
|
4486
|
+
}
|
4487
|
+
};
|
4488
|
+
}
|
4489
|
+
return {
|
4490
|
+
status: 400,
|
4491
|
+
json: {
|
4492
|
+
message: commitRes.error.message,
|
4493
|
+
details: []
|
4494
|
+
}
|
4495
|
+
};
|
4496
|
+
}
|
4497
|
+
// TODO: serverOps.markApplied(patchIds);
|
4498
|
+
return {
|
4499
|
+
status: 200,
|
4500
|
+
json: {} // TODO:
|
4501
|
+
};
|
3937
4502
|
}
|
4503
|
+
return {
|
4504
|
+
status: 401,
|
4505
|
+
json: {
|
4506
|
+
message: "Unauthorized"
|
4507
|
+
}
|
4508
|
+
};
|
4509
|
+
} else {
|
4510
|
+
throw new Error("Invalid server ops");
|
3938
4511
|
}
|
3939
|
-
};
|
3940
|
-
}
|
3941
|
-
if (this.serverOps instanceof ValOpsFS) {
|
3942
|
-
await this.serverOps.saveFiles(preparedCommit);
|
3943
|
-
return {
|
3944
|
-
status: 200,
|
3945
|
-
json: {} // TODO:
|
3946
|
-
};
|
3947
|
-
} else if (this.serverOps instanceof ValOpsHttp) {
|
3948
|
-
if (auth.error === undefined && auth.id) {
|
3949
|
-
await this.serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
|
3950
|
-
return {
|
3951
|
-
status: 200,
|
3952
|
-
json: {} // TODO:
|
3953
|
-
};
|
3954
4512
|
}
|
3955
|
-
|
3956
|
-
|
3957
|
-
|
3958
|
-
|
4513
|
+
},
|
4514
|
+
//#region files
|
4515
|
+
"/files": {
|
4516
|
+
GET: async req => {
|
4517
|
+
const query = req.query;
|
4518
|
+
const filePath = req.path;
|
4519
|
+
// NOTE: no auth here since you would need the patch_id to get something that is not published.
|
4520
|
+
// For everything that is published, well they are already public so no auth required there...
|
4521
|
+
// We could imagine adding auth just to be a 200% certain,
|
4522
|
+
// 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, ...).
|
4523
|
+
// So: 1) patch ids are not possible to guess (but possible to brute force)
|
4524
|
+
// 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)
|
4525
|
+
// 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
|
4526
|
+
// Thus: attack surface + ease of attack + benefit = low probability of attack
|
4527
|
+
// If we couldn't argue that patch ids are secret enough, then this would be a problem.
|
4528
|
+
let cacheControl;
|
4529
|
+
let fileBuffer;
|
4530
|
+
let mimeType;
|
4531
|
+
if (query.patch_id) {
|
4532
|
+
fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
|
4533
|
+
mimeType = core.Internal.filenameToMimeType(filePath);
|
4534
|
+
cacheControl = "public, max-age=20000, immutable";
|
4535
|
+
} else {
|
4536
|
+
fileBuffer = await serverOps.getBinaryFile(filePath);
|
3959
4537
|
}
|
3960
|
-
|
3961
|
-
|
3962
|
-
|
3963
|
-
|
3964
|
-
|
3965
|
-
|
3966
|
-
|
3967
|
-
|
3968
|
-
|
3969
|
-
|
3970
|
-
|
3971
|
-
|
3972
|
-
|
3973
|
-
|
3974
|
-
|
3975
|
-
|
3976
|
-
|
3977
|
-
let fileBuffer;
|
3978
|
-
if (query.patch_id) {
|
3979
|
-
fileBuffer = await this.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
|
3980
|
-
} else {
|
3981
|
-
fileBuffer = await this.serverOps.getBinaryFile(filePath);
|
3982
|
-
}
|
3983
|
-
if (fileBuffer) {
|
3984
|
-
return {
|
3985
|
-
status: 200,
|
3986
|
-
body: bufferToReadableStream(fileBuffer)
|
3987
|
-
};
|
3988
|
-
} else {
|
3989
|
-
return {
|
3990
|
-
status: 404,
|
3991
|
-
json: {
|
3992
|
-
message: "File not found"
|
4538
|
+
if (fileBuffer) {
|
4539
|
+
return {
|
4540
|
+
status: 200,
|
4541
|
+
headers: {
|
4542
|
+
// TODO: we could use ETag and return 304 instead
|
4543
|
+
"Content-Type": mimeType || "application/octet-stream",
|
4544
|
+
"Cache-Control": cacheControl || "public, max-age=0, must-revalidate"
|
4545
|
+
},
|
4546
|
+
body: bufferToReadableStream(fileBuffer)
|
4547
|
+
};
|
4548
|
+
} else {
|
4549
|
+
return {
|
4550
|
+
status: 404,
|
4551
|
+
json: {
|
4552
|
+
message: "File not found"
|
4553
|
+
}
|
4554
|
+
};
|
3993
4555
|
}
|
3994
|
-
}
|
4556
|
+
}
|
3995
4557
|
}
|
4558
|
+
};
|
4559
|
+
};
|
4560
|
+
function formatPatchSourceError(error) {
|
4561
|
+
if ("message" in error) {
|
4562
|
+
return error.message;
|
4563
|
+
} else if (Array.isArray(error)) {
|
4564
|
+
return error.map(formatPatchSourceError).join("\n");
|
4565
|
+
} else {
|
4566
|
+
const _exhaustiveCheck = error;
|
4567
|
+
return "Unknown patch source error: " + JSON.stringify(_exhaustiveCheck);
|
3996
4568
|
}
|
3997
4569
|
}
|
3998
4570
|
function verifyCallbackReq(stateCookie, queryParams) {
|
@@ -4130,7 +4702,7 @@ async function withAuth(secret, cookies, errorMessageType, handler) {
|
|
4130
4702
|
status: 401,
|
4131
4703
|
json: {
|
4132
4704
|
message: "Session invalid or, most likely, expired. You will need to login again.",
|
4133
|
-
details: verification.error
|
4705
|
+
details: zodValidationError.fromError(verification.error).toString()
|
4134
4706
|
}
|
4135
4707
|
};
|
4136
4708
|
}
|
@@ -4290,14 +4862,14 @@ function guessMimeTypeFromPath(filePath) {
|
|
4290
4862
|
return null;
|
4291
4863
|
}
|
4292
4864
|
|
4293
|
-
async function createValServer(valModules, route, opts, callbacks, formatter) {
|
4294
|
-
const valServerConfig = await initHandlerOptions(route, opts);
|
4295
|
-
return
|
4865
|
+
async function createValServer(valModules, route, opts, config, callbacks, formatter) {
|
4866
|
+
const valServerConfig = await initHandlerOptions(route, opts, config);
|
4867
|
+
return ValServer(valModules, {
|
4296
4868
|
formatter,
|
4297
4869
|
...valServerConfig
|
4298
4870
|
}, callbacks);
|
4299
4871
|
}
|
4300
|
-
async function initHandlerOptions(route, opts) {
|
4872
|
+
async function initHandlerOptions(route, opts, config) {
|
4301
4873
|
const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
|
4302
4874
|
const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
|
4303
4875
|
const isProxyMode = opts.mode === "proxy" || opts.mode === undefined && (maybeApiKey || maybeValSecret);
|
@@ -4342,7 +4914,8 @@ async function initHandlerOptions(route, opts) {
|
|
4342
4914
|
valEnableRedirectUrl,
|
4343
4915
|
valDisableRedirectUrl,
|
4344
4916
|
valContentUrl,
|
4345
|
-
valBuildUrl
|
4917
|
+
valBuildUrl,
|
4918
|
+
config
|
4346
4919
|
};
|
4347
4920
|
} else {
|
4348
4921
|
const cwd = process.cwd();
|
@@ -4356,7 +4929,8 @@ async function initHandlerOptions(route, opts) {
|
|
4356
4929
|
valBuildUrl,
|
4357
4930
|
apiKey: maybeApiKey,
|
4358
4931
|
valSecret: maybeValSecret,
|
4359
|
-
project: maybeValProject
|
4932
|
+
project: maybeValProject,
|
4933
|
+
config
|
4360
4934
|
};
|
4361
4935
|
}
|
4362
4936
|
}
|
@@ -4417,17 +4991,9 @@ async function readCommit(gitDir, branchName) {
|
|
4417
4991
|
return undefined;
|
4418
4992
|
}
|
4419
4993
|
}
|
4420
|
-
const {
|
4421
|
-
VAL_SESSION_COOKIE,
|
4422
|
-
VAL_STATE_COOKIE
|
4423
|
-
} = core.Internal;
|
4424
|
-
const TREE_PATH_PREFIX = "/tree/~";
|
4425
|
-
const PATCHES_PATH_PREFIX = "/patches/~";
|
4426
|
-
const FILES_PATH_PREFIX = "/files";
|
4427
4994
|
function createValApiRouter(route, valServerPromise, convert) {
|
4428
4995
|
const uiRequestHandler = server.createUIRequestHandler();
|
4429
4996
|
return async req => {
|
4430
|
-
var _req$method;
|
4431
4997
|
const valServer = await valServerPromise;
|
4432
4998
|
const url = new URL(req.url);
|
4433
4999
|
if (!url.pathname.startsWith(route)) {
|
@@ -4441,115 +5007,235 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
4441
5007
|
json: error
|
4442
5008
|
});
|
4443
5009
|
}
|
4444
|
-
const
|
4445
|
-
|
4446
|
-
|
4447
|
-
|
4448
|
-
|
4449
|
-
|
4450
|
-
|
4451
|
-
|
4452
|
-
|
4453
|
-
|
4454
|
-
|
4455
|
-
|
5010
|
+
const path = url.pathname.slice(route.length);
|
5011
|
+
const groupQueryParams = arr => {
|
5012
|
+
const map = {};
|
5013
|
+
for (const [key, value] of arr) {
|
5014
|
+
const list = map[key] || [];
|
5015
|
+
list.push(value);
|
5016
|
+
map[key] = list;
|
5017
|
+
}
|
5018
|
+
return map;
|
5019
|
+
};
|
5020
|
+
async function getValServerResponse(reqApiRoutePath, req) {
|
5021
|
+
var _req$method, _anyApi$route, _anyValServer$route;
|
5022
|
+
const anyApi =
|
5023
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
5024
|
+
internal.Api;
|
5025
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
5026
|
+
const anyValServer = valServer;
|
5027
|
+
const method = (_req$method = req.method) === null || _req$method === void 0 ? void 0 : _req$method.toUpperCase();
|
5028
|
+
let route = null;
|
5029
|
+
let path = undefined;
|
5030
|
+
for (const routeDef of Object.keys(internal.Api)) {
|
5031
|
+
if (routeDef === reqApiRoutePath) {
|
5032
|
+
route = routeDef;
|
5033
|
+
break;
|
5034
|
+
}
|
5035
|
+
if (reqApiRoutePath.startsWith(routeDef)) {
|
5036
|
+
var _anyApi$routeDef;
|
5037
|
+
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;
|
5038
|
+
if (reqDefinition) {
|
5039
|
+
route = routeDef;
|
5040
|
+
if (reqDefinition.path) {
|
5041
|
+
const subPath = reqApiRoutePath.slice(routeDef.length);
|
5042
|
+
const pathRes = reqDefinition.path.safeParse(subPath);
|
5043
|
+
if (!pathRes.success) {
|
5044
|
+
return zodErrorResult(pathRes.error, `invalid path: '${subPath}' endpoint: '${routeDef}'`);
|
5045
|
+
} else {
|
5046
|
+
path = pathRes.data;
|
4456
5047
|
}
|
4457
|
-
}
|
5048
|
+
}
|
5049
|
+
break;
|
4458
5050
|
}
|
4459
|
-
|
4460
|
-
|
4461
|
-
|
4462
|
-
|
5051
|
+
}
|
5052
|
+
}
|
5053
|
+
if (!route) {
|
5054
|
+
return {
|
5055
|
+
status: 404,
|
5056
|
+
json: {
|
5057
|
+
message: "Route not found. Valid routes are: " + Object.keys(internal.Api),
|
5058
|
+
details: {
|
5059
|
+
route,
|
5060
|
+
method
|
4463
5061
|
}
|
4464
|
-
}
|
5062
|
+
}
|
5063
|
+
};
|
5064
|
+
}
|
5065
|
+
const apiEndpoint = anyApi === null || anyApi === void 0 || (_anyApi$route = anyApi[route]) === null || _anyApi$route === void 0 ? void 0 : _anyApi$route[method];
|
5066
|
+
const reqDefinition = apiEndpoint === null || apiEndpoint === void 0 ? void 0 : apiEndpoint.req;
|
5067
|
+
if (!reqDefinition) {
|
5068
|
+
return {
|
5069
|
+
status: 404,
|
5070
|
+
json: {
|
5071
|
+
message: `Requested method ${method} on route ${route} is not valid. Valid methods are: ${Object.keys(anyApi[route]).join(", ")}`,
|
5072
|
+
details: {
|
5073
|
+
route,
|
5074
|
+
method
|
5075
|
+
}
|
5076
|
+
}
|
5077
|
+
};
|
5078
|
+
}
|
5079
|
+
const endpointImpl = anyValServer === null || anyValServer === void 0 || (_anyValServer$route = anyValServer[route]) === null || _anyValServer$route === void 0 ? void 0 : _anyValServer$route[method];
|
5080
|
+
if (!endpointImpl) {
|
5081
|
+
return {
|
5082
|
+
status: 500,
|
5083
|
+
json: {
|
5084
|
+
message: "Missing server implementation of route with method. This might be caused by a mismatch between Val package versions.",
|
5085
|
+
details: {
|
5086
|
+
valid: {
|
5087
|
+
route: {
|
5088
|
+
server: Object.keys(anyValServer || {}),
|
5089
|
+
api: Object.keys(anyApi || {})
|
5090
|
+
},
|
5091
|
+
method: {
|
5092
|
+
server: Object.keys((anyValServer === null || anyValServer === void 0 ? void 0 : anyValServer[route]) || {}),
|
5093
|
+
api: Object.keys((anyApi === null || anyApi === void 0 ? void 0 : anyApi[route]) || {})
|
5094
|
+
}
|
5095
|
+
},
|
5096
|
+
route,
|
5097
|
+
method
|
5098
|
+
}
|
5099
|
+
}
|
5100
|
+
};
|
5101
|
+
}
|
5102
|
+
let bodyRes;
|
5103
|
+
try {
|
5104
|
+
bodyRes = reqDefinition.body ? reqDefinition.body.safeParse(await req.json()) : {
|
5105
|
+
success: true,
|
5106
|
+
data: {}
|
5107
|
+
};
|
5108
|
+
if (!bodyRes.success) {
|
5109
|
+
return zodErrorResult(bodyRes.error, "invalid body data");
|
4465
5110
|
}
|
5111
|
+
} catch (e) {
|
5112
|
+
return {
|
5113
|
+
status: 400,
|
5114
|
+
json: {
|
5115
|
+
message: "Could not parse request body",
|
5116
|
+
details: {
|
5117
|
+
error: JSON.stringify(e)
|
5118
|
+
}
|
5119
|
+
}
|
5120
|
+
};
|
5121
|
+
}
|
5122
|
+
const cookiesRes = reqDefinition.cookies ? getCookies(req, reqDefinition.cookies) : {
|
5123
|
+
success: true,
|
5124
|
+
data: {}
|
4466
5125
|
};
|
5126
|
+
if (!cookiesRes.success) {
|
5127
|
+
return zodErrorResult(cookiesRes.error, "invalid cookies");
|
5128
|
+
}
|
5129
|
+
const actualQueryParams = groupQueryParams(Array.from(url.searchParams.entries()));
|
5130
|
+
let query = {};
|
5131
|
+
if (reqDefinition.query) {
|
5132
|
+
// This is code is particularly heavy, however
|
5133
|
+
// @see ValidQueryParamTypes in ApiRouter.ts where we explain what we want to support
|
5134
|
+
// We prioritized a declarative ApiRouter, so this code is what we ended up with for better of worse
|
5135
|
+
const queryRules = {};
|
5136
|
+
for (const [key, zodRule] of Object.entries(reqDefinition.query)) {
|
5137
|
+
let innerType = zodRule;
|
5138
|
+
let isOptional = false;
|
5139
|
+
let isArray = false;
|
5140
|
+
// extract inner types:
|
5141
|
+
if (innerType instanceof z.z.ZodOptional) {
|
5142
|
+
isOptional = true;
|
5143
|
+
innerType = innerType.unwrap();
|
5144
|
+
}
|
5145
|
+
if (innerType instanceof z.z.ZodArray) {
|
5146
|
+
isArray = true;
|
5147
|
+
innerType = innerType.element;
|
5148
|
+
}
|
5149
|
+
// convert boolean to union of literals true and false so we can parse it as a string
|
5150
|
+
if (innerType instanceof z.z.ZodBoolean) {
|
5151
|
+
innerType = z.z.union([z.z.literal("true"), z.z.literal("false")]).transform(arg => arg === "true");
|
5152
|
+
}
|
5153
|
+
// re-build rules:
|
5154
|
+
let arrayCompatibleRule = innerType;
|
5155
|
+
arrayCompatibleRule = z.z.array(innerType); // we always want to parse an array because we group the query params by into an array
|
5156
|
+
if (isOptional) {
|
5157
|
+
arrayCompatibleRule = arrayCompatibleRule.optional();
|
5158
|
+
}
|
5159
|
+
if (!isArray) {
|
5160
|
+
arrayCompatibleRule = arrayCompatibleRule.transform(arg => arg && arg[0]);
|
5161
|
+
}
|
5162
|
+
queryRules[key] = arrayCompatibleRule;
|
5163
|
+
}
|
5164
|
+
const queryRes = z.z.object(queryRules).safeParse(actualQueryParams);
|
5165
|
+
if (!queryRes.success) {
|
5166
|
+
return zodErrorResult(queryRes.error, `invalid query params: (${JSON.stringify(actualQueryParams)})`);
|
5167
|
+
}
|
5168
|
+
query = queryRes.data;
|
5169
|
+
}
|
5170
|
+
const res = await endpointImpl({
|
5171
|
+
body: bodyRes.data,
|
5172
|
+
cookies: cookiesRes.data,
|
5173
|
+
query,
|
5174
|
+
path
|
5175
|
+
});
|
5176
|
+
if (res.status === 500) {
|
5177
|
+
var _res$json;
|
5178
|
+
return {
|
5179
|
+
status: 500,
|
5180
|
+
json: {
|
5181
|
+
message: ((_res$json = res.json) === null || _res$json === void 0 ? void 0 : _res$json.message) || "Internal Server Error"
|
5182
|
+
}
|
5183
|
+
};
|
5184
|
+
}
|
5185
|
+
const resDef = apiEndpoint.res;
|
5186
|
+
if (resDef) {
|
5187
|
+
const responseResult = resDef.safeParse(res);
|
5188
|
+
if (!responseResult.success) {
|
5189
|
+
return {
|
5190
|
+
status: 500,
|
5191
|
+
json: {
|
5192
|
+
message: "Could not validate response. This is likely a bug in Val server.",
|
5193
|
+
details: {
|
5194
|
+
response: res,
|
5195
|
+
errors: formatZodErrorString(responseResult.error)
|
5196
|
+
}
|
5197
|
+
}
|
5198
|
+
};
|
5199
|
+
}
|
5200
|
+
}
|
5201
|
+
return res;
|
4467
5202
|
}
|
4468
|
-
const path = url.pathname.slice(route.length);
|
4469
5203
|
if (path.startsWith("/static")) {
|
4470
5204
|
return convert(await uiRequestHandler(path.slice("/static".length), url.href));
|
4471
|
-
} else if (path === "/session") {
|
4472
|
-
return convert(await valServer.session(getCookies(req, [VAL_SESSION_COOKIE])));
|
4473
|
-
} else if (path === "/authorize") {
|
4474
|
-
return convert(await valServer.authorize({
|
4475
|
-
redirect_to: url.searchParams.get("redirect_to") || undefined
|
4476
|
-
}));
|
4477
|
-
} else if (path === "/callback") {
|
4478
|
-
return convert(await valServer.callback({
|
4479
|
-
code: url.searchParams.get("code") || undefined,
|
4480
|
-
state: url.searchParams.get("state") || undefined
|
4481
|
-
}, getCookies(req, [VAL_STATE_COOKIE])));
|
4482
|
-
} else if (path === "/logout") {
|
4483
|
-
return convert(await valServer.logout());
|
4484
|
-
} else if (path === "/enable") {
|
4485
|
-
return convert(await valServer.enable({
|
4486
|
-
redirect_to: url.searchParams.get("redirect_to") || undefined
|
4487
|
-
}));
|
4488
|
-
} else if (path === "/disable") {
|
4489
|
-
return convert(await valServer.disable({
|
4490
|
-
redirect_to: url.searchParams.get("redirect_to") || undefined
|
4491
|
-
}));
|
4492
|
-
} else if (method === "POST" && path === "/save") {
|
4493
|
-
const body = await req.json();
|
4494
|
-
return convert(await valServer.postSave(body, getCookies(req, [VAL_SESSION_COOKIE])));
|
4495
|
-
// } else if (method === "POST" && path === "/validate") {
|
4496
|
-
// const body = (await req.json()) as unknown;
|
4497
|
-
// return convert(
|
4498
|
-
// await valServer.postValidate(
|
4499
|
-
// body,
|
4500
|
-
// getCookies(req, [VAL_SESSION_COOKIE]),
|
4501
|
-
// requestHeaders
|
4502
|
-
// )
|
4503
|
-
// );
|
4504
|
-
} else if (method === "GET" && path === "/schema") {
|
4505
|
-
return convert(await valServer.getSchema(getCookies(req, [VAL_SESSION_COOKIE])));
|
4506
|
-
} else if (method === "PUT" && path.startsWith(TREE_PATH_PREFIX)) {
|
4507
|
-
return withTreePath(path, TREE_PATH_PREFIX)(async treePath => convert(await valServer.putTree(await req.json(), treePath, {
|
4508
|
-
patches_sha: url.searchParams.get("patches_sha") || undefined,
|
4509
|
-
validate_all: url.searchParams.get("validate_all") || undefined,
|
4510
|
-
validate_binary_files: url.searchParams.get("validate_binary_files") || undefined,
|
4511
|
-
validate_sources: url.searchParams.get("validate_sources") || undefined
|
4512
|
-
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
4513
|
-
} else if (method === "GET" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
4514
|
-
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.getPatches({
|
4515
|
-
authors: url.searchParams.getAll("author"),
|
4516
|
-
patchIds: url.searchParams.getAll("patch_id"),
|
4517
|
-
omitPatch: url.searchParams.get("omit_patch") || undefined
|
4518
|
-
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
4519
|
-
} else if (method === "DELETE" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
4520
|
-
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.deletePatches({
|
4521
|
-
id: url.searchParams.getAll("id")
|
4522
|
-
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
4523
|
-
} else if (method === "GET" && path.startsWith(FILES_PATH_PREFIX)) {
|
4524
|
-
const treePath = path.slice(FILES_PATH_PREFIX.length);
|
4525
|
-
return convert(await valServer.getFiles(treePath, {
|
4526
|
-
patch_id: url.searchParams.get("patch_id") || undefined
|
4527
|
-
}));
|
4528
5205
|
} else {
|
4529
|
-
return convert(
|
4530
|
-
|
4531
|
-
|
4532
|
-
|
4533
|
-
|
4534
|
-
|
4535
|
-
|
4536
|
-
|
4537
|
-
|
4538
|
-
|
5206
|
+
return convert(await getValServerResponse(path, req));
|
5207
|
+
}
|
5208
|
+
};
|
5209
|
+
}
|
5210
|
+
function formatZodErrorString(error) {
|
5211
|
+
const errors = zodValidationError.fromZodError(error).toString();
|
5212
|
+
return errors.length > 640 ? `${errors.slice(0, 640)}...` : errors;
|
5213
|
+
}
|
5214
|
+
function zodErrorResult(error, message) {
|
5215
|
+
return {
|
5216
|
+
status: 400,
|
5217
|
+
json: {
|
5218
|
+
message: "Bad Request: " + message,
|
5219
|
+
details: {
|
5220
|
+
errors: formatZodErrorString(error)
|
5221
|
+
}
|
4539
5222
|
}
|
4540
5223
|
};
|
4541
5224
|
}
|
4542
5225
|
|
4543
5226
|
// TODO: is this naive implementation is too naive?
|
4544
|
-
function getCookies(req,
|
4545
|
-
var _req$headers
|
4546
|
-
|
4547
|
-
|
4548
|
-
|
4549
|
-
|
4550
|
-
|
4551
|
-
|
4552
|
-
|
5227
|
+
function getCookies(req, cookiesDef) {
|
5228
|
+
var _req$headers;
|
5229
|
+
const input = {};
|
5230
|
+
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("; ");
|
5231
|
+
for (const name of Object.keys(cookiesDef)) {
|
5232
|
+
const cookie = cookieParts === null || cookieParts === void 0 ? void 0 : cookieParts.find(cookie => cookie.startsWith(`${name}=`));
|
5233
|
+
const value = cookie ? decodeURIComponent(cookie === null || cookie === void 0 ? void 0 : cookie.split("=")[1]) : undefined;
|
5234
|
+
if (value) {
|
5235
|
+
input[name.trim()] = value;
|
5236
|
+
}
|
5237
|
+
}
|
5238
|
+
return z.z.object(cookiesDef).safeParse(input);
|
4553
5239
|
}
|
4554
5240
|
|
4555
5241
|
/**
|
@@ -4592,6 +5278,49 @@ class ValFSHost {
|
|
4592
5278
|
}
|
4593
5279
|
}
|
4594
5280
|
|
5281
|
+
async function extractImageMetadata(filename, input) {
|
5282
|
+
const imageSize = sizeOf__default["default"](input);
|
5283
|
+
let mimeType = null;
|
5284
|
+
if (imageSize.type) {
|
5285
|
+
const possibleMimeType = `image/${imageSize.type}`;
|
5286
|
+
if (core.Internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
|
5287
|
+
mimeType = possibleMimeType;
|
5288
|
+
}
|
5289
|
+
const filenameBasedLookup = core.Internal.filenameToMimeType(filename);
|
5290
|
+
if (filenameBasedLookup) {
|
5291
|
+
mimeType = filenameBasedLookup;
|
5292
|
+
}
|
5293
|
+
}
|
5294
|
+
if (!mimeType) {
|
5295
|
+
mimeType = "application/octet-stream";
|
5296
|
+
}
|
5297
|
+
let {
|
5298
|
+
width,
|
5299
|
+
height
|
5300
|
+
} = imageSize;
|
5301
|
+
if (!width || !height) {
|
5302
|
+
width = 0;
|
5303
|
+
height = 0;
|
5304
|
+
}
|
5305
|
+
return {
|
5306
|
+
width,
|
5307
|
+
height,
|
5308
|
+
mimeType
|
5309
|
+
};
|
5310
|
+
}
|
5311
|
+
async function extractFileMetadata(filename,
|
5312
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
5313
|
+
_input // TODO: use buffer to determine mimetype
|
5314
|
+
) {
|
5315
|
+
let mimeType = core.Internal.filenameToMimeType(filename);
|
5316
|
+
if (!mimeType) {
|
5317
|
+
mimeType = "application/octet-stream";
|
5318
|
+
}
|
5319
|
+
return {
|
5320
|
+
mimeType
|
5321
|
+
};
|
5322
|
+
}
|
5323
|
+
|
4595
5324
|
function getValidationErrorFileRef(validationError) {
|
4596
5325
|
const maybeRef = validationError.value && typeof validationError.value === "object" && core.FILE_REF_PROP in validationError.value && typeof validationError.value[core.FILE_REF_PROP] === "string" ? validationError.value[core.FILE_REF_PROP] : undefined;
|
4597
5326
|
if (!maybeRef) {
|
@@ -4619,15 +5348,15 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4619
5348
|
throw Error("Cannot fix file without a file reference");
|
4620
5349
|
}
|
4621
5350
|
const filename = fsPath__namespace["default"].join(config.projectRoot, fileRef);
|
4622
|
-
|
4623
|
-
return extractFileMetadata(fileRef
|
5351
|
+
fs__default["default"].readFileSync(filename);
|
5352
|
+
return extractFileMetadata(fileRef);
|
4624
5353
|
}
|
4625
5354
|
const remainingErrors = [];
|
4626
5355
|
const patch$1 = [];
|
4627
5356
|
for (const fix of validationError.fixes || []) {
|
4628
5357
|
if (fix === "image:replace-metadata" || fix === "image:add-metadata") {
|
4629
5358
|
const imageMetadata = await getImageMetadata();
|
4630
|
-
if (imageMetadata.width === undefined || imageMetadata.height === undefined
|
5359
|
+
if (imageMetadata.width === undefined || imageMetadata.height === undefined) {
|
4631
5360
|
remainingErrors.push({
|
4632
5361
|
...validationError,
|
4633
5362
|
message: "Failed to get image metadata",
|
@@ -4638,8 +5367,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4638
5367
|
const metadataIsCorrect =
|
4639
5368
|
// metadata is a prop that is an object
|
4640
5369
|
typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
|
4641
|
-
// sha256 is correct
|
4642
|
-
"sha256" in currentValue.metadata && currentValue.metadata.sha256 === imageMetadata.sha256 &&
|
4643
5370
|
// width is correct
|
4644
5371
|
"width" in currentValue.metadata && currentValue.metadata.width === imageMetadata.width &&
|
4645
5372
|
// height is correct
|
@@ -4656,18 +5383,11 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4656
5383
|
value: {
|
4657
5384
|
width: imageMetadata.width,
|
4658
5385
|
height: imageMetadata.height,
|
4659
|
-
sha256: imageMetadata.sha256,
|
4660
5386
|
mimeType: imageMetadata.mimeType
|
4661
5387
|
}
|
4662
5388
|
});
|
4663
5389
|
} else {
|
4664
5390
|
if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
|
4665
|
-
if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== imageMetadata.sha256) {
|
4666
|
-
remainingErrors.push({
|
4667
|
-
message: "Image metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + imageMetadata.sha256 + ".",
|
4668
|
-
fixes: undefined
|
4669
|
-
});
|
4670
|
-
}
|
4671
5391
|
if (!("width" in currentValue.metadata) || currentValue.metadata.width !== imageMetadata.width) {
|
4672
5392
|
remainingErrors.push({
|
4673
5393
|
message: "Image metadata width is incorrect! Found: " + ("width" in currentValue.metadata ? currentValue.metadata.width : "<empty>") + ". Expected: " + imageMetadata.width,
|
@@ -4702,14 +5422,13 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4702
5422
|
value: {
|
4703
5423
|
width: imageMetadata.width,
|
4704
5424
|
height: imageMetadata.height,
|
4705
|
-
sha256: imageMetadata.sha256,
|
4706
5425
|
mimeType: imageMetadata.mimeType
|
4707
5426
|
}
|
4708
5427
|
});
|
4709
5428
|
}
|
4710
5429
|
} else if (fix === "file:add-metadata" || fix === "file:check-metadata") {
|
4711
5430
|
const fileMetadata = await getFileMetadata();
|
4712
|
-
if (fileMetadata
|
5431
|
+
if (fileMetadata === undefined) {
|
4713
5432
|
remainingErrors.push({
|
4714
5433
|
...validationError,
|
4715
5434
|
message: "Failed to get image metadata",
|
@@ -4720,8 +5439,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4720
5439
|
const metadataIsCorrect =
|
4721
5440
|
// metadata is a prop that is an object
|
4722
5441
|
typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
|
4723
|
-
// sha256 is correct
|
4724
|
-
"sha256" in currentValue.metadata && currentValue.metadata.sha256 === fileMetadata.sha256 &&
|
4725
5442
|
// mimeType is correct
|
4726
5443
|
"mimeType" in currentValue.metadata && currentValue.metadata.mimeType === fileMetadata.mimeType;
|
4727
5444
|
|
@@ -4732,7 +5449,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4732
5449
|
op: "replace",
|
4733
5450
|
path: patch.sourceToPatchPath(sourcePath).concat("metadata"),
|
4734
5451
|
value: {
|
4735
|
-
sha256: fileMetadata.sha256,
|
4736
5452
|
...(fileMetadata.mimeType ? {
|
4737
5453
|
mimeType: fileMetadata.mimeType
|
4738
5454
|
} : {})
|
@@ -4740,12 +5456,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4740
5456
|
});
|
4741
5457
|
} else {
|
4742
5458
|
if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
|
4743
|
-
if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== fileMetadata.sha256) {
|
4744
|
-
remainingErrors.push({
|
4745
|
-
message: "File metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + fileMetadata.sha256 + ".",
|
4746
|
-
fixes: undefined
|
4747
|
-
});
|
4748
|
-
}
|
4749
5459
|
if (!("mimeType" in currentValue.metadata) || currentValue.metadata.mimeType !== fileMetadata.mimeType) {
|
4750
5460
|
remainingErrors.push({
|
4751
5461
|
message: "File metadata mimeType is incorrect! Found: " + ("mimeType" in currentValue.metadata ? currentValue.metadata.mimeType : "<empty>") + ". Expected: " + fileMetadata.mimeType,
|
@@ -4766,7 +5476,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4766
5476
|
op: "add",
|
4767
5477
|
path: patch.sourceToPatchPath(sourcePath).concat("metadata"),
|
4768
5478
|
value: {
|
4769
|
-
sha256: fileMetadata.sha256,
|
4770
5479
|
...(fileMetadata.mimeType ? {
|
4771
5480
|
mimeType: fileMetadata.mimeType
|
4772
5481
|
} : {})
|