@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
@@ -8,12 +8,12 @@ import fsPath__default from 'path';
|
|
8
8
|
import fs, { promises } from 'fs';
|
9
9
|
import { transform } from 'sucrase';
|
10
10
|
import { VAL_CSS_PATH, VAL_APP_ID, VAL_OVERLAY_ID } from '@valbuild/ui';
|
11
|
+
import { VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE, VAL_SESSION_COOKIE, Api } from '@valbuild/shared/internal';
|
11
12
|
import { createUIRequestHandler } from '@valbuild/ui/server';
|
12
|
-
import { MIME_TYPES_TO_EXT, filenameToMimeType, VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE as VAL_STATE_COOKIE$1, VAL_SESSION_COOKIE as VAL_SESSION_COOKIE$1 } from '@valbuild/shared/internal';
|
13
13
|
import crypto$1 from 'crypto';
|
14
14
|
import z$1, { z } from 'zod';
|
15
15
|
import sizeOf from 'image-size';
|
16
|
-
import { fromError } from 'zod-validation-error';
|
16
|
+
import { fromError, fromZodError } from 'zod-validation-error';
|
17
17
|
|
18
18
|
class ValSyntaxError {
|
19
19
|
constructor(message, node) {
|
@@ -1243,56 +1243,6 @@ function encodeJwt(payload, sessionKey) {
|
|
1243
1243
|
return `${jwtHeaderBase64}.${payloadBase64}.${crypto$1.createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
|
1244
1244
|
}
|
1245
1245
|
|
1246
|
-
const textEncoder$2 = new TextEncoder();
|
1247
|
-
async function extractImageMetadata(filename, input) {
|
1248
|
-
const imageSize = sizeOf(input);
|
1249
|
-
let mimeType = null;
|
1250
|
-
if (imageSize.type) {
|
1251
|
-
const possibleMimeType = `image/${imageSize.type}`;
|
1252
|
-
if (MIME_TYPES_TO_EXT[possibleMimeType]) {
|
1253
|
-
mimeType = possibleMimeType;
|
1254
|
-
}
|
1255
|
-
const filenameBasedLookup = filenameToMimeType(filename);
|
1256
|
-
if (filenameBasedLookup) {
|
1257
|
-
mimeType = filenameBasedLookup;
|
1258
|
-
}
|
1259
|
-
}
|
1260
|
-
if (!mimeType) {
|
1261
|
-
mimeType = "application/octet-stream";
|
1262
|
-
}
|
1263
|
-
let {
|
1264
|
-
width,
|
1265
|
-
height
|
1266
|
-
} = imageSize;
|
1267
|
-
if (!width || !height) {
|
1268
|
-
width = 0;
|
1269
|
-
height = 0;
|
1270
|
-
}
|
1271
|
-
const sha256 = getSha256(mimeType, input);
|
1272
|
-
return {
|
1273
|
-
width,
|
1274
|
-
height,
|
1275
|
-
sha256,
|
1276
|
-
mimeType
|
1277
|
-
};
|
1278
|
-
}
|
1279
|
-
function getSha256(mimeType, input) {
|
1280
|
-
return Internal.getSHA256Hash(textEncoder$2.encode(
|
1281
|
-
// TODO: we should probably store the mimetype in the metadata and reuse it here
|
1282
|
-
`data:${mimeType};base64,${input.toString("base64")}`));
|
1283
|
-
}
|
1284
|
-
async function extractFileMetadata(filename, input) {
|
1285
|
-
let mimeType = filenameToMimeType(filename);
|
1286
|
-
if (!mimeType) {
|
1287
|
-
mimeType = "application/octet-stream";
|
1288
|
-
}
|
1289
|
-
const sha256 = getSha256(mimeType, input);
|
1290
|
-
return {
|
1291
|
-
sha256,
|
1292
|
-
mimeType
|
1293
|
-
};
|
1294
|
-
}
|
1295
|
-
|
1296
1246
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
1297
1247
|
const textEncoder$1 = new TextEncoder();
|
1298
1248
|
const jsonOps = new JSONOps();
|
@@ -1321,15 +1271,20 @@ class ValOps {
|
|
1321
1271
|
this.modulesErrors = null;
|
1322
1272
|
}
|
1323
1273
|
hash(input) {
|
1324
|
-
|
1325
|
-
if (typeof input === "string") {
|
1326
|
-
str = input;
|
1327
|
-
} else {
|
1328
|
-
str = JSON.stringify(input);
|
1329
|
-
}
|
1330
|
-
return Internal.getSHA256Hash(textEncoder$1.encode(str));
|
1274
|
+
return Internal.getSHA256Hash(textEncoder$1.encode(input));
|
1331
1275
|
}
|
1332
1276
|
|
1277
|
+
// #region stat
|
1278
|
+
/**
|
1279
|
+
* Get the status from Val
|
1280
|
+
*
|
1281
|
+
* This works differently in ValOpsFS and ValOpsHttp:
|
1282
|
+
* - 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).
|
1283
|
+
* - In ValOpsHttp (in production) it returns a WebSocket URL so that the client can connect directly.
|
1284
|
+
*
|
1285
|
+
* 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.
|
1286
|
+
*/
|
1287
|
+
|
1333
1288
|
// #region initTree
|
1334
1289
|
async initTree() {
|
1335
1290
|
if (this.baseSha === null || this.schemaSha === null || this.sources === null || this.schemas === null || this.modulesErrors === null) {
|
@@ -1386,17 +1341,25 @@ class ValOps {
|
|
1386
1341
|
addModuleError(`source in ${path} is undefined`, moduleIdx, path);
|
1387
1342
|
return;
|
1388
1343
|
}
|
1344
|
+
let serializedSchema;
|
1345
|
+
try {
|
1346
|
+
serializedSchema = schema.serialize();
|
1347
|
+
} catch (e) {
|
1348
|
+
const message = e instanceof Error ? e.message : JSON.stringify(e);
|
1349
|
+
addModuleError(`Could not serialize module: '${path}'. Error: ${message}`, moduleIdx, path);
|
1350
|
+
return;
|
1351
|
+
}
|
1389
1352
|
const pathM = path;
|
1390
1353
|
currentSources[pathM] = source;
|
1391
1354
|
currentSchemas[pathM] = schema;
|
1392
1355
|
// make sure the checks above is enough that this does not fail - even if val modules are not set up correctly
|
1393
|
-
baseSha
|
1356
|
+
baseSha = this.hash(baseSha + JSON.stringify({
|
1394
1357
|
path,
|
1395
|
-
schema:
|
1358
|
+
schema: serializedSchema,
|
1396
1359
|
source,
|
1397
1360
|
modulesErrors: currentModulesErrors
|
1398
|
-
});
|
1399
|
-
schemaSha
|
1361
|
+
}));
|
1362
|
+
schemaSha = this.hash(schemaSha + JSON.stringify(serializedSchema));
|
1400
1363
|
});
|
1401
1364
|
}
|
1402
1365
|
this.sources = currentSources;
|
@@ -1490,10 +1453,14 @@ class ValOps {
|
|
1490
1453
|
if (!errors[path]) {
|
1491
1454
|
errors[path] = [];
|
1492
1455
|
}
|
1493
|
-
errors[path].push({
|
1456
|
+
errors[path].push(...patches.map(({
|
1457
|
+
patchId
|
1458
|
+
}) => ({
|
1459
|
+
patchId,
|
1494
1460
|
invalidPath: true,
|
1461
|
+
skipped: true,
|
1495
1462
|
error: new PatchError(`Module at path: '${path}' not found`)
|
1496
|
-
});
|
1463
|
+
})));
|
1497
1464
|
}
|
1498
1465
|
patchedSources[path] = sources[path];
|
1499
1466
|
for (const {
|
@@ -1502,6 +1469,7 @@ class ValOps {
|
|
1502
1469
|
if (errors[path]) {
|
1503
1470
|
errors[path].push({
|
1504
1471
|
patchId: patchId,
|
1472
|
+
skipped: true,
|
1505
1473
|
error: new PatchError(`Cannot apply patch: previous errors exists`)
|
1506
1474
|
});
|
1507
1475
|
} else {
|
@@ -1509,6 +1477,7 @@ class ValOps {
|
|
1509
1477
|
if (!patchData) {
|
1510
1478
|
errors[path] = [{
|
1511
1479
|
patchId: patchId,
|
1480
|
+
skipped: false,
|
1512
1481
|
error: new PatchError(`Patch not found`)
|
1513
1482
|
}];
|
1514
1483
|
continue;
|
@@ -1539,6 +1508,7 @@ class ValOps {
|
|
1539
1508
|
}
|
1540
1509
|
errors[path].push({
|
1541
1510
|
patchId: patchId,
|
1511
|
+
skipped: false,
|
1542
1512
|
error: patchRes.error
|
1543
1513
|
});
|
1544
1514
|
} else {
|
@@ -1585,25 +1555,27 @@ class ValOps {
|
|
1585
1555
|
}
|
1586
1556
|
for (const [sourcePathS, validationErrors] of Object.entries(res)) {
|
1587
1557
|
const sourcePath = sourcePathS;
|
1588
|
-
|
1589
|
-
|
1590
|
-
if (
|
1591
|
-
|
1592
|
-
|
1593
|
-
|
1594
|
-
|
1595
|
-
|
1596
|
-
|
1597
|
-
|
1598
|
-
|
1599
|
-
errors[path]
|
1600
|
-
|
1601
|
-
|
1602
|
-
|
1603
|
-
|
1604
|
-
errors[path].validations[sourcePath]
|
1558
|
+
if (validationErrors) {
|
1559
|
+
for (const validationError of validationErrors) {
|
1560
|
+
if (isOnlyFileCheckValidationError(validationError)) {
|
1561
|
+
if (files[sourcePath]) {
|
1562
|
+
throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
|
1563
|
+
}
|
1564
|
+
const value = validationError.value;
|
1565
|
+
if (isFileSource(value)) {
|
1566
|
+
files[sourcePath] = value;
|
1567
|
+
}
|
1568
|
+
} else {
|
1569
|
+
if (!errors[path]) {
|
1570
|
+
errors[path] = {
|
1571
|
+
validations: {}
|
1572
|
+
};
|
1573
|
+
}
|
1574
|
+
if (!errors[path].validations[sourcePath]) {
|
1575
|
+
errors[path].validations[sourcePath] = [];
|
1576
|
+
}
|
1577
|
+
errors[path].validations[sourcePath].push(validationError);
|
1605
1578
|
}
|
1606
|
-
errors[path].validations[sourcePath].push(validationError);
|
1607
1579
|
}
|
1608
1580
|
}
|
1609
1581
|
}
|
@@ -1791,8 +1763,22 @@ class ValOps {
|
|
1791
1763
|
const patchRes = applyPatch(tsSourceFile, tsOps, sourceFileOps);
|
1792
1764
|
if (result.isErr(patchRes)) {
|
1793
1765
|
if (Array.isArray(patchRes.error)) {
|
1766
|
+
for (const error of patchRes.error) {
|
1767
|
+
console.error("Could not patch", JSON.stringify({
|
1768
|
+
path,
|
1769
|
+
patchId,
|
1770
|
+
error,
|
1771
|
+
sourceFileOps
|
1772
|
+
}, null, 2));
|
1773
|
+
}
|
1794
1774
|
errors.push(...patchRes.error);
|
1795
1775
|
} else {
|
1776
|
+
console.error("Could not patch", JSON.stringify({
|
1777
|
+
path,
|
1778
|
+
patchId,
|
1779
|
+
error: patchRes.error,
|
1780
|
+
sourceFileOps
|
1781
|
+
}, null, 2));
|
1796
1782
|
errors.push(patchRes.error);
|
1797
1783
|
}
|
1798
1784
|
triedPatches.push(patchId);
|
@@ -1886,16 +1872,23 @@ class ValOps {
|
|
1886
1872
|
}
|
1887
1873
|
|
1888
1874
|
// #region createPatch
|
1889
|
-
async createPatch(path, patch, authorId) {
|
1890
|
-
const
|
1891
|
-
|
1892
|
-
|
1893
|
-
|
1894
|
-
|
1875
|
+
async createPatch(path, patchAnalysis, patch, authorId) {
|
1876
|
+
const initTree = await this.initTree();
|
1877
|
+
const schemas = initTree.schemas;
|
1878
|
+
const moduleErrors = initTree.moduleErrors;
|
1879
|
+
let sources = initTree.sources;
|
1880
|
+
if (patchAnalysis) {
|
1881
|
+
const tree = await this.getTree(patchAnalysis);
|
1882
|
+
sources = {
|
1883
|
+
...sources,
|
1884
|
+
...tree.sources
|
1885
|
+
};
|
1886
|
+
}
|
1895
1887
|
const source = sources[path];
|
1896
1888
|
const schema = schemas[path];
|
1897
1889
|
const moduleError = moduleErrors.find(e => e.path === path);
|
1898
1890
|
if (moduleError) {
|
1891
|
+
console.error(`Cannot patch. Module at path: '${path}' has fatal errors: "${moduleError.message}"`);
|
1899
1892
|
return {
|
1900
1893
|
error: {
|
1901
1894
|
message: `Cannot patch. Module at path: '${path}' has fatal errors: ` + moduleErrors.map(m => `"${m.message}"`).join(" and ")
|
@@ -1903,6 +1896,7 @@ class ValOps {
|
|
1903
1896
|
};
|
1904
1897
|
}
|
1905
1898
|
if (!source) {
|
1899
|
+
console.error(`Cannot patch. Module source at path: '${path}' does not exist`);
|
1906
1900
|
return {
|
1907
1901
|
error: {
|
1908
1902
|
message: `Cannot patch. Module source at path: '${path}' does not exist`
|
@@ -1910,6 +1904,7 @@ class ValOps {
|
|
1910
1904
|
};
|
1911
1905
|
}
|
1912
1906
|
if (!schema) {
|
1907
|
+
console.error(`Cannot patch. Module schema at path: '${path}' does not exist`);
|
1913
1908
|
return {
|
1914
1909
|
error: {
|
1915
1910
|
message: `Cannot patch. Module schema at path: '${path}' does not exist`
|
@@ -1927,10 +1922,12 @@ class ValOps {
|
|
1927
1922
|
filePath
|
1928
1923
|
} = op;
|
1929
1924
|
if (files[filePath]) {
|
1925
|
+
console.error(`Cannot have multiple files with same path in same patch. Path: ${filePath}`);
|
1930
1926
|
files[filePath] = {
|
1931
1927
|
error: new PatchError("Cannot have multiple files with same path in same patch")
|
1932
1928
|
};
|
1933
1929
|
} else if (typeof value !== "string") {
|
1930
|
+
console.error(`Value is not a string. Path: ${filePath}. Value: ${value}`);
|
1934
1931
|
files[filePath] = {
|
1935
1932
|
error: new PatchError("Value is not a string")
|
1936
1933
|
};
|
@@ -1946,15 +1943,14 @@ class ValOps {
|
|
1946
1943
|
path: op.path,
|
1947
1944
|
filePath,
|
1948
1945
|
nestedFilePath: op.nestedFilePath,
|
1949
|
-
value:
|
1950
|
-
sha256
|
1951
|
-
}
|
1946
|
+
value: sha256
|
1952
1947
|
});
|
1953
1948
|
}
|
1954
1949
|
}
|
1955
1950
|
}
|
1956
1951
|
const saveRes = await this.saveSourceFilePatch(path, sourceFileOps, authorId);
|
1957
1952
|
if (saveRes.error) {
|
1953
|
+
console.error(`Could not save source file patch at path: '${path}'. Error: ${saveRes.error.message}`);
|
1958
1954
|
return {
|
1959
1955
|
error: saveRes.error
|
1960
1956
|
};
|
@@ -1978,17 +1974,20 @@ class ValOps {
|
|
1978
1974
|
? "image" : schemaAtPath instanceof FileSchema ? "file" : schemaAtPath.serialize().type;
|
1979
1975
|
} catch (e) {
|
1980
1976
|
if (e instanceof Error) {
|
1977
|
+
console.error(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`);
|
1981
1978
|
return {
|
1982
1979
|
filePath,
|
1983
1980
|
error: new PatchError(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`)
|
1984
1981
|
};
|
1985
1982
|
}
|
1983
|
+
console.error(`Could not resolve file type at: ${modulePath}. Unknown error.`);
|
1986
1984
|
return {
|
1987
1985
|
filePath,
|
1988
1986
|
error: new PatchError(`Could not resolve file type at: ${modulePath}. Unknown error.`)
|
1989
1987
|
};
|
1990
1988
|
}
|
1991
1989
|
if (type !== "image" && type !== "file") {
|
1990
|
+
console.error("Unknown file type (resolved from schema): " + type);
|
1992
1991
|
return {
|
1993
1992
|
filePath,
|
1994
1993
|
error: new PatchError("Unknown file type (resolved from schema): " + type)
|
@@ -1996,6 +1995,7 @@ class ValOps {
|
|
1996
1995
|
}
|
1997
1996
|
const mimeType = getMimeTypeFromBase64(data.value);
|
1998
1997
|
if (!mimeType) {
|
1998
|
+
console.error("Could not get mimeType from base 64 encoded value");
|
1999
1999
|
return {
|
2000
2000
|
filePath,
|
2001
2001
|
error: new PatchError("Could not get mimeType from base 64 encoded value. First chars were: " + data.value.slice(0, 20))
|
@@ -2003,6 +2003,7 @@ class ValOps {
|
|
2003
2003
|
}
|
2004
2004
|
const buffer = bufferFromDataUrl(data.value);
|
2005
2005
|
if (!buffer) {
|
2006
|
+
console.error("Could not create buffer from base 64 encoded value");
|
2006
2007
|
return {
|
2007
2008
|
filePath,
|
2008
2009
|
error: new PatchError("Could not create buffer from base 64 encoded value")
|
@@ -2010,6 +2011,7 @@ class ValOps {
|
|
2010
2011
|
}
|
2011
2012
|
const metadataOps = createMetadataFromBuffer(type, mimeType, buffer);
|
2012
2013
|
if (metadataOps.errors) {
|
2014
|
+
console.error(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`);
|
2013
2015
|
return {
|
2014
2016
|
filePath,
|
2015
2017
|
error: new PatchError(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`)
|
@@ -2031,6 +2033,14 @@ class ValOps {
|
|
2031
2033
|
};
|
2032
2034
|
}
|
2033
2035
|
}));
|
2036
|
+
const errors = saveFileRes.filter(f => !!f.error);
|
2037
|
+
if (errors.length > 0) {
|
2038
|
+
return {
|
2039
|
+
error: {
|
2040
|
+
message: "Could not save patch: " + errors.map(e => e.error.message).join(", ")
|
2041
|
+
}
|
2042
|
+
};
|
2043
|
+
}
|
2034
2044
|
return {
|
2035
2045
|
patchId,
|
2036
2046
|
files: saveFileRes,
|
@@ -2055,14 +2065,13 @@ function isFileSource(value) {
|
|
2055
2065
|
}
|
2056
2066
|
function getFieldsForType(type) {
|
2057
2067
|
if (type === "file") {
|
2058
|
-
return ["
|
2068
|
+
return ["mimeType"];
|
2059
2069
|
} else if (type === "image") {
|
2060
|
-
return ["
|
2070
|
+
return ["mimeType", "height", "width"];
|
2061
2071
|
}
|
2062
2072
|
throw new Error("Unknown type: " + type);
|
2063
2073
|
}
|
2064
2074
|
function createMetadataFromBuffer(type, mimeType, buffer) {
|
2065
|
-
const sha256 = getSha256(mimeType, buffer);
|
2066
2075
|
const errors = [];
|
2067
2076
|
let availableMetadata;
|
2068
2077
|
if (type === "image") {
|
@@ -2071,7 +2080,7 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
|
|
2071
2080
|
height,
|
2072
2081
|
type
|
2073
2082
|
} = sizeOf(buffer);
|
2074
|
-
const normalizedType = type === "jpg" ? "jpeg" : type;
|
2083
|
+
const normalizedType = type === "jpg" ? "jpeg" : type === "svg" ? "svg+xml" : type;
|
2075
2084
|
if (type !== undefined && `image/${normalizedType}` !== mimeType) {
|
2076
2085
|
return {
|
2077
2086
|
errors: [{
|
@@ -2080,14 +2089,12 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
|
|
2080
2089
|
};
|
2081
2090
|
}
|
2082
2091
|
availableMetadata = {
|
2083
|
-
sha256: sha256,
|
2084
2092
|
mimeType,
|
2085
2093
|
height,
|
2086
2094
|
width
|
2087
2095
|
};
|
2088
2096
|
} else {
|
2089
2097
|
availableMetadata = {
|
2090
|
-
sha256: sha256,
|
2091
2098
|
mimeType
|
2092
2099
|
};
|
2093
2100
|
}
|
@@ -2207,9 +2214,7 @@ const OperationT = z$1.discriminatedUnion("op", [z$1.object({
|
|
2207
2214
|
path: z$1.array(z$1.string()),
|
2208
2215
|
filePath: z$1.string(),
|
2209
2216
|
nestedFilePath: z$1.array(z$1.string()).optional(),
|
2210
|
-
value: z$1.
|
2211
|
-
sha256: z$1.string()
|
2212
|
-
})])
|
2217
|
+
value: z$1.string()
|
2213
2218
|
}).strict()]);
|
2214
2219
|
const Patch = z$1.array(OperationT);
|
2215
2220
|
|
@@ -2223,6 +2228,161 @@ class ValOpsFS extends ValOps {
|
|
2223
2228
|
async onInit() {
|
2224
2229
|
// do nothing
|
2225
2230
|
}
|
2231
|
+
async getStat(params) {
|
2232
|
+
// In ValOpsFS, we don't have a websocket server to listen to file changes so we use long-polling.
|
2233
|
+
// If a file that Val depends on changes, we break the connection and tell the client to request again to get the latest values.
|
2234
|
+
try {
|
2235
|
+
var _this$options, _this$options2;
|
2236
|
+
const currentBaseSha = await this.getBaseSha();
|
2237
|
+
const currentSchemaSha = await this.getSchemaSha();
|
2238
|
+
const moduleFilePaths = Object.keys(await this.getSchemas());
|
2239
|
+
const patchData = await this.readPatches();
|
2240
|
+
const patches = [];
|
2241
|
+
// TODO: use proper patch sequences when available:
|
2242
|
+
for (const [patchId] of Object.entries(patchData.patches).sort(([, a], [, b]) => {
|
2243
|
+
return a.createdAt.localeCompare(b.createdAt, undefined);
|
2244
|
+
})) {
|
2245
|
+
patches.push(patchId);
|
2246
|
+
}
|
2247
|
+
// something changed: return immediately
|
2248
|
+
const didChange = !params || currentBaseSha !== params.baseSha || currentSchemaSha !== params.schemaSha || patches.length !== params.patches.length || patches.some((p, i) => p !== params.patches[i]);
|
2249
|
+
if (didChange) {
|
2250
|
+
return {
|
2251
|
+
type: "did-change",
|
2252
|
+
baseSha: currentBaseSha,
|
2253
|
+
schemaSha: currentSchemaSha,
|
2254
|
+
patches
|
2255
|
+
};
|
2256
|
+
}
|
2257
|
+
let fsWatcher = null;
|
2258
|
+
let stopPolling = false;
|
2259
|
+
const didDirectoryChangeUsingPolling = (dir, interval, setHandle) => {
|
2260
|
+
const mtimeInDir = {};
|
2261
|
+
if (fs.existsSync(dir)) {
|
2262
|
+
for (const file of fs.readdirSync(dir)) {
|
2263
|
+
mtimeInDir[file] = fs.statSync(fsPath__default.join(dir, file)).mtime.getTime();
|
2264
|
+
}
|
2265
|
+
}
|
2266
|
+
return new Promise(resolve => {
|
2267
|
+
const go = resolve => {
|
2268
|
+
const start = Date.now();
|
2269
|
+
if (fs.existsSync(dir)) {
|
2270
|
+
const subDirs = fs.readdirSync(dir);
|
2271
|
+
// amount of files changed
|
2272
|
+
if (subDirs.length !== Object.keys(mtimeInDir).length) {
|
2273
|
+
resolve("request-again");
|
2274
|
+
}
|
2275
|
+
for (const file of fs.readdirSync(dir)) {
|
2276
|
+
const mtime = fs.statSync(fsPath__default.join(dir, file)).mtime.getTime();
|
2277
|
+
if (mtime !== mtimeInDir[file]) {
|
2278
|
+
resolve("request-again");
|
2279
|
+
}
|
2280
|
+
}
|
2281
|
+
} else {
|
2282
|
+
// dir had files, but now is deleted
|
2283
|
+
if (Object.keys(mtimeInDir).length > 0) {
|
2284
|
+
resolve("request-again");
|
2285
|
+
}
|
2286
|
+
}
|
2287
|
+
if (Date.now() - start > interval) {
|
2288
|
+
console.warn("Val: polling interval of patches exceeded");
|
2289
|
+
}
|
2290
|
+
if (stopPolling) {
|
2291
|
+
return;
|
2292
|
+
}
|
2293
|
+
setHandle(setTimeout(() => go(resolve), interval));
|
2294
|
+
};
|
2295
|
+
setHandle(setTimeout(() => go(resolve), interval));
|
2296
|
+
});
|
2297
|
+
};
|
2298
|
+
const didFilesChangeUsingPolling = (files, interval, setHandle) => {
|
2299
|
+
const mtimes = {};
|
2300
|
+
for (const file of files) {
|
2301
|
+
if (fs.existsSync(file)) {
|
2302
|
+
mtimes[file] = fs.statSync(file).mtime.getTime();
|
2303
|
+
} else {
|
2304
|
+
mtimes[file] = -1;
|
2305
|
+
}
|
2306
|
+
}
|
2307
|
+
return new Promise(resolve => {
|
2308
|
+
const go = resolve => {
|
2309
|
+
const start = Date.now();
|
2310
|
+
for (const file of files) {
|
2311
|
+
const mtime = fs.existsSync(file) ? fs.statSync(file).mtime.getTime() : -1;
|
2312
|
+
if (mtime !== mtimes[file]) {
|
2313
|
+
resolve("request-again");
|
2314
|
+
}
|
2315
|
+
}
|
2316
|
+
if (Date.now() - start > interval) {
|
2317
|
+
console.warn("Val: polling interval of files exceeded");
|
2318
|
+
}
|
2319
|
+
setHandle(setTimeout(() => go(resolve), interval));
|
2320
|
+
};
|
2321
|
+
if (stopPolling) {
|
2322
|
+
return;
|
2323
|
+
}
|
2324
|
+
setHandle(setTimeout(() => go(resolve), interval));
|
2325
|
+
});
|
2326
|
+
};
|
2327
|
+
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
|
2328
|
+
const disableFilePolling = ((_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.disableFilePolling) || false;
|
2329
|
+
let patchesDirHandle;
|
2330
|
+
let valFilesIntervalHandle;
|
2331
|
+
const type = await Promise.race([
|
2332
|
+
// 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
|
2333
|
+
disableFilePolling ? new Promise(() => {}) : didDirectoryChangeUsingPolling(this.getPatchesDir(), statFilePollingInterval, handle => {
|
2334
|
+
patchesDirHandle = handle;
|
2335
|
+
}),
|
2336
|
+
// we poll the files that Val depends on for changes
|
2337
|
+
disableFilePolling ? new Promise(() => {}) : didFilesChangeUsingPolling([fsPath__default.join(this.rootDir, "val.config.ts"), fsPath__default.join(this.rootDir, "val.modules.ts"), fsPath__default.join(this.rootDir, "val.config.js"), fsPath__default.join(this.rootDir, "val.modules.js"), ...moduleFilePaths.map(p => fsPath__default.join(this.rootDir, p))], statFilePollingInterval, handle => {
|
2338
|
+
valFilesIntervalHandle = handle;
|
2339
|
+
}), new Promise(resolve => {
|
2340
|
+
fsWatcher = fs.watch(this.rootDir, {
|
2341
|
+
recursive: true
|
2342
|
+
}, (eventType, filename) => {
|
2343
|
+
if (!filename) {
|
2344
|
+
return;
|
2345
|
+
}
|
2346
|
+
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");
|
2347
|
+
if (isChange) {
|
2348
|
+
// a file that Val depends on just changed or a patch was created, break connection and request stat again to get the new values
|
2349
|
+
resolve("request-again");
|
2350
|
+
}
|
2351
|
+
});
|
2352
|
+
}), new Promise(resolve => {
|
2353
|
+
var _this$options3;
|
2354
|
+
return setTimeout(() => resolve("no-change"), ((_this$options3 = this.options) === null || _this$options3 === void 0 ? void 0 : _this$options3.statPollingInterval) || 20000);
|
2355
|
+
})]).finally(() => {
|
2356
|
+
if (fsWatcher) {
|
2357
|
+
fsWatcher.close();
|
2358
|
+
}
|
2359
|
+
stopPolling = true;
|
2360
|
+
clearInterval(patchesDirHandle);
|
2361
|
+
clearInterval(valFilesIntervalHandle);
|
2362
|
+
});
|
2363
|
+
return {
|
2364
|
+
type,
|
2365
|
+
baseSha: currentBaseSha,
|
2366
|
+
schemaSha: currentSchemaSha,
|
2367
|
+
patches
|
2368
|
+
};
|
2369
|
+
} catch (err) {
|
2370
|
+
if (err instanceof Error) {
|
2371
|
+
return {
|
2372
|
+
type: "error",
|
2373
|
+
error: {
|
2374
|
+
message: err.message
|
2375
|
+
}
|
2376
|
+
};
|
2377
|
+
}
|
2378
|
+
return {
|
2379
|
+
type: "error",
|
2380
|
+
error: {
|
2381
|
+
message: "Unknown error (getStat)"
|
2382
|
+
}
|
2383
|
+
};
|
2384
|
+
}
|
2385
|
+
}
|
2226
2386
|
async readPatches(includes) {
|
2227
2387
|
const patchesCacheDir = this.getPatchesDir();
|
2228
2388
|
let patchJsonFiles = [];
|
@@ -2231,8 +2391,8 @@ class ValOpsFS extends ValOps {
|
|
2231
2391
|
}
|
2232
2392
|
const patches = {};
|
2233
2393
|
const errors = {};
|
2234
|
-
const
|
2235
|
-
for (const patchIdNum of
|
2394
|
+
const parsedPatchIds = patchJsonFiles.map(file => parseInt(fsPath__default.basename(fsPath__default.dirname(file)), 10)).sort();
|
2395
|
+
for (const patchIdNum of parsedPatchIds) {
|
2236
2396
|
if (Number.isNaN(patchIdNum)) {
|
2237
2397
|
throw new Error("Could not parse patch id from file name. Files found: " + patchJsonFiles.join(", "));
|
2238
2398
|
}
|
@@ -2273,6 +2433,12 @@ class ValOpsFS extends ValOps {
|
|
2273
2433
|
errors: allErrors,
|
2274
2434
|
patches: allPatches
|
2275
2435
|
} = await this.readPatches(filters.patchIds);
|
2436
|
+
if (allErrors && Object.keys(allErrors).length > 0) {
|
2437
|
+
for (const [patchId, error] of Object.entries(allErrors)) {
|
2438
|
+
console.error("Error reading patch", patchId, error);
|
2439
|
+
errors[patchId] = error;
|
2440
|
+
}
|
2441
|
+
}
|
2276
2442
|
for (const [patchIdS, patch] of Object.entries(allPatches)) {
|
2277
2443
|
const patchId = patchIdS;
|
2278
2444
|
if (filters.authors && !(patch.authorId === null || filters.authors.includes(patch.authorId))) {
|
@@ -2288,10 +2454,6 @@ class ValOpsFS extends ValOps {
|
|
2288
2454
|
authorId: patch.authorId,
|
2289
2455
|
appliedAt: patch.appliedAt
|
2290
2456
|
};
|
2291
|
-
const error = allErrors && allErrors[patchId];
|
2292
|
-
if (error) {
|
2293
|
-
errors[patchId] = error;
|
2294
|
-
}
|
2295
2457
|
}
|
2296
2458
|
if (errors && Object.keys(errors).length > 0) {
|
2297
2459
|
return {
|
@@ -2635,11 +2797,11 @@ class ValOpsFS extends ValOps {
|
|
2635
2797
|
getPatchDir(patchId) {
|
2636
2798
|
return fsPath__default.join(this.getPatchesDir(), patchId);
|
2637
2799
|
}
|
2638
|
-
getBinaryFilePath(
|
2639
|
-
return fsPath__default.join(this.getPatchDir(patchId), "files",
|
2800
|
+
getBinaryFilePath(filePath, patchId) {
|
2801
|
+
return fsPath__default.join(this.getPatchDir(patchId), "files", filePath, fsPath__default.basename(filePath));
|
2640
2802
|
}
|
2641
|
-
getBinaryFileMetadataPath(
|
2642
|
-
return fsPath__default.join(this.getPatchDir(patchId), "files",
|
2803
|
+
getBinaryFileMetadataPath(filePath, patchId) {
|
2804
|
+
return fsPath__default.join(this.getPatchDir(patchId), "files", filePath, "metadata.json");
|
2643
2805
|
}
|
2644
2806
|
getPatchFilePath(patchId) {
|
2645
2807
|
return fsPath__default.join(this.getPatchDir(patchId), "patch.json");
|
@@ -2712,12 +2874,10 @@ const BaseSha = z.string().refine(s => !!s); // TODO: validate
|
|
2712
2874
|
const AuthorId = z.string().refine(s => !!s); // TODO: validate
|
2713
2875
|
const ModuleFilePath = z.string().refine(s => !!s); // TODO: validate
|
2714
2876
|
const Metadata = z.union([z.object({
|
2715
|
-
sha256: z.string(),
|
2716
2877
|
mimeType: z.string(),
|
2717
2878
|
width: z.number(),
|
2718
2879
|
height: z.number()
|
2719
2880
|
}), z.object({
|
2720
|
-
sha256: z.string(),
|
2721
2881
|
mimeType: z.string()
|
2722
2882
|
})]);
|
2723
2883
|
const MetadataRes = z.object({
|
@@ -2738,7 +2898,7 @@ const BasePatchResponse = z.object({
|
|
2738
2898
|
});
|
2739
2899
|
const GetPatches = z.object({
|
2740
2900
|
patches: z.array(z.intersection(z.object({
|
2741
|
-
patch: Patch
|
2901
|
+
patch: Patch.optional()
|
2742
2902
|
}), BasePatchResponse)),
|
2743
2903
|
errors: z.array(z.object({
|
2744
2904
|
patchId: PatchId.optional(),
|
@@ -2789,7 +2949,9 @@ const CommitResponse = z.object({
|
|
2789
2949
|
branch: z.string()
|
2790
2950
|
});
|
2791
2951
|
class ValOpsHttp extends ValOps {
|
2792
|
-
constructor(hostUrl, project, commitSha,
|
2952
|
+
constructor(hostUrl, project, commitSha,
|
2953
|
+
// TODO: CommitSha
|
2954
|
+
branch, apiKey, valModules, options) {
|
2793
2955
|
super(valModules, options);
|
2794
2956
|
this.hostUrl = hostUrl;
|
2795
2957
|
this.project = project;
|
@@ -2803,7 +2965,150 @@ class ValOpsHttp extends ValOps {
|
|
2803
2965
|
async onInit() {
|
2804
2966
|
// TODO: unused for now. Implement or remove
|
2805
2967
|
}
|
2968
|
+
async getStat(params) {
|
2969
|
+
if (!(params !== null && params !== void 0 && params.profileId)) {
|
2970
|
+
return {
|
2971
|
+
type: "error",
|
2972
|
+
error: {
|
2973
|
+
message: "No profileId provided"
|
2974
|
+
}
|
2975
|
+
};
|
2976
|
+
}
|
2977
|
+
const currentBaseSha = await this.getBaseSha();
|
2978
|
+
const currentSchemaSha = await this.getSchemaSha();
|
2979
|
+
const patchData = await this.fetchPatches({
|
2980
|
+
omitPatch: true,
|
2981
|
+
authors: undefined,
|
2982
|
+
patchIds: undefined,
|
2983
|
+
moduleFilePaths: undefined
|
2984
|
+
});
|
2985
|
+
const patches = [];
|
2986
|
+
// TODO: use proper patch sequences when available:
|
2987
|
+
for (const [patchId] of Object.entries(patchData.patches).sort(([, a], [, b]) => {
|
2988
|
+
return a.createdAt.localeCompare(b.createdAt, undefined);
|
2989
|
+
})) {
|
2990
|
+
patches.push(patchId);
|
2991
|
+
}
|
2992
|
+
const webSocketNonceRes = await this.getWebSocketNonce(params.profileId);
|
2993
|
+
if (webSocketNonceRes.status === "error") {
|
2994
|
+
return {
|
2995
|
+
type: "error",
|
2996
|
+
error: webSocketNonceRes.error
|
2997
|
+
};
|
2998
|
+
}
|
2999
|
+
const {
|
3000
|
+
nonce,
|
3001
|
+
url
|
3002
|
+
} = webSocketNonceRes.data;
|
3003
|
+
return {
|
3004
|
+
type: "use-websocket",
|
3005
|
+
url,
|
3006
|
+
nonce,
|
3007
|
+
baseSha: currentBaseSha,
|
3008
|
+
schemaSha: currentSchemaSha,
|
3009
|
+
patches,
|
3010
|
+
commitSha: this.commitSha
|
3011
|
+
};
|
3012
|
+
}
|
3013
|
+
async getWebSocketNonce(profileId) {
|
3014
|
+
return fetch(`${this.hostUrl}/v1/${this.project}/websocket/nonces`, {
|
3015
|
+
method: "POST",
|
3016
|
+
body: JSON.stringify({
|
3017
|
+
branch: this.branch,
|
3018
|
+
profileId
|
3019
|
+
}),
|
3020
|
+
headers: {
|
3021
|
+
...this.authHeaders,
|
3022
|
+
"Content-Type": "application/json"
|
3023
|
+
}
|
3024
|
+
}).then(async res => {
|
3025
|
+
if (res.ok) {
|
3026
|
+
const json = await res.json();
|
3027
|
+
if (typeof json.nonce !== "string" || typeof json.url !== "string") {
|
3028
|
+
return {
|
3029
|
+
status: "error",
|
3030
|
+
error: {
|
3031
|
+
message: "Invalid nonce response: " + JSON.stringify(json)
|
3032
|
+
}
|
3033
|
+
};
|
3034
|
+
}
|
3035
|
+
if (!json.url.startsWith("ws://") && !json.url.startsWith("wss://")) {
|
3036
|
+
return {
|
3037
|
+
status: "error",
|
3038
|
+
error: {
|
3039
|
+
message: "Invalid websocket url: " + json.url
|
3040
|
+
}
|
3041
|
+
};
|
3042
|
+
}
|
3043
|
+
return {
|
3044
|
+
status: "success",
|
3045
|
+
data: {
|
3046
|
+
nonce: json.nonce,
|
3047
|
+
url: json.url
|
3048
|
+
}
|
3049
|
+
};
|
3050
|
+
}
|
3051
|
+
return {
|
3052
|
+
status: "error",
|
3053
|
+
error: {
|
3054
|
+
message: "Could not get nonce. HTTP error: " + res.status + " " + res.statusText
|
3055
|
+
}
|
3056
|
+
};
|
3057
|
+
}).catch(e => {
|
3058
|
+
console.error("Could not get nonce (connection error?):", e instanceof Error ? e.message : e.toString());
|
3059
|
+
return {
|
3060
|
+
status: "error",
|
3061
|
+
error: {
|
3062
|
+
message: "Could not get nonce. Error: " + (e instanceof Error ? e.message : e.toString())
|
3063
|
+
}
|
3064
|
+
};
|
3065
|
+
});
|
3066
|
+
}
|
2806
3067
|
async fetchPatches(filters) {
|
3068
|
+
// Split patchIds into chunks to avoid too long query strings
|
3069
|
+
// NOTE: fetching patches results are cached, so this should reduce the pressure on the server
|
3070
|
+
const chunkSize = 100;
|
3071
|
+
const patchIds = filters.patchIds || [];
|
3072
|
+
const patchIdChunks = [];
|
3073
|
+
for (let i = 0; i < patchIds.length; i += chunkSize) {
|
3074
|
+
patchIdChunks.push(patchIds.slice(i, i + chunkSize));
|
3075
|
+
}
|
3076
|
+
let allPatches = {};
|
3077
|
+
let allErrors = {};
|
3078
|
+
if (patchIds === undefined || patchIds.length === 0) {
|
3079
|
+
return this.fetchPatchesInternal({
|
3080
|
+
patchIds: patchIds,
|
3081
|
+
authors: filters.authors,
|
3082
|
+
moduleFilePaths: filters.moduleFilePaths,
|
3083
|
+
omitPatch: filters.omitPatch
|
3084
|
+
});
|
3085
|
+
}
|
3086
|
+
for (const res of await Promise.all(patchIdChunks.map(patchIdChunk => this.fetchPatchesInternal({
|
3087
|
+
patchIds: patchIdChunk,
|
3088
|
+
authors: filters.authors,
|
3089
|
+
moduleFilePaths: filters.moduleFilePaths,
|
3090
|
+
omitPatch: filters.omitPatch
|
3091
|
+
})))) {
|
3092
|
+
if ("error" in res) {
|
3093
|
+
return res;
|
3094
|
+
}
|
3095
|
+
allPatches = {
|
3096
|
+
...allPatches,
|
3097
|
+
...res.patches
|
3098
|
+
};
|
3099
|
+
if (res.errors) {
|
3100
|
+
allErrors = {
|
3101
|
+
...allErrors,
|
3102
|
+
...res.errors
|
3103
|
+
};
|
3104
|
+
}
|
3105
|
+
}
|
3106
|
+
return {
|
3107
|
+
patches: allPatches,
|
3108
|
+
errors: Object.keys(allErrors).length > 0 ? allErrors : undefined
|
3109
|
+
};
|
3110
|
+
}
|
3111
|
+
async fetchPatchesInternal(filters) {
|
2807
3112
|
const params = [];
|
2808
3113
|
params.push(["branch", this.branch]);
|
2809
3114
|
if (filters.patchIds) {
|
@@ -3052,7 +3357,7 @@ class ValOpsHttp extends ValOps {
|
|
3052
3357
|
if (!file) {
|
3053
3358
|
return null;
|
3054
3359
|
}
|
3055
|
-
return
|
3360
|
+
return bufferFromDataUrl(file.value) ?? null;
|
3056
3361
|
}
|
3057
3362
|
async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
|
3058
3363
|
const params = new URLSearchParams();
|
@@ -3160,6 +3465,7 @@ class ValOpsHttp extends ValOps {
|
|
3160
3465
|
}
|
3161
3466
|
async commit(prepared, message, committer, newBranch) {
|
3162
3467
|
try {
|
3468
|
+
var _res$headers$get;
|
3163
3469
|
const existingBranch = this.branch;
|
3164
3470
|
const res = await fetch(`${this.hostUrl}/v1/${this.project}/commit`, {
|
3165
3471
|
method: "POST",
|
@@ -3195,6 +3501,22 @@ class ValOpsHttp extends ValOps {
|
|
3195
3501
|
}
|
3196
3502
|
};
|
3197
3503
|
}
|
3504
|
+
if ((_res$headers$get = res.headers.get("Content-Type")) !== null && _res$headers$get !== void 0 && _res$headers$get.includes("application/json")) {
|
3505
|
+
const json = await res.json();
|
3506
|
+
if (json.isNotFastForward) {
|
3507
|
+
return {
|
3508
|
+
isNotFastForward: true,
|
3509
|
+
error: {
|
3510
|
+
message: "Could not commit. Not a fast-forward commit"
|
3511
|
+
}
|
3512
|
+
};
|
3513
|
+
}
|
3514
|
+
return {
|
3515
|
+
error: {
|
3516
|
+
message: json.message
|
3517
|
+
}
|
3518
|
+
};
|
3519
|
+
}
|
3198
3520
|
return {
|
3199
3521
|
error: {
|
3200
3522
|
message: "Could not commit. HTTP error: " + res.status + " " + res.statusText
|
@@ -3211,251 +3533,61 @@ class ValOpsHttp extends ValOps {
|
|
3211
3533
|
}
|
3212
3534
|
|
3213
3535
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
3214
|
-
|
3215
|
-
|
3216
|
-
|
3217
|
-
|
3218
|
-
|
3219
|
-
|
3220
|
-
|
3221
|
-
|
3222
|
-
|
3223
|
-
|
3224
|
-
|
3225
|
-
|
3226
|
-
|
3227
|
-
|
3228
|
-
|
3229
|
-
|
3230
|
-
throw new Error("Invalid mode: " + (options === null || options === void 0 ? void 0 : options.mode));
|
3231
|
-
}
|
3536
|
+
const ValServer = (valModules, options, callbacks) => {
|
3537
|
+
let serverOps;
|
3538
|
+
if (options.mode === "fs") {
|
3539
|
+
serverOps = new ValOpsFS(options.cwd, valModules, {
|
3540
|
+
formatter: options.formatter,
|
3541
|
+
config: options.config
|
3542
|
+
});
|
3543
|
+
} else if (options.mode === "http") {
|
3544
|
+
serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
|
3545
|
+
formatter: options.formatter,
|
3546
|
+
root: options.root,
|
3547
|
+
config: options.config
|
3548
|
+
});
|
3549
|
+
} else {
|
3550
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
3551
|
+
throw new Error("Invalid mode: " + (options === null || options === void 0 ? void 0 : options.mode));
|
3232
3552
|
}
|
3233
|
-
|
3234
|
-
|
3235
|
-
|
3236
|
-
const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
|
3237
|
-
if (typeof redirectToRes !== "string") {
|
3238
|
-
return redirectToRes;
|
3553
|
+
const getAuthorizeUrl = (publicValApiRe, token) => {
|
3554
|
+
if (!options.project) {
|
3555
|
+
throw new Error("Project is not set");
|
3239
3556
|
}
|
3240
|
-
|
3241
|
-
|
3242
|
-
cookies: {
|
3243
|
-
[VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
|
3244
|
-
},
|
3245
|
-
status: 302,
|
3246
|
-
redirectTo: redirectToRes
|
3247
|
-
};
|
3248
|
-
}
|
3249
|
-
async disable(query) {
|
3250
|
-
const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
|
3251
|
-
if (typeof redirectToRes !== "string") {
|
3252
|
-
return redirectToRes;
|
3557
|
+
if (!options.valBuildUrl) {
|
3558
|
+
throw new Error("Val build url is not set");
|
3253
3559
|
}
|
3254
|
-
|
3255
|
-
|
3256
|
-
|
3257
|
-
|
3258
|
-
|
3259
|
-
|
3260
|
-
|
3261
|
-
|
3262
|
-
redirectTo: redirectToRes
|
3263
|
-
};
|
3264
|
-
}
|
3265
|
-
async authorize(query) {
|
3266
|
-
if (typeof query.redirect_to !== "string") {
|
3267
|
-
return {
|
3268
|
-
status: 400,
|
3269
|
-
json: {
|
3270
|
-
message: "Missing redirect_to query param"
|
3271
|
-
}
|
3272
|
-
};
|
3560
|
+
const url = new URL(`/auth/${options.project}/authorize`, options.valBuildUrl);
|
3561
|
+
url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRe}/callback`));
|
3562
|
+
url.searchParams.set("state", token);
|
3563
|
+
return url.toString();
|
3564
|
+
};
|
3565
|
+
const getAppErrorUrl = error => {
|
3566
|
+
if (!options.project) {
|
3567
|
+
throw new Error("Project is not set");
|
3273
3568
|
}
|
3274
|
-
|
3275
|
-
|
3276
|
-
const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
|
3277
|
-
await this.callbacks.onEnable(true);
|
3278
|
-
return {
|
3279
|
-
cookies: {
|
3280
|
-
[VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
|
3281
|
-
[VAL_STATE_COOKIE$1]: {
|
3282
|
-
value: createStateCookie({
|
3283
|
-
redirect_to: query.redirect_to,
|
3284
|
-
token
|
3285
|
-
}),
|
3286
|
-
options: {
|
3287
|
-
httpOnly: true,
|
3288
|
-
sameSite: "lax",
|
3289
|
-
expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
|
3290
|
-
}
|
3291
|
-
}
|
3292
|
-
},
|
3293
|
-
status: 302,
|
3294
|
-
redirectTo: appAuthorizeUrl
|
3295
|
-
};
|
3296
|
-
}
|
3297
|
-
async callback(query, cookies) {
|
3298
|
-
if (!this.options.project) {
|
3299
|
-
return {
|
3300
|
-
status: 302,
|
3301
|
-
cookies: {
|
3302
|
-
[VAL_STATE_COOKIE$1]: {
|
3303
|
-
value: null
|
3304
|
-
}
|
3305
|
-
},
|
3306
|
-
redirectTo: this.getAppErrorUrl("Project is not set")
|
3307
|
-
};
|
3308
|
-
}
|
3309
|
-
if (!this.options.valSecret) {
|
3310
|
-
return {
|
3311
|
-
status: 302,
|
3312
|
-
cookies: {
|
3313
|
-
[VAL_STATE_COOKIE$1]: {
|
3314
|
-
value: null
|
3315
|
-
}
|
3316
|
-
},
|
3317
|
-
redirectTo: this.getAppErrorUrl("Secret is not set")
|
3318
|
-
};
|
3319
|
-
}
|
3320
|
-
const {
|
3321
|
-
success: callbackReqSuccess,
|
3322
|
-
error: callbackReqError
|
3323
|
-
} = verifyCallbackReq(cookies[VAL_STATE_COOKIE$1], query);
|
3324
|
-
if (callbackReqError !== null) {
|
3325
|
-
return {
|
3326
|
-
status: 302,
|
3327
|
-
cookies: {
|
3328
|
-
[VAL_STATE_COOKIE$1]: {
|
3329
|
-
value: null
|
3330
|
-
}
|
3331
|
-
},
|
3332
|
-
redirectTo: this.getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`)
|
3333
|
-
};
|
3334
|
-
}
|
3335
|
-
const data = await this.consumeCode(callbackReqSuccess.code);
|
3336
|
-
if (data === null) {
|
3337
|
-
return {
|
3338
|
-
status: 302,
|
3339
|
-
cookies: {
|
3340
|
-
[VAL_STATE_COOKIE$1]: {
|
3341
|
-
value: null
|
3342
|
-
}
|
3343
|
-
},
|
3344
|
-
redirectTo: this.getAppErrorUrl("Failed to exchange code for user")
|
3345
|
-
};
|
3346
|
-
}
|
3347
|
-
const exp = getExpire();
|
3348
|
-
const valSecret = this.options.valSecret;
|
3349
|
-
if (!valSecret) {
|
3350
|
-
return {
|
3351
|
-
status: 302,
|
3352
|
-
cookies: {
|
3353
|
-
[VAL_STATE_COOKIE$1]: {
|
3354
|
-
value: null
|
3355
|
-
}
|
3356
|
-
},
|
3357
|
-
redirectTo: this.getAppErrorUrl("Setup is not correct: secret is missing")
|
3358
|
-
};
|
3359
|
-
}
|
3360
|
-
const cookie = encodeJwt({
|
3361
|
-
...data,
|
3362
|
-
exp // this is the client side exp
|
3363
|
-
}, valSecret);
|
3364
|
-
return {
|
3365
|
-
status: 302,
|
3366
|
-
cookies: {
|
3367
|
-
[VAL_STATE_COOKIE$1]: {
|
3368
|
-
value: null
|
3369
|
-
},
|
3370
|
-
[VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
|
3371
|
-
[VAL_SESSION_COOKIE$1]: {
|
3372
|
-
value: cookie,
|
3373
|
-
options: {
|
3374
|
-
httpOnly: true,
|
3375
|
-
sameSite: "strict",
|
3376
|
-
path: "/",
|
3377
|
-
secure: true,
|
3378
|
-
expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
|
3379
|
-
}
|
3380
|
-
}
|
3381
|
-
},
|
3382
|
-
redirectTo: callbackReqSuccess.redirect_uri || "/"
|
3383
|
-
};
|
3384
|
-
}
|
3385
|
-
async session(cookies) {
|
3386
|
-
if (this.serverOps instanceof ValOpsFS) {
|
3387
|
-
return {
|
3388
|
-
status: 200,
|
3389
|
-
json: {
|
3390
|
-
mode: "local",
|
3391
|
-
enabled: await this.callbacks.isEnabled()
|
3392
|
-
}
|
3393
|
-
};
|
3394
|
-
}
|
3395
|
-
if (!this.options.project) {
|
3396
|
-
return {
|
3397
|
-
status: 500,
|
3398
|
-
json: {
|
3399
|
-
message: "Project is not set"
|
3400
|
-
}
|
3401
|
-
};
|
3402
|
-
}
|
3403
|
-
if (!this.options.valSecret) {
|
3404
|
-
return {
|
3405
|
-
status: 500,
|
3406
|
-
json: {
|
3407
|
-
message: "Secret is not set"
|
3408
|
-
}
|
3409
|
-
};
|
3569
|
+
if (!options.valBuildUrl) {
|
3570
|
+
throw new Error("Val build url is not set");
|
3410
3571
|
}
|
3411
|
-
|
3412
|
-
|
3413
|
-
|
3414
|
-
|
3415
|
-
|
3416
|
-
|
3417
|
-
}
|
3418
|
-
};
|
3419
|
-
}
|
3420
|
-
const url = new URL(`/api/val/${this.options.project}/auth/session`, this.options.valBuildUrl);
|
3421
|
-
const fetchRes = await fetch(url, {
|
3422
|
-
headers: getAuthHeaders(data.token, "application/json")
|
3423
|
-
});
|
3424
|
-
if (fetchRes.status === 200) {
|
3425
|
-
return {
|
3426
|
-
status: fetchRes.status,
|
3427
|
-
json: {
|
3428
|
-
mode: "proxy",
|
3429
|
-
enabled: await this.callbacks.isEnabled(),
|
3430
|
-
...(await fetchRes.json())
|
3431
|
-
}
|
3432
|
-
};
|
3433
|
-
} else {
|
3434
|
-
return {
|
3435
|
-
status: fetchRes.status,
|
3436
|
-
json: {
|
3437
|
-
message: "Failed to authorize",
|
3438
|
-
...(await fetchRes.json())
|
3439
|
-
}
|
3440
|
-
};
|
3441
|
-
}
|
3442
|
-
});
|
3443
|
-
}
|
3444
|
-
async consumeCode(code) {
|
3445
|
-
if (!this.options.project) {
|
3572
|
+
const url = new URL(`/auth/${options.project}/authorize`, options.valBuildUrl);
|
3573
|
+
url.searchParams.set("error", encodeURIComponent(error));
|
3574
|
+
return url.toString();
|
3575
|
+
};
|
3576
|
+
const consumeCode = async code => {
|
3577
|
+
if (!options.project) {
|
3446
3578
|
throw new Error("Project is not set");
|
3447
3579
|
}
|
3448
|
-
if (!
|
3580
|
+
if (!options.valBuildUrl) {
|
3449
3581
|
throw new Error("Val build url is not set");
|
3450
3582
|
}
|
3451
|
-
const url = new URL(`/api/val/${
|
3583
|
+
const url = new URL(`/api/val/${options.project}/auth/token`, options.valBuildUrl);
|
3452
3584
|
url.searchParams.set("code", encodeURIComponent(code));
|
3453
|
-
if (!
|
3585
|
+
if (!options.apiKey) {
|
3454
3586
|
return null;
|
3455
3587
|
}
|
3456
3588
|
return fetch(url, {
|
3457
3589
|
method: "POST",
|
3458
|
-
headers: getAuthHeaders(
|
3590
|
+
headers: getAuthHeaders(options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
|
3459
3591
|
}).then(async res => {
|
3460
3592
|
if (res.status === 200) {
|
3461
3593
|
const token = await res.text();
|
@@ -3475,34 +3607,11 @@ class ValServer {
|
|
3475
3607
|
console.debug("Failed to get user from code: ", err);
|
3476
3608
|
return null;
|
3477
3609
|
});
|
3478
|
-
}
|
3479
|
-
|
3480
|
-
|
3481
|
-
|
3482
|
-
|
3483
|
-
if (!this.options.valBuildUrl) {
|
3484
|
-
throw new Error("Val build url is not set");
|
3485
|
-
}
|
3486
|
-
const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
|
3487
|
-
url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRe}/callback`));
|
3488
|
-
url.searchParams.set("state", token);
|
3489
|
-
return url.toString();
|
3490
|
-
}
|
3491
|
-
getAppErrorUrl(error) {
|
3492
|
-
if (!this.options.project) {
|
3493
|
-
throw new Error("Project is not set");
|
3494
|
-
}
|
3495
|
-
if (!this.options.valBuildUrl) {
|
3496
|
-
throw new Error("Val build url is not set");
|
3497
|
-
}
|
3498
|
-
const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
|
3499
|
-
url.searchParams.set("error", encodeURIComponent(error));
|
3500
|
-
return url.toString();
|
3501
|
-
}
|
3502
|
-
getAuth(cookies) {
|
3503
|
-
const cookie = cookies[VAL_SESSION_COOKIE$1];
|
3504
|
-
if (!this.options.valSecret) {
|
3505
|
-
if (this.serverOps instanceof ValOpsFS) {
|
3610
|
+
};
|
3611
|
+
const getAuth = cookies => {
|
3612
|
+
const cookie = cookies[VAL_SESSION_COOKIE];
|
3613
|
+
if (!options.valSecret) {
|
3614
|
+
if (serverOps instanceof ValOpsFS) {
|
3506
3615
|
return {
|
3507
3616
|
error: null,
|
3508
3617
|
id: null
|
@@ -3514,9 +3623,9 @@ class ValServer {
|
|
3514
3623
|
}
|
3515
3624
|
}
|
3516
3625
|
if (typeof cookie === "string") {
|
3517
|
-
const decodedToken = decodeJwt(cookie,
|
3626
|
+
const decodedToken = decodeJwt(cookie, options.valSecret);
|
3518
3627
|
if (!decodedToken) {
|
3519
|
-
if (
|
3628
|
+
if (serverOps instanceof ValOpsFS) {
|
3520
3629
|
return {
|
3521
3630
|
error: null,
|
3522
3631
|
id: null
|
@@ -3528,7 +3637,7 @@ class ValServer {
|
|
3528
3637
|
}
|
3529
3638
|
const verification = IntegratedServerJwtPayload.safeParse(decodedToken);
|
3530
3639
|
if (!verification.success) {
|
3531
|
-
if (
|
3640
|
+
if (serverOps instanceof ValOpsFS) {
|
3532
3641
|
return {
|
3533
3642
|
error: null,
|
3534
3643
|
id: null
|
@@ -3542,7 +3651,7 @@ class ValServer {
|
|
3542
3651
|
id: verification.data.sub
|
3543
3652
|
};
|
3544
3653
|
} else {
|
3545
|
-
if (
|
3654
|
+
if (serverOps instanceof ValOpsFS) {
|
3546
3655
|
return {
|
3547
3656
|
error: null,
|
3548
3657
|
id: null
|
@@ -3552,417 +3661,880 @@ class ValServer {
|
|
3552
3661
|
error: "Login required: cookie not found"
|
3553
3662
|
};
|
3554
3663
|
}
|
3555
|
-
}
|
3556
|
-
async
|
3664
|
+
};
|
3665
|
+
const authorize = async redirectTo => {
|
3666
|
+
const token = crypto.randomUUID();
|
3667
|
+
const redirectUrl = new URL(redirectTo);
|
3668
|
+
const appAuthorizeUrl = getAuthorizeUrl(`${redirectUrl.origin}/${options.route}`, token);
|
3669
|
+
await callbacks.onEnable(true);
|
3557
3670
|
return {
|
3558
|
-
status: 200,
|
3559
3671
|
cookies: {
|
3560
|
-
[
|
3561
|
-
|
3562
|
-
|
3563
|
-
|
3564
|
-
|
3672
|
+
[VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
|
3673
|
+
[VAL_STATE_COOKIE]: {
|
3674
|
+
value: createStateCookie({
|
3675
|
+
redirect_to: redirectTo,
|
3676
|
+
token
|
3677
|
+
}),
|
3678
|
+
options: {
|
3679
|
+
httpOnly: true,
|
3680
|
+
sameSite: "lax",
|
3681
|
+
expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
|
3682
|
+
}
|
3565
3683
|
}
|
3566
|
-
}
|
3684
|
+
},
|
3685
|
+
status: 302,
|
3686
|
+
redirectTo: appAuthorizeUrl
|
3567
3687
|
};
|
3568
|
-
}
|
3569
|
-
|
3570
|
-
|
3571
|
-
|
3572
|
-
|
3573
|
-
|
3574
|
-
|
3575
|
-
|
3576
|
-
|
3577
|
-
|
3688
|
+
};
|
3689
|
+
return {
|
3690
|
+
"/draft/enable": {
|
3691
|
+
GET: async req => {
|
3692
|
+
const cookies = req.cookies;
|
3693
|
+
const auth = getAuth(cookies);
|
3694
|
+
if (auth.error) {
|
3695
|
+
return {
|
3696
|
+
status: 401,
|
3697
|
+
json: {
|
3698
|
+
message: auth.error
|
3699
|
+
}
|
3700
|
+
};
|
3578
3701
|
}
|
3579
|
-
|
3580
|
-
|
3581
|
-
|
3582
|
-
|
3583
|
-
status: 401,
|
3584
|
-
json: {
|
3585
|
-
message: "Unauthorized"
|
3702
|
+
const query = req.query;
|
3703
|
+
const redirectToRes = getRedirectUrl(query, options.valEnableRedirectUrl);
|
3704
|
+
if (typeof redirectToRes !== "string") {
|
3705
|
+
return redirectToRes;
|
3586
3706
|
}
|
3587
|
-
|
3588
|
-
|
3589
|
-
|
3590
|
-
|
3591
|
-
|
3592
|
-
|
3593
|
-
|
3594
|
-
|
3595
|
-
|
3596
|
-
|
3597
|
-
|
3598
|
-
|
3599
|
-
|
3600
|
-
|
3601
|
-
|
3707
|
+
await callbacks.onEnable(true);
|
3708
|
+
return {
|
3709
|
+
status: 302,
|
3710
|
+
redirectTo: redirectToRes
|
3711
|
+
};
|
3712
|
+
}
|
3713
|
+
},
|
3714
|
+
"/draft/disable": {
|
3715
|
+
GET: async req => {
|
3716
|
+
const cookies = req.cookies;
|
3717
|
+
const auth = getAuth(cookies);
|
3718
|
+
if (auth.error) {
|
3719
|
+
return {
|
3720
|
+
status: 401,
|
3721
|
+
json: {
|
3722
|
+
message: auth.error
|
3723
|
+
}
|
3724
|
+
};
|
3602
3725
|
}
|
3603
|
-
|
3604
|
-
|
3605
|
-
|
3606
|
-
|
3607
|
-
json: patches
|
3608
|
-
};
|
3609
|
-
}
|
3610
|
-
async deletePatches(query, cookies) {
|
3611
|
-
const auth = this.getAuth(cookies);
|
3612
|
-
if (auth.error) {
|
3613
|
-
return {
|
3614
|
-
status: 401,
|
3615
|
-
json: {
|
3616
|
-
message: auth.error
|
3726
|
+
const query = req.query;
|
3727
|
+
const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
|
3728
|
+
if (typeof redirectToRes !== "string") {
|
3729
|
+
return redirectToRes;
|
3617
3730
|
}
|
3618
|
-
|
3619
|
-
|
3620
|
-
|
3621
|
-
|
3622
|
-
|
3623
|
-
|
3624
|
-
|
3731
|
+
await callbacks.onDisable(true);
|
3732
|
+
return {
|
3733
|
+
status: 302,
|
3734
|
+
redirectTo: redirectToRes
|
3735
|
+
};
|
3736
|
+
}
|
3737
|
+
},
|
3738
|
+
"/draft/stat": {
|
3739
|
+
GET: async req => {
|
3740
|
+
const cookies = req.cookies;
|
3741
|
+
const auth = getAuth(cookies);
|
3742
|
+
if (auth.error) {
|
3743
|
+
return {
|
3744
|
+
status: 401,
|
3745
|
+
json: {
|
3746
|
+
message: auth.error
|
3747
|
+
}
|
3748
|
+
};
|
3625
3749
|
}
|
3626
|
-
|
3627
|
-
|
3628
|
-
|
3629
|
-
|
3630
|
-
|
3631
|
-
|
3632
|
-
|
3633
|
-
|
3634
|
-
|
3635
|
-
|
3636
|
-
|
3750
|
+
return {
|
3751
|
+
status: 200,
|
3752
|
+
json: {
|
3753
|
+
draftMode: await callbacks.isEnabled()
|
3754
|
+
}
|
3755
|
+
};
|
3756
|
+
}
|
3757
|
+
},
|
3758
|
+
"/enable": {
|
3759
|
+
GET: async req => {
|
3760
|
+
const cookies = req.cookies;
|
3761
|
+
const auth = getAuth(cookies);
|
3762
|
+
const query = req.query;
|
3763
|
+
const redirectToRes = getRedirectUrl(query, options.valEnableRedirectUrl);
|
3764
|
+
if (auth.error) {
|
3765
|
+
if (typeof redirectToRes === "string") {
|
3766
|
+
return authorize(redirectToRes);
|
3767
|
+
}
|
3768
|
+
return {
|
3769
|
+
status: 401,
|
3770
|
+
json: {
|
3771
|
+
message: auth.error
|
3772
|
+
}
|
3773
|
+
};
|
3637
3774
|
}
|
3638
|
-
|
3639
|
-
|
3640
|
-
return {
|
3641
|
-
status: 200,
|
3642
|
-
json: ids
|
3643
|
-
};
|
3644
|
-
}
|
3645
|
-
|
3646
|
-
//#region tree ops
|
3647
|
-
async getSchema(cookies) {
|
3648
|
-
const auth = this.getAuth(cookies);
|
3649
|
-
if (auth.error) {
|
3650
|
-
return {
|
3651
|
-
status: 401,
|
3652
|
-
json: {
|
3653
|
-
message: auth.error
|
3775
|
+
if (typeof redirectToRes !== "string") {
|
3776
|
+
return redirectToRes;
|
3654
3777
|
}
|
3655
|
-
|
3656
|
-
|
3657
|
-
|
3658
|
-
|
3659
|
-
|
3660
|
-
|
3661
|
-
|
3778
|
+
await callbacks.onEnable(true);
|
3779
|
+
return {
|
3780
|
+
cookies: {
|
3781
|
+
[VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
|
3782
|
+
},
|
3783
|
+
status: 302,
|
3784
|
+
redirectTo: redirectToRes
|
3785
|
+
};
|
3786
|
+
}
|
3787
|
+
},
|
3788
|
+
"/disable": {
|
3789
|
+
GET: async req => {
|
3790
|
+
const cookies = req.cookies;
|
3791
|
+
const auth = getAuth(cookies);
|
3792
|
+
if (auth.error) {
|
3793
|
+
return {
|
3794
|
+
status: 401,
|
3795
|
+
json: {
|
3796
|
+
message: auth.error
|
3797
|
+
}
|
3798
|
+
};
|
3662
3799
|
}
|
3663
|
-
|
3664
|
-
|
3665
|
-
|
3666
|
-
|
3667
|
-
console.error("Val: Module errors", moduleErrors);
|
3668
|
-
return {
|
3669
|
-
status: 500,
|
3670
|
-
json: {
|
3671
|
-
message: "Val is not correctly setup. Check the val.modules file",
|
3672
|
-
details: moduleErrors
|
3800
|
+
const query = req.query;
|
3801
|
+
const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
|
3802
|
+
if (typeof redirectToRes !== "string") {
|
3803
|
+
return redirectToRes;
|
3673
3804
|
}
|
3674
|
-
|
3675
|
-
|
3676
|
-
|
3677
|
-
|
3678
|
-
|
3679
|
-
|
3680
|
-
|
3681
|
-
|
3682
|
-
|
3683
|
-
|
3684
|
-
status: 200,
|
3685
|
-
json: {
|
3686
|
-
schemaSha,
|
3687
|
-
schemas: serializedSchemas
|
3805
|
+
await callbacks.onDisable(true);
|
3806
|
+
return {
|
3807
|
+
cookies: {
|
3808
|
+
[VAL_ENABLE_COOKIE_NAME]: {
|
3809
|
+
value: "false"
|
3810
|
+
}
|
3811
|
+
},
|
3812
|
+
status: 302,
|
3813
|
+
redirectTo: redirectToRes
|
3814
|
+
};
|
3688
3815
|
}
|
3689
|
-
}
|
3690
|
-
|
3691
|
-
|
3692
|
-
|
3693
|
-
|
3694
|
-
|
3695
|
-
|
3696
|
-
|
3697
|
-
|
3698
|
-
|
3816
|
+
},
|
3817
|
+
//#region auth
|
3818
|
+
"/authorize": {
|
3819
|
+
GET: async req => {
|
3820
|
+
const query = req.query;
|
3821
|
+
if (typeof query.redirect_to !== "string") {
|
3822
|
+
return {
|
3823
|
+
status: 400,
|
3824
|
+
json: {
|
3825
|
+
message: "Missing redirect_to query param"
|
3826
|
+
}
|
3827
|
+
};
|
3699
3828
|
}
|
3700
|
-
|
3701
|
-
|
3702
|
-
|
3703
|
-
|
3704
|
-
|
3705
|
-
|
3706
|
-
|
3829
|
+
const redirectTo = query.redirect_to;
|
3830
|
+
return authorize(redirectTo);
|
3831
|
+
}
|
3832
|
+
},
|
3833
|
+
"/callback": {
|
3834
|
+
GET: async req => {
|
3835
|
+
const cookies = req.cookies;
|
3836
|
+
const query = req.query;
|
3837
|
+
if (!options.project) {
|
3838
|
+
return {
|
3839
|
+
status: 302,
|
3840
|
+
cookies: {
|
3841
|
+
[VAL_STATE_COOKIE]: {
|
3842
|
+
value: null
|
3843
|
+
}
|
3844
|
+
},
|
3845
|
+
redirectTo: getAppErrorUrl("Project is not set")
|
3846
|
+
};
|
3707
3847
|
}
|
3708
|
-
|
3709
|
-
|
3710
|
-
|
3711
|
-
|
3712
|
-
|
3713
|
-
|
3714
|
-
|
3715
|
-
|
3716
|
-
|
3717
|
-
|
3718
|
-
}).optional()
|
3719
|
-
}).optional();
|
3720
|
-
const moduleErrors = await this.serverOps.getModuleErrors();
|
3721
|
-
if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
|
3722
|
-
console.error("Val: Module errors", moduleErrors);
|
3723
|
-
return {
|
3724
|
-
status: 500,
|
3725
|
-
json: {
|
3726
|
-
message: "Val is not correctly setup. Check the val.modules file",
|
3727
|
-
details: moduleErrors
|
3848
|
+
if (!options.valSecret) {
|
3849
|
+
return {
|
3850
|
+
status: 302,
|
3851
|
+
cookies: {
|
3852
|
+
[VAL_STATE_COOKIE]: {
|
3853
|
+
value: null
|
3854
|
+
}
|
3855
|
+
},
|
3856
|
+
redirectTo: getAppErrorUrl("Secret is not set")
|
3857
|
+
};
|
3728
3858
|
}
|
3729
|
-
|
3730
|
-
|
3731
|
-
|
3732
|
-
|
3733
|
-
|
3734
|
-
|
3735
|
-
|
3736
|
-
|
3737
|
-
|
3859
|
+
const {
|
3860
|
+
success: callbackReqSuccess,
|
3861
|
+
error: callbackReqError
|
3862
|
+
} = verifyCallbackReq(cookies[VAL_STATE_COOKIE], query);
|
3863
|
+
if (callbackReqError !== null) {
|
3864
|
+
return {
|
3865
|
+
status: 302,
|
3866
|
+
cookies: {
|
3867
|
+
[VAL_STATE_COOKIE]: {
|
3868
|
+
value: null
|
3869
|
+
}
|
3870
|
+
},
|
3871
|
+
redirectTo: getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`)
|
3872
|
+
};
|
3873
|
+
}
|
3874
|
+
const data = await consumeCode(callbackReqSuccess.code);
|
3875
|
+
if (data === null) {
|
3876
|
+
return {
|
3877
|
+
status: 302,
|
3878
|
+
cookies: {
|
3879
|
+
[VAL_STATE_COOKIE]: {
|
3880
|
+
value: null
|
3881
|
+
}
|
3882
|
+
},
|
3883
|
+
redirectTo: getAppErrorUrl("Failed to exchange code for user")
|
3884
|
+
};
|
3885
|
+
}
|
3886
|
+
const exp = getExpire();
|
3887
|
+
const valSecret = options.valSecret;
|
3888
|
+
if (!valSecret) {
|
3889
|
+
return {
|
3890
|
+
status: 302,
|
3891
|
+
cookies: {
|
3892
|
+
[VAL_STATE_COOKIE]: {
|
3893
|
+
value: null
|
3894
|
+
}
|
3895
|
+
},
|
3896
|
+
redirectTo: getAppErrorUrl("Setup is not correct: secret is missing")
|
3897
|
+
};
|
3898
|
+
}
|
3899
|
+
const cookie = encodeJwt({
|
3900
|
+
...data,
|
3901
|
+
exp // this is the client side exp
|
3902
|
+
}, valSecret);
|
3903
|
+
return {
|
3904
|
+
status: 302,
|
3905
|
+
cookies: {
|
3906
|
+
[VAL_STATE_COOKIE]: {
|
3907
|
+
value: null
|
3908
|
+
},
|
3909
|
+
[VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
|
3910
|
+
[VAL_SESSION_COOKIE]: {
|
3911
|
+
value: cookie,
|
3912
|
+
options: {
|
3913
|
+
httpOnly: true,
|
3914
|
+
sameSite: "strict",
|
3915
|
+
path: "/",
|
3916
|
+
secure: true,
|
3917
|
+
expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
|
3918
|
+
}
|
3919
|
+
}
|
3920
|
+
},
|
3921
|
+
redirectTo: callbackReqSuccess.redirect_uri || "/"
|
3922
|
+
};
|
3923
|
+
}
|
3924
|
+
},
|
3925
|
+
"/session": {
|
3926
|
+
GET: async req => {
|
3927
|
+
const cookies = req.cookies;
|
3928
|
+
if (serverOps instanceof ValOpsFS) {
|
3929
|
+
return {
|
3930
|
+
status: 200,
|
3931
|
+
json: {
|
3932
|
+
mode: "local",
|
3933
|
+
enabled: await callbacks.isEnabled()
|
3934
|
+
}
|
3935
|
+
};
|
3936
|
+
}
|
3937
|
+
if (!options.project) {
|
3938
|
+
return {
|
3939
|
+
status: 500,
|
3940
|
+
json: {
|
3941
|
+
message: "Project is not set"
|
3942
|
+
}
|
3943
|
+
};
|
3944
|
+
}
|
3945
|
+
if (!options.valSecret) {
|
3946
|
+
return {
|
3947
|
+
status: 500,
|
3948
|
+
json: {
|
3949
|
+
message: "Secret is not set"
|
3950
|
+
}
|
3951
|
+
};
|
3952
|
+
}
|
3953
|
+
return withAuth(options.valSecret, cookies, "session", async data => {
|
3954
|
+
if (!options.valBuildUrl) {
|
3955
|
+
return {
|
3956
|
+
status: 500,
|
3957
|
+
json: {
|
3958
|
+
message: "Val is not correctly setup. Build url is missing"
|
3959
|
+
}
|
3960
|
+
};
|
3961
|
+
}
|
3962
|
+
const url = new URL(`/api/val/${options.project}/auth/session`, options.valBuildUrl);
|
3963
|
+
const fetchRes = await fetch(url, {
|
3964
|
+
headers: getAuthHeaders(data.token, "application/json")
|
3965
|
+
});
|
3966
|
+
if (fetchRes.status === 200) {
|
3967
|
+
return {
|
3968
|
+
status: fetchRes.status,
|
3969
|
+
json: {
|
3970
|
+
mode: "proxy",
|
3971
|
+
enabled: await callbacks.isEnabled(),
|
3972
|
+
...(await fetchRes.json())
|
3973
|
+
}
|
3974
|
+
};
|
3975
|
+
} else {
|
3976
|
+
return {
|
3977
|
+
status: fetchRes.status,
|
3978
|
+
json: {
|
3979
|
+
message: "Failed to authorize",
|
3980
|
+
...(await fetchRes.json())
|
3981
|
+
}
|
3982
|
+
};
|
3983
|
+
}
|
3984
|
+
});
|
3985
|
+
}
|
3986
|
+
},
|
3987
|
+
"/logout": {
|
3988
|
+
GET: async () => {
|
3989
|
+
return {
|
3990
|
+
status: 200,
|
3991
|
+
cookies: {
|
3992
|
+
[VAL_SESSION_COOKIE]: {
|
3993
|
+
value: null
|
3994
|
+
},
|
3995
|
+
[VAL_STATE_COOKIE]: {
|
3996
|
+
value: null
|
3997
|
+
}
|
3998
|
+
}
|
3999
|
+
};
|
4000
|
+
}
|
4001
|
+
},
|
4002
|
+
//#region stat
|
4003
|
+
"/stat": {
|
4004
|
+
POST: async req => {
|
4005
|
+
const cookies = req.cookies;
|
4006
|
+
const auth = getAuth(cookies);
|
4007
|
+
if (auth.error) {
|
4008
|
+
return {
|
4009
|
+
status: 401,
|
4010
|
+
json: {
|
4011
|
+
message: auth.error
|
4012
|
+
}
|
4013
|
+
};
|
4014
|
+
}
|
4015
|
+
if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
|
4016
|
+
return {
|
4017
|
+
status: 401,
|
4018
|
+
json: {
|
4019
|
+
message: "Unauthorized"
|
4020
|
+
}
|
4021
|
+
};
|
4022
|
+
}
|
4023
|
+
const currentStat = await serverOps.getStat({
|
4024
|
+
...req.body,
|
4025
|
+
profileId: "id" in auth ? auth.id : undefined
|
4026
|
+
});
|
4027
|
+
if (currentStat.type === "error") {
|
4028
|
+
return {
|
4029
|
+
status: 500,
|
4030
|
+
json: currentStat.error
|
4031
|
+
};
|
4032
|
+
}
|
4033
|
+
return {
|
4034
|
+
status: 200,
|
4035
|
+
json: {
|
4036
|
+
...currentStat,
|
4037
|
+
config: options.config
|
4038
|
+
}
|
4039
|
+
};
|
4040
|
+
}
|
4041
|
+
},
|
4042
|
+
//#region patches
|
4043
|
+
"/patches/~": {
|
4044
|
+
GET: async req => {
|
4045
|
+
const query = req.query;
|
4046
|
+
const cookies = req.cookies;
|
4047
|
+
const auth = getAuth(cookies);
|
4048
|
+
if (auth.error) {
|
4049
|
+
return {
|
4050
|
+
status: 401,
|
4051
|
+
json: {
|
4052
|
+
message: auth.error
|
4053
|
+
}
|
4054
|
+
};
|
4055
|
+
}
|
4056
|
+
if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
|
4057
|
+
return {
|
4058
|
+
status: 401,
|
4059
|
+
json: {
|
4060
|
+
message: "Unauthorized"
|
4061
|
+
}
|
4062
|
+
};
|
4063
|
+
}
|
4064
|
+
const authors = query.author;
|
4065
|
+
const patches = await serverOps.fetchPatches({
|
4066
|
+
authors,
|
4067
|
+
patchIds: query.patch_id,
|
4068
|
+
omitPatch: query.omit_patch === true,
|
4069
|
+
moduleFilePaths: query.module_file_path
|
4070
|
+
});
|
4071
|
+
if (patches.error) {
|
4072
|
+
// Error is singular
|
4073
|
+
console.error("Val: Failed to get patches", patches.errors);
|
4074
|
+
return {
|
4075
|
+
status: 500,
|
4076
|
+
json: {
|
4077
|
+
message: patches.error.message,
|
4078
|
+
details: patches.error
|
4079
|
+
}
|
4080
|
+
};
|
4081
|
+
}
|
4082
|
+
if (patches.errors && Object.keys(patches.errors).length > 0) {
|
4083
|
+
// Errors is plural. Different property than above.
|
4084
|
+
console.error("Val: Failed to get patches", patches.errors);
|
4085
|
+
return {
|
4086
|
+
status: 500,
|
4087
|
+
json: {
|
4088
|
+
message: "Failed to get patches",
|
4089
|
+
details: patches.errors
|
4090
|
+
}
|
4091
|
+
};
|
4092
|
+
}
|
4093
|
+
return {
|
4094
|
+
status: 200,
|
4095
|
+
json: patches
|
4096
|
+
};
|
4097
|
+
},
|
4098
|
+
DELETE: async req => {
|
4099
|
+
const query = req.query;
|
4100
|
+
const cookies = req.cookies;
|
4101
|
+
const auth = getAuth(cookies);
|
4102
|
+
if (auth.error) {
|
4103
|
+
return {
|
4104
|
+
status: 401,
|
4105
|
+
json: {
|
4106
|
+
message: auth.error
|
4107
|
+
}
|
4108
|
+
};
|
4109
|
+
}
|
4110
|
+
if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
|
4111
|
+
return {
|
4112
|
+
status: 401,
|
4113
|
+
json: {
|
4114
|
+
message: "Unauthorized"
|
4115
|
+
}
|
4116
|
+
};
|
4117
|
+
}
|
4118
|
+
const ids = query.id;
|
4119
|
+
const deleteRes = await serverOps.deletePatches(ids);
|
4120
|
+
if (deleteRes.errors && Object.keys(deleteRes.errors).length > 0) {
|
4121
|
+
console.error("Val: Failed to delete patches", deleteRes.errors);
|
4122
|
+
return {
|
4123
|
+
status: 500,
|
4124
|
+
json: {
|
4125
|
+
message: "Failed to delete patches",
|
4126
|
+
details: deleteRes.errors
|
4127
|
+
}
|
4128
|
+
};
|
4129
|
+
}
|
4130
|
+
return {
|
4131
|
+
status: 200,
|
4132
|
+
json: ids
|
4133
|
+
};
|
4134
|
+
}
|
4135
|
+
},
|
4136
|
+
//#region schema
|
4137
|
+
"/schema": {
|
4138
|
+
GET: async req => {
|
4139
|
+
const cookies = req.cookies;
|
4140
|
+
const auth = getAuth(cookies);
|
4141
|
+
if (auth.error) {
|
4142
|
+
return {
|
4143
|
+
status: 401,
|
4144
|
+
json: {
|
4145
|
+
message: auth.error
|
4146
|
+
}
|
4147
|
+
};
|
4148
|
+
}
|
4149
|
+
if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
|
4150
|
+
return {
|
4151
|
+
status: 401,
|
4152
|
+
json: {
|
4153
|
+
message: "Unauthorized"
|
4154
|
+
}
|
4155
|
+
};
|
4156
|
+
}
|
4157
|
+
const moduleErrors = await serverOps.getModuleErrors();
|
4158
|
+
if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
|
4159
|
+
console.error("Val: Module errors", moduleErrors);
|
4160
|
+
return {
|
4161
|
+
status: 500,
|
4162
|
+
json: {
|
4163
|
+
message: `Got errors while fetching modules: ${moduleErrors.filter(error => error).map(error => error.message).join(", ")}`,
|
4164
|
+
details: moduleErrors
|
4165
|
+
}
|
4166
|
+
};
|
4167
|
+
}
|
4168
|
+
const schemaSha = await serverOps.getSchemaSha();
|
4169
|
+
const schemas = await serverOps.getSchemas();
|
4170
|
+
const serializedSchemas = {};
|
4171
|
+
try {
|
4172
|
+
for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
|
4173
|
+
const moduleFilePath = moduleFilePathS;
|
4174
|
+
serializedSchemas[moduleFilePath] = schema.serialize();
|
4175
|
+
}
|
4176
|
+
} catch (e) {
|
4177
|
+
console.error("Val: Failed to serialize schemas", e);
|
4178
|
+
return {
|
4179
|
+
status: 500,
|
4180
|
+
json: {
|
4181
|
+
message: "Failed to serialize schemas",
|
4182
|
+
details: [{
|
4183
|
+
message: e instanceof Error ? e.message : JSON.stringify(e)
|
4184
|
+
}]
|
4185
|
+
}
|
4186
|
+
};
|
4187
|
+
}
|
4188
|
+
return {
|
4189
|
+
status: 200,
|
4190
|
+
json: {
|
4191
|
+
schemaSha,
|
4192
|
+
schemas: serializedSchemas
|
4193
|
+
}
|
4194
|
+
};
|
4195
|
+
}
|
4196
|
+
},
|
4197
|
+
// #region sources
|
4198
|
+
"/sources": {
|
4199
|
+
PUT: async req => {
|
4200
|
+
var _body$patchIds;
|
4201
|
+
const query = req.query;
|
4202
|
+
const cookies = req.cookies;
|
4203
|
+
const body = req.body;
|
4204
|
+
const treePath = req.path || "";
|
4205
|
+
const auth = getAuth(cookies);
|
4206
|
+
if (auth.error) {
|
4207
|
+
return {
|
4208
|
+
status: 401,
|
4209
|
+
json: {
|
4210
|
+
message: auth.error
|
4211
|
+
}
|
4212
|
+
};
|
4213
|
+
}
|
4214
|
+
if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
|
4215
|
+
return {
|
4216
|
+
status: 401,
|
4217
|
+
json: {
|
4218
|
+
message: "Unauthorized"
|
4219
|
+
}
|
4220
|
+
};
|
4221
|
+
}
|
4222
|
+
const moduleErrors = await serverOps.getModuleErrors();
|
4223
|
+
if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
|
4224
|
+
console.error("Val: Module errors", moduleErrors);
|
4225
|
+
return {
|
4226
|
+
status: 500,
|
4227
|
+
json: {
|
4228
|
+
message: "Val is not correctly setup. Check the val.modules file",
|
4229
|
+
details: moduleErrors
|
4230
|
+
}
|
4231
|
+
};
|
4232
|
+
}
|
4233
|
+
let tree;
|
4234
|
+
let patchAnalysis = null;
|
4235
|
+
let newPatchIds = undefined;
|
4236
|
+
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) {
|
4237
|
+
// TODO: validate patches_sha
|
4238
|
+
const patchIds = body === null || body === void 0 ? void 0 : body.patchIds;
|
4239
|
+
const patchOps = patchIds && patchIds.length > 0 ? await serverOps.fetchPatches({
|
4240
|
+
patchIds,
|
4241
|
+
omitPatch: false
|
4242
|
+
}) : {
|
4243
|
+
patches: {}
|
4244
|
+
};
|
4245
|
+
if (patchOps.error) {
|
4246
|
+
return {
|
4247
|
+
status: 400,
|
4248
|
+
json: {
|
4249
|
+
message: "Failed to fetch patches: " + patchOps.error.message,
|
4250
|
+
details: []
|
4251
|
+
}
|
4252
|
+
};
|
4253
|
+
}
|
4254
|
+
let patchErrors = undefined;
|
4255
|
+
for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
|
4256
|
+
const patchId = patchIdS;
|
4257
|
+
if (!patchErrors) {
|
4258
|
+
patchErrors = {};
|
4259
|
+
}
|
4260
|
+
patchErrors[patchId] = {
|
4261
|
+
message: error.message
|
4262
|
+
};
|
4263
|
+
}
|
4264
|
+
// TODO: errors
|
4265
|
+
patchAnalysis = serverOps.analyzePatches(patchOps.patches);
|
4266
|
+
if (body !== null && body !== void 0 && body.addPatches) {
|
4267
|
+
for (const addPatch of body.addPatches) {
|
4268
|
+
const newPatchModuleFilePath = addPatch.path;
|
4269
|
+
const newPatchOps = addPatch.patch;
|
4270
|
+
const authorId = "id" in auth ? auth.id : null;
|
4271
|
+
const createPatchRes = await serverOps.createPatch(newPatchModuleFilePath, {
|
4272
|
+
...patchAnalysis,
|
4273
|
+
...patchOps
|
4274
|
+
}, newPatchOps, authorId);
|
4275
|
+
if (createPatchRes.error) {
|
4276
|
+
return {
|
4277
|
+
status: 500,
|
4278
|
+
json: {
|
4279
|
+
message: "Failed to create patch: " + createPatchRes.error.message,
|
4280
|
+
details: createPatchRes.error
|
4281
|
+
}
|
4282
|
+
};
|
4283
|
+
}
|
4284
|
+
if (!newPatchIds) {
|
4285
|
+
newPatchIds = [createPatchRes.patchId];
|
4286
|
+
} else {
|
4287
|
+
newPatchIds.push(createPatchRes.patchId);
|
4288
|
+
}
|
4289
|
+
patchOps.patches[createPatchRes.patchId] = {
|
4290
|
+
path: newPatchModuleFilePath,
|
4291
|
+
patch: newPatchOps,
|
4292
|
+
authorId,
|
4293
|
+
createdAt: createPatchRes.createdAt,
|
4294
|
+
appliedAt: null
|
4295
|
+
};
|
4296
|
+
patchAnalysis.patchesByModule[newPatchModuleFilePath] = [...(patchAnalysis.patchesByModule[newPatchModuleFilePath] || []), {
|
4297
|
+
patchId: createPatchRes.patchId
|
4298
|
+
}];
|
4299
|
+
}
|
4300
|
+
}
|
4301
|
+
tree = {
|
4302
|
+
...(await serverOps.getTree({
|
4303
|
+
...patchAnalysis,
|
4304
|
+
...patchOps
|
4305
|
+
}))
|
4306
|
+
};
|
4307
|
+
if (query.validate_all) {
|
4308
|
+
const allTree = await serverOps.getTree();
|
4309
|
+
tree = {
|
4310
|
+
sources: {
|
4311
|
+
...allTree.sources,
|
4312
|
+
...tree.sources
|
4313
|
+
},
|
4314
|
+
errors: {
|
4315
|
+
...allTree.errors,
|
4316
|
+
...tree.errors
|
4317
|
+
}
|
4318
|
+
};
|
4319
|
+
}
|
4320
|
+
} else {
|
4321
|
+
tree = await serverOps.getTree();
|
3738
4322
|
}
|
3739
|
-
|
3740
|
-
|
3741
|
-
|
3742
|
-
let patchAnalysis = null;
|
3743
|
-
let newPatchId = undefined;
|
3744
|
-
if ((_bodyRes$data = bodyRes.data) !== null && _bodyRes$data !== void 0 && _bodyRes$data.patchIds && ((_bodyRes$data2 = bodyRes.data) === null || _bodyRes$data2 === void 0 || (_bodyRes$data2 = _bodyRes$data2.patchIds) === null || _bodyRes$data2 === void 0 ? void 0 : _bodyRes$data2.length) > 0 || (_bodyRes$data3 = bodyRes.data) !== null && _bodyRes$data3 !== void 0 && _bodyRes$data3.addPatch) {
|
3745
|
-
var _bodyRes$data4, _bodyRes$data5;
|
3746
|
-
// TODO: validate patches_sha
|
3747
|
-
const patchIds = (_bodyRes$data4 = bodyRes.data) === null || _bodyRes$data4 === void 0 ? void 0 : _bodyRes$data4.patchIds;
|
3748
|
-
const patchOps = patchIds && patchIds.length > 0 ? await this.serverOps.fetchPatches({
|
3749
|
-
patchIds,
|
3750
|
-
omitPatch: false
|
3751
|
-
}) : {
|
3752
|
-
patches: {}
|
3753
|
-
};
|
3754
|
-
let patchErrors = undefined;
|
3755
|
-
for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
|
3756
|
-
const patchId = patchIdS;
|
3757
|
-
if (!patchErrors) {
|
3758
|
-
patchErrors = {};
|
3759
|
-
}
|
3760
|
-
patchErrors[patchId] = {
|
3761
|
-
message: error.message
|
4323
|
+
let sourcesValidation = {
|
4324
|
+
errors: {},
|
4325
|
+
files: {}
|
3762
4326
|
};
|
3763
|
-
|
3764
|
-
|
3765
|
-
|
3766
|
-
|
3767
|
-
|
3768
|
-
|
3769
|
-
|
3770
|
-
|
3771
|
-
|
4327
|
+
if (query.validate_sources || query.validate_binary_files) {
|
4328
|
+
const schemas = await serverOps.getSchemas();
|
4329
|
+
sourcesValidation = await serverOps.validateSources(schemas, tree.sources);
|
4330
|
+
|
4331
|
+
// TODO: send validation errors
|
4332
|
+
if (query.validate_binary_files) {
|
4333
|
+
await serverOps.validateFiles(schemas, tree.sources, sourcesValidation.files);
|
4334
|
+
}
|
4335
|
+
}
|
4336
|
+
const schemaSha = await serverOps.getSchemaSha();
|
4337
|
+
const modules = {};
|
4338
|
+
for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
|
4339
|
+
const moduleFilePath = moduleFilePathS;
|
4340
|
+
if (moduleFilePath.startsWith(treePath)) {
|
4341
|
+
var _sourcesValidation$er;
|
4342
|
+
modules[moduleFilePath] = {
|
4343
|
+
source: module,
|
4344
|
+
patches: patchAnalysis && patchAnalysis.patchesByModule[moduleFilePath] ? {
|
4345
|
+
applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
|
4346
|
+
} : undefined,
|
4347
|
+
validationErrors: (_sourcesValidation$er = sourcesValidation.errors[moduleFilePath]) === null || _sourcesValidation$er === void 0 ? void 0 : _sourcesValidation$er.validations
|
4348
|
+
};
|
4349
|
+
}
|
4350
|
+
}
|
4351
|
+
if (tree.errors && Object.keys(tree.errors).length > 0) {
|
4352
|
+
const res = {
|
4353
|
+
status: 400,
|
3772
4354
|
json: {
|
3773
|
-
|
3774
|
-
|
4355
|
+
type: "patch-error",
|
4356
|
+
schemaSha,
|
4357
|
+
modules,
|
4358
|
+
errors: Object.fromEntries(Object.entries(tree.errors).map(([key, value]) => [key, value.map(error => ({
|
4359
|
+
patchId: error.patchId,
|
4360
|
+
skipped: error.skipped,
|
4361
|
+
error: {
|
4362
|
+
message: error.error.message
|
4363
|
+
}
|
4364
|
+
}))])),
|
4365
|
+
message: "One or more patches failed to be applied"
|
3775
4366
|
}
|
3776
4367
|
};
|
4368
|
+
return res;
|
3777
4369
|
}
|
3778
|
-
|
3779
|
-
|
3780
|
-
|
3781
|
-
|
3782
|
-
|
3783
|
-
|
3784
|
-
// status: 500,
|
3785
|
-
// json: {
|
3786
|
-
// message: "Failed to create patch",
|
3787
|
-
// details: fileRes.error,
|
3788
|
-
// },
|
3789
|
-
// };
|
3790
|
-
// }
|
3791
|
-
// }
|
3792
|
-
newPatchId = createPatchRes.patchId;
|
3793
|
-
patchOps.patches[createPatchRes.patchId] = {
|
3794
|
-
path: newPatchModuleFilePath,
|
3795
|
-
patch: newPatchOps,
|
3796
|
-
authorId,
|
3797
|
-
createdAt: createPatchRes.createdAt,
|
3798
|
-
appliedAt: null
|
3799
|
-
};
|
3800
|
-
}
|
3801
|
-
// TODO: errors
|
3802
|
-
patchAnalysis = this.serverOps.analyzePatches(patchOps.patches);
|
3803
|
-
tree = {
|
3804
|
-
...(await this.serverOps.getTree({
|
3805
|
-
...patchAnalysis,
|
3806
|
-
...patchOps
|
3807
|
-
}))
|
3808
|
-
};
|
3809
|
-
if (query.validate_all === "true") {
|
3810
|
-
const allTree = await this.serverOps.getTree();
|
3811
|
-
tree = {
|
3812
|
-
sources: {
|
3813
|
-
...allTree.sources,
|
3814
|
-
...tree.sources
|
3815
|
-
},
|
3816
|
-
errors: {
|
3817
|
-
...allTree.errors,
|
3818
|
-
...tree.errors
|
4370
|
+
const res = {
|
4371
|
+
status: 200,
|
4372
|
+
json: {
|
4373
|
+
schemaSha,
|
4374
|
+
modules,
|
4375
|
+
newPatchIds
|
3819
4376
|
}
|
3820
4377
|
};
|
4378
|
+
return res;
|
3821
4379
|
}
|
3822
|
-
}
|
3823
|
-
|
3824
|
-
|
3825
|
-
|
3826
|
-
|
3827
|
-
|
3828
|
-
|
3829
|
-
|
3830
|
-
|
3831
|
-
|
3832
|
-
|
3833
|
-
|
3834
|
-
|
3835
|
-
}
|
3836
|
-
}
|
3837
|
-
const schemaSha = await this.serverOps.getSchemaSha();
|
3838
|
-
const modules = {};
|
3839
|
-
for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
|
3840
|
-
const moduleFilePath = moduleFilePathS;
|
3841
|
-
if (moduleFilePath.startsWith(treePath)) {
|
3842
|
-
modules[moduleFilePath] = {
|
3843
|
-
source: module,
|
3844
|
-
patches: patchAnalysis ? {
|
3845
|
-
applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
|
3846
|
-
} : undefined
|
3847
|
-
};
|
3848
|
-
}
|
3849
|
-
}
|
3850
|
-
return {
|
3851
|
-
status: 200,
|
3852
|
-
json: {
|
3853
|
-
schemaSha,
|
3854
|
-
modules,
|
3855
|
-
newPatchId
|
3856
|
-
}
|
3857
|
-
};
|
3858
|
-
}
|
3859
|
-
async postSave(body, cookies) {
|
3860
|
-
const auth = this.getAuth(cookies);
|
3861
|
-
if (auth.error) {
|
3862
|
-
return {
|
3863
|
-
status: 401,
|
3864
|
-
json: {
|
3865
|
-
message: auth.error
|
4380
|
+
},
|
4381
|
+
"/save": {
|
4382
|
+
POST: async req => {
|
4383
|
+
const cookies = req.cookies;
|
4384
|
+
const body = req.body;
|
4385
|
+
const auth = getAuth(cookies);
|
4386
|
+
if (auth.error) {
|
4387
|
+
return {
|
4388
|
+
status: 401,
|
4389
|
+
json: {
|
4390
|
+
message: auth.error
|
4391
|
+
}
|
4392
|
+
};
|
3866
4393
|
}
|
3867
|
-
|
3868
|
-
|
3869
|
-
|
3870
|
-
|
3871
|
-
|
3872
|
-
|
3873
|
-
|
3874
|
-
|
3875
|
-
|
3876
|
-
|
3877
|
-
|
3878
|
-
|
3879
|
-
|
4394
|
+
const PostSaveBody = z.object({
|
4395
|
+
patchIds: z.array(z.string().refine(id => true // TODO:
|
4396
|
+
))
|
4397
|
+
});
|
4398
|
+
const bodyRes = PostSaveBody.safeParse(body);
|
4399
|
+
if (!bodyRes.success) {
|
4400
|
+
return {
|
4401
|
+
status: 400,
|
4402
|
+
json: {
|
4403
|
+
message: "Invalid body: " + fromError(bodyRes.error).toString(),
|
4404
|
+
details: bodyRes.error.errors
|
4405
|
+
}
|
4406
|
+
};
|
3880
4407
|
}
|
3881
|
-
|
3882
|
-
|
3883
|
-
|
3884
|
-
|
3885
|
-
|
3886
|
-
|
3887
|
-
|
3888
|
-
|
3889
|
-
|
3890
|
-
|
3891
|
-
|
3892
|
-
|
3893
|
-
|
3894
|
-
|
3895
|
-
if (preparedCommit.hasErrors) {
|
3896
|
-
console.error("Failed to create commit", {
|
3897
|
-
sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
|
3898
|
-
binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
|
3899
|
-
});
|
3900
|
-
return {
|
3901
|
-
status: 400,
|
3902
|
-
json: {
|
3903
|
-
message: "Failed to create commit",
|
3904
|
-
details: {
|
4408
|
+
const {
|
4409
|
+
patchIds
|
4410
|
+
} = bodyRes.data;
|
4411
|
+
const patches = await serverOps.fetchPatches({
|
4412
|
+
patchIds,
|
4413
|
+
omitPatch: false
|
4414
|
+
});
|
4415
|
+
const analysis = serverOps.analyzePatches(patches.patches);
|
4416
|
+
const preparedCommit = await serverOps.prepare({
|
4417
|
+
...analysis,
|
4418
|
+
...patches
|
4419
|
+
});
|
4420
|
+
if (preparedCommit.hasErrors) {
|
4421
|
+
console.error("Failed to create commit", JSON.stringify({
|
3905
4422
|
sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
|
3906
4423
|
binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
|
4424
|
+
}, null, 2));
|
4425
|
+
return {
|
4426
|
+
status: 400,
|
4427
|
+
json: {
|
4428
|
+
message: "Failed to create commit",
|
4429
|
+
details: {
|
4430
|
+
sourceFilePatchErrors: Object.fromEntries(Object.entries(preparedCommit.sourceFilePatchErrors).map(([key, errors]) => [key, errors.map(e => ({
|
4431
|
+
message: formatPatchSourceError(e)
|
4432
|
+
}))])),
|
4433
|
+
binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
|
4434
|
+
}
|
4435
|
+
}
|
4436
|
+
};
|
4437
|
+
}
|
4438
|
+
if (serverOps instanceof ValOpsFS) {
|
4439
|
+
await serverOps.saveFiles(preparedCommit);
|
4440
|
+
await serverOps.deletePatches(patchIds);
|
4441
|
+
return {
|
4442
|
+
status: 200,
|
4443
|
+
json: {} // TODO:
|
4444
|
+
};
|
4445
|
+
} else if (serverOps instanceof ValOpsHttp) {
|
4446
|
+
if (auth.error === undefined && auth.id) {
|
4447
|
+
const commitRes = await serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
|
4448
|
+
if (commitRes.error) {
|
4449
|
+
console.error("Failed to commit", commitRes.error);
|
4450
|
+
if ("isNotFastForward" in commitRes && commitRes.isNotFastForward) {
|
4451
|
+
return {
|
4452
|
+
status: 409,
|
4453
|
+
json: {
|
4454
|
+
isNotFastForward: true,
|
4455
|
+
message: "Cannot commit: this is not the latest version of this branch"
|
4456
|
+
}
|
4457
|
+
};
|
4458
|
+
}
|
4459
|
+
return {
|
4460
|
+
status: 400,
|
4461
|
+
json: {
|
4462
|
+
message: commitRes.error.message,
|
4463
|
+
details: []
|
4464
|
+
}
|
4465
|
+
};
|
4466
|
+
}
|
4467
|
+
// TODO: serverOps.markApplied(patchIds);
|
4468
|
+
return {
|
4469
|
+
status: 200,
|
4470
|
+
json: {} // TODO:
|
4471
|
+
};
|
3907
4472
|
}
|
4473
|
+
return {
|
4474
|
+
status: 401,
|
4475
|
+
json: {
|
4476
|
+
message: "Unauthorized"
|
4477
|
+
}
|
4478
|
+
};
|
4479
|
+
} else {
|
4480
|
+
throw new Error("Invalid server ops");
|
3908
4481
|
}
|
3909
|
-
};
|
3910
|
-
}
|
3911
|
-
if (this.serverOps instanceof ValOpsFS) {
|
3912
|
-
await this.serverOps.saveFiles(preparedCommit);
|
3913
|
-
return {
|
3914
|
-
status: 200,
|
3915
|
-
json: {} // TODO:
|
3916
|
-
};
|
3917
|
-
} else if (this.serverOps instanceof ValOpsHttp) {
|
3918
|
-
if (auth.error === undefined && auth.id) {
|
3919
|
-
await this.serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
|
3920
|
-
return {
|
3921
|
-
status: 200,
|
3922
|
-
json: {} // TODO:
|
3923
|
-
};
|
3924
4482
|
}
|
3925
|
-
|
3926
|
-
|
3927
|
-
|
3928
|
-
|
4483
|
+
},
|
4484
|
+
//#region files
|
4485
|
+
"/files": {
|
4486
|
+
GET: async req => {
|
4487
|
+
const query = req.query;
|
4488
|
+
const filePath = req.path;
|
4489
|
+
// NOTE: no auth here since you would need the patch_id to get something that is not published.
|
4490
|
+
// For everything that is published, well they are already public so no auth required there...
|
4491
|
+
// We could imagine adding auth just to be a 200% certain,
|
4492
|
+
// 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, ...).
|
4493
|
+
// So: 1) patch ids are not possible to guess (but possible to brute force)
|
4494
|
+
// 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)
|
4495
|
+
// 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
|
4496
|
+
// Thus: attack surface + ease of attack + benefit = low probability of attack
|
4497
|
+
// If we couldn't argue that patch ids are secret enough, then this would be a problem.
|
4498
|
+
let cacheControl;
|
4499
|
+
let fileBuffer;
|
4500
|
+
let mimeType;
|
4501
|
+
if (query.patch_id) {
|
4502
|
+
fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
|
4503
|
+
mimeType = Internal.filenameToMimeType(filePath);
|
4504
|
+
cacheControl = "public, max-age=20000, immutable";
|
4505
|
+
} else {
|
4506
|
+
fileBuffer = await serverOps.getBinaryFile(filePath);
|
3929
4507
|
}
|
3930
|
-
|
3931
|
-
|
3932
|
-
|
3933
|
-
|
3934
|
-
|
3935
|
-
|
3936
|
-
|
3937
|
-
|
3938
|
-
|
3939
|
-
|
3940
|
-
|
3941
|
-
|
3942
|
-
|
3943
|
-
|
3944
|
-
|
3945
|
-
|
3946
|
-
|
3947
|
-
let fileBuffer;
|
3948
|
-
if (query.patch_id) {
|
3949
|
-
fileBuffer = await this.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
|
3950
|
-
} else {
|
3951
|
-
fileBuffer = await this.serverOps.getBinaryFile(filePath);
|
3952
|
-
}
|
3953
|
-
if (fileBuffer) {
|
3954
|
-
return {
|
3955
|
-
status: 200,
|
3956
|
-
body: bufferToReadableStream(fileBuffer)
|
3957
|
-
};
|
3958
|
-
} else {
|
3959
|
-
return {
|
3960
|
-
status: 404,
|
3961
|
-
json: {
|
3962
|
-
message: "File not found"
|
4508
|
+
if (fileBuffer) {
|
4509
|
+
return {
|
4510
|
+
status: 200,
|
4511
|
+
headers: {
|
4512
|
+
// TODO: we could use ETag and return 304 instead
|
4513
|
+
"Content-Type": mimeType || "application/octet-stream",
|
4514
|
+
"Cache-Control": cacheControl || "public, max-age=0, must-revalidate"
|
4515
|
+
},
|
4516
|
+
body: bufferToReadableStream(fileBuffer)
|
4517
|
+
};
|
4518
|
+
} else {
|
4519
|
+
return {
|
4520
|
+
status: 404,
|
4521
|
+
json: {
|
4522
|
+
message: "File not found"
|
4523
|
+
}
|
4524
|
+
};
|
3963
4525
|
}
|
3964
|
-
}
|
4526
|
+
}
|
3965
4527
|
}
|
4528
|
+
};
|
4529
|
+
};
|
4530
|
+
function formatPatchSourceError(error) {
|
4531
|
+
if ("message" in error) {
|
4532
|
+
return error.message;
|
4533
|
+
} else if (Array.isArray(error)) {
|
4534
|
+
return error.map(formatPatchSourceError).join("\n");
|
4535
|
+
} else {
|
4536
|
+
const _exhaustiveCheck = error;
|
4537
|
+
return "Unknown patch source error: " + JSON.stringify(_exhaustiveCheck);
|
3966
4538
|
}
|
3967
4539
|
}
|
3968
4540
|
function verifyCallbackReq(stateCookie, queryParams) {
|
@@ -4082,7 +4654,7 @@ const IntegratedServerJwtPayload = z.object({
|
|
4082
4654
|
project: z.string()
|
4083
4655
|
});
|
4084
4656
|
async function withAuth(secret, cookies, errorMessageType, handler) {
|
4085
|
-
const cookie = cookies[VAL_SESSION_COOKIE
|
4657
|
+
const cookie = cookies[VAL_SESSION_COOKIE];
|
4086
4658
|
if (typeof cookie === "string") {
|
4087
4659
|
const decodedToken = decodeJwt(cookie, secret);
|
4088
4660
|
if (!decodedToken) {
|
@@ -4100,7 +4672,7 @@ async function withAuth(secret, cookies, errorMessageType, handler) {
|
|
4100
4672
|
status: 401,
|
4101
4673
|
json: {
|
4102
4674
|
message: "Session invalid or, most likely, expired. You will need to login again.",
|
4103
|
-
details: verification.error
|
4675
|
+
details: fromError(verification.error).toString()
|
4104
4676
|
}
|
4105
4677
|
};
|
4106
4678
|
}
|
@@ -4260,14 +4832,14 @@ function guessMimeTypeFromPath(filePath) {
|
|
4260
4832
|
return null;
|
4261
4833
|
}
|
4262
4834
|
|
4263
|
-
async function createValServer(valModules, route, opts, callbacks, formatter) {
|
4264
|
-
const valServerConfig = await initHandlerOptions(route, opts);
|
4265
|
-
return
|
4835
|
+
async function createValServer(valModules, route, opts, config, callbacks, formatter) {
|
4836
|
+
const valServerConfig = await initHandlerOptions(route, opts, config);
|
4837
|
+
return ValServer(valModules, {
|
4266
4838
|
formatter,
|
4267
4839
|
...valServerConfig
|
4268
4840
|
}, callbacks);
|
4269
4841
|
}
|
4270
|
-
async function initHandlerOptions(route, opts) {
|
4842
|
+
async function initHandlerOptions(route, opts, config) {
|
4271
4843
|
const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
|
4272
4844
|
const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
|
4273
4845
|
const isProxyMode = opts.mode === "proxy" || opts.mode === undefined && (maybeApiKey || maybeValSecret);
|
@@ -4312,7 +4884,8 @@ async function initHandlerOptions(route, opts) {
|
|
4312
4884
|
valEnableRedirectUrl,
|
4313
4885
|
valDisableRedirectUrl,
|
4314
4886
|
valContentUrl,
|
4315
|
-
valBuildUrl
|
4887
|
+
valBuildUrl,
|
4888
|
+
config
|
4316
4889
|
};
|
4317
4890
|
} else {
|
4318
4891
|
const cwd = process.cwd();
|
@@ -4326,7 +4899,8 @@ async function initHandlerOptions(route, opts) {
|
|
4326
4899
|
valBuildUrl,
|
4327
4900
|
apiKey: maybeApiKey,
|
4328
4901
|
valSecret: maybeValSecret,
|
4329
|
-
project: maybeValProject
|
4902
|
+
project: maybeValProject,
|
4903
|
+
config
|
4330
4904
|
};
|
4331
4905
|
}
|
4332
4906
|
}
|
@@ -4387,17 +4961,9 @@ async function readCommit(gitDir, branchName) {
|
|
4387
4961
|
return undefined;
|
4388
4962
|
}
|
4389
4963
|
}
|
4390
|
-
const {
|
4391
|
-
VAL_SESSION_COOKIE,
|
4392
|
-
VAL_STATE_COOKIE
|
4393
|
-
} = Internal;
|
4394
|
-
const TREE_PATH_PREFIX = "/tree/~";
|
4395
|
-
const PATCHES_PATH_PREFIX = "/patches/~";
|
4396
|
-
const FILES_PATH_PREFIX = "/files";
|
4397
4964
|
function createValApiRouter(route, valServerPromise, convert) {
|
4398
4965
|
const uiRequestHandler = createUIRequestHandler();
|
4399
4966
|
return async req => {
|
4400
|
-
var _req$method;
|
4401
4967
|
const valServer = await valServerPromise;
|
4402
4968
|
const url = new URL(req.url);
|
4403
4969
|
if (!url.pathname.startsWith(route)) {
|
@@ -4411,115 +4977,235 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
4411
4977
|
json: error
|
4412
4978
|
});
|
4413
4979
|
}
|
4414
|
-
const
|
4415
|
-
|
4416
|
-
|
4417
|
-
|
4418
|
-
|
4419
|
-
|
4420
|
-
|
4421
|
-
|
4422
|
-
|
4423
|
-
|
4424
|
-
|
4425
|
-
|
4980
|
+
const path = url.pathname.slice(route.length);
|
4981
|
+
const groupQueryParams = arr => {
|
4982
|
+
const map = {};
|
4983
|
+
for (const [key, value] of arr) {
|
4984
|
+
const list = map[key] || [];
|
4985
|
+
list.push(value);
|
4986
|
+
map[key] = list;
|
4987
|
+
}
|
4988
|
+
return map;
|
4989
|
+
};
|
4990
|
+
async function getValServerResponse(reqApiRoutePath, req) {
|
4991
|
+
var _req$method, _anyApi$route, _anyValServer$route;
|
4992
|
+
const anyApi =
|
4993
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
4994
|
+
Api;
|
4995
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
4996
|
+
const anyValServer = valServer;
|
4997
|
+
const method = (_req$method = req.method) === null || _req$method === void 0 ? void 0 : _req$method.toUpperCase();
|
4998
|
+
let route = null;
|
4999
|
+
let path = undefined;
|
5000
|
+
for (const routeDef of Object.keys(Api)) {
|
5001
|
+
if (routeDef === reqApiRoutePath) {
|
5002
|
+
route = routeDef;
|
5003
|
+
break;
|
5004
|
+
}
|
5005
|
+
if (reqApiRoutePath.startsWith(routeDef)) {
|
5006
|
+
var _anyApi$routeDef;
|
5007
|
+
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;
|
5008
|
+
if (reqDefinition) {
|
5009
|
+
route = routeDef;
|
5010
|
+
if (reqDefinition.path) {
|
5011
|
+
const subPath = reqApiRoutePath.slice(routeDef.length);
|
5012
|
+
const pathRes = reqDefinition.path.safeParse(subPath);
|
5013
|
+
if (!pathRes.success) {
|
5014
|
+
return zodErrorResult(pathRes.error, `invalid path: '${subPath}' endpoint: '${routeDef}'`);
|
5015
|
+
} else {
|
5016
|
+
path = pathRes.data;
|
4426
5017
|
}
|
4427
|
-
}
|
5018
|
+
}
|
5019
|
+
break;
|
4428
5020
|
}
|
4429
|
-
|
4430
|
-
|
4431
|
-
|
4432
|
-
|
5021
|
+
}
|
5022
|
+
}
|
5023
|
+
if (!route) {
|
5024
|
+
return {
|
5025
|
+
status: 404,
|
5026
|
+
json: {
|
5027
|
+
message: "Route not found. Valid routes are: " + Object.keys(Api),
|
5028
|
+
details: {
|
5029
|
+
route,
|
5030
|
+
method
|
4433
5031
|
}
|
4434
|
-
}
|
5032
|
+
}
|
5033
|
+
};
|
5034
|
+
}
|
5035
|
+
const apiEndpoint = anyApi === null || anyApi === void 0 || (_anyApi$route = anyApi[route]) === null || _anyApi$route === void 0 ? void 0 : _anyApi$route[method];
|
5036
|
+
const reqDefinition = apiEndpoint === null || apiEndpoint === void 0 ? void 0 : apiEndpoint.req;
|
5037
|
+
if (!reqDefinition) {
|
5038
|
+
return {
|
5039
|
+
status: 404,
|
5040
|
+
json: {
|
5041
|
+
message: `Requested method ${method} on route ${route} is not valid. Valid methods are: ${Object.keys(anyApi[route]).join(", ")}`,
|
5042
|
+
details: {
|
5043
|
+
route,
|
5044
|
+
method
|
5045
|
+
}
|
5046
|
+
}
|
5047
|
+
};
|
5048
|
+
}
|
5049
|
+
const endpointImpl = anyValServer === null || anyValServer === void 0 || (_anyValServer$route = anyValServer[route]) === null || _anyValServer$route === void 0 ? void 0 : _anyValServer$route[method];
|
5050
|
+
if (!endpointImpl) {
|
5051
|
+
return {
|
5052
|
+
status: 500,
|
5053
|
+
json: {
|
5054
|
+
message: "Missing server implementation of route with method. This might be caused by a mismatch between Val package versions.",
|
5055
|
+
details: {
|
5056
|
+
valid: {
|
5057
|
+
route: {
|
5058
|
+
server: Object.keys(anyValServer || {}),
|
5059
|
+
api: Object.keys(anyApi || {})
|
5060
|
+
},
|
5061
|
+
method: {
|
5062
|
+
server: Object.keys((anyValServer === null || anyValServer === void 0 ? void 0 : anyValServer[route]) || {}),
|
5063
|
+
api: Object.keys((anyApi === null || anyApi === void 0 ? void 0 : anyApi[route]) || {})
|
5064
|
+
}
|
5065
|
+
},
|
5066
|
+
route,
|
5067
|
+
method
|
5068
|
+
}
|
5069
|
+
}
|
5070
|
+
};
|
5071
|
+
}
|
5072
|
+
let bodyRes;
|
5073
|
+
try {
|
5074
|
+
bodyRes = reqDefinition.body ? reqDefinition.body.safeParse(await req.json()) : {
|
5075
|
+
success: true,
|
5076
|
+
data: {}
|
5077
|
+
};
|
5078
|
+
if (!bodyRes.success) {
|
5079
|
+
return zodErrorResult(bodyRes.error, "invalid body data");
|
4435
5080
|
}
|
5081
|
+
} catch (e) {
|
5082
|
+
return {
|
5083
|
+
status: 400,
|
5084
|
+
json: {
|
5085
|
+
message: "Could not parse request body",
|
5086
|
+
details: {
|
5087
|
+
error: JSON.stringify(e)
|
5088
|
+
}
|
5089
|
+
}
|
5090
|
+
};
|
5091
|
+
}
|
5092
|
+
const cookiesRes = reqDefinition.cookies ? getCookies(req, reqDefinition.cookies) : {
|
5093
|
+
success: true,
|
5094
|
+
data: {}
|
4436
5095
|
};
|
5096
|
+
if (!cookiesRes.success) {
|
5097
|
+
return zodErrorResult(cookiesRes.error, "invalid cookies");
|
5098
|
+
}
|
5099
|
+
const actualQueryParams = groupQueryParams(Array.from(url.searchParams.entries()));
|
5100
|
+
let query = {};
|
5101
|
+
if (reqDefinition.query) {
|
5102
|
+
// This is code is particularly heavy, however
|
5103
|
+
// @see ValidQueryParamTypes in ApiRouter.ts where we explain what we want to support
|
5104
|
+
// We prioritized a declarative ApiRouter, so this code is what we ended up with for better of worse
|
5105
|
+
const queryRules = {};
|
5106
|
+
for (const [key, zodRule] of Object.entries(reqDefinition.query)) {
|
5107
|
+
let innerType = zodRule;
|
5108
|
+
let isOptional = false;
|
5109
|
+
let isArray = false;
|
5110
|
+
// extract inner types:
|
5111
|
+
if (innerType instanceof z.ZodOptional) {
|
5112
|
+
isOptional = true;
|
5113
|
+
innerType = innerType.unwrap();
|
5114
|
+
}
|
5115
|
+
if (innerType instanceof z.ZodArray) {
|
5116
|
+
isArray = true;
|
5117
|
+
innerType = innerType.element;
|
5118
|
+
}
|
5119
|
+
// convert boolean to union of literals true and false so we can parse it as a string
|
5120
|
+
if (innerType instanceof z.ZodBoolean) {
|
5121
|
+
innerType = z.union([z.literal("true"), z.literal("false")]).transform(arg => arg === "true");
|
5122
|
+
}
|
5123
|
+
// re-build rules:
|
5124
|
+
let arrayCompatibleRule = innerType;
|
5125
|
+
arrayCompatibleRule = z.array(innerType); // we always want to parse an array because we group the query params by into an array
|
5126
|
+
if (isOptional) {
|
5127
|
+
arrayCompatibleRule = arrayCompatibleRule.optional();
|
5128
|
+
}
|
5129
|
+
if (!isArray) {
|
5130
|
+
arrayCompatibleRule = arrayCompatibleRule.transform(arg => arg && arg[0]);
|
5131
|
+
}
|
5132
|
+
queryRules[key] = arrayCompatibleRule;
|
5133
|
+
}
|
5134
|
+
const queryRes = z.object(queryRules).safeParse(actualQueryParams);
|
5135
|
+
if (!queryRes.success) {
|
5136
|
+
return zodErrorResult(queryRes.error, `invalid query params: (${JSON.stringify(actualQueryParams)})`);
|
5137
|
+
}
|
5138
|
+
query = queryRes.data;
|
5139
|
+
}
|
5140
|
+
const res = await endpointImpl({
|
5141
|
+
body: bodyRes.data,
|
5142
|
+
cookies: cookiesRes.data,
|
5143
|
+
query,
|
5144
|
+
path
|
5145
|
+
});
|
5146
|
+
if (res.status === 500) {
|
5147
|
+
var _res$json;
|
5148
|
+
return {
|
5149
|
+
status: 500,
|
5150
|
+
json: {
|
5151
|
+
message: ((_res$json = res.json) === null || _res$json === void 0 ? void 0 : _res$json.message) || "Internal Server Error"
|
5152
|
+
}
|
5153
|
+
};
|
5154
|
+
}
|
5155
|
+
const resDef = apiEndpoint.res;
|
5156
|
+
if (resDef) {
|
5157
|
+
const responseResult = resDef.safeParse(res);
|
5158
|
+
if (!responseResult.success) {
|
5159
|
+
return {
|
5160
|
+
status: 500,
|
5161
|
+
json: {
|
5162
|
+
message: "Could not validate response. This is likely a bug in Val server.",
|
5163
|
+
details: {
|
5164
|
+
response: res,
|
5165
|
+
errors: formatZodErrorString(responseResult.error)
|
5166
|
+
}
|
5167
|
+
}
|
5168
|
+
};
|
5169
|
+
}
|
5170
|
+
}
|
5171
|
+
return res;
|
4437
5172
|
}
|
4438
|
-
const path = url.pathname.slice(route.length);
|
4439
5173
|
if (path.startsWith("/static")) {
|
4440
5174
|
return convert(await uiRequestHandler(path.slice("/static".length), url.href));
|
4441
|
-
} else if (path === "/session") {
|
4442
|
-
return convert(await valServer.session(getCookies(req, [VAL_SESSION_COOKIE])));
|
4443
|
-
} else if (path === "/authorize") {
|
4444
|
-
return convert(await valServer.authorize({
|
4445
|
-
redirect_to: url.searchParams.get("redirect_to") || undefined
|
4446
|
-
}));
|
4447
|
-
} else if (path === "/callback") {
|
4448
|
-
return convert(await valServer.callback({
|
4449
|
-
code: url.searchParams.get("code") || undefined,
|
4450
|
-
state: url.searchParams.get("state") || undefined
|
4451
|
-
}, getCookies(req, [VAL_STATE_COOKIE])));
|
4452
|
-
} else if (path === "/logout") {
|
4453
|
-
return convert(await valServer.logout());
|
4454
|
-
} else if (path === "/enable") {
|
4455
|
-
return convert(await valServer.enable({
|
4456
|
-
redirect_to: url.searchParams.get("redirect_to") || undefined
|
4457
|
-
}));
|
4458
|
-
} else if (path === "/disable") {
|
4459
|
-
return convert(await valServer.disable({
|
4460
|
-
redirect_to: url.searchParams.get("redirect_to") || undefined
|
4461
|
-
}));
|
4462
|
-
} else if (method === "POST" && path === "/save") {
|
4463
|
-
const body = await req.json();
|
4464
|
-
return convert(await valServer.postSave(body, getCookies(req, [VAL_SESSION_COOKIE])));
|
4465
|
-
// } else if (method === "POST" && path === "/validate") {
|
4466
|
-
// const body = (await req.json()) as unknown;
|
4467
|
-
// return convert(
|
4468
|
-
// await valServer.postValidate(
|
4469
|
-
// body,
|
4470
|
-
// getCookies(req, [VAL_SESSION_COOKIE]),
|
4471
|
-
// requestHeaders
|
4472
|
-
// )
|
4473
|
-
// );
|
4474
|
-
} else if (method === "GET" && path === "/schema") {
|
4475
|
-
return convert(await valServer.getSchema(getCookies(req, [VAL_SESSION_COOKIE])));
|
4476
|
-
} else if (method === "PUT" && path.startsWith(TREE_PATH_PREFIX)) {
|
4477
|
-
return withTreePath(path, TREE_PATH_PREFIX)(async treePath => convert(await valServer.putTree(await req.json(), treePath, {
|
4478
|
-
patches_sha: url.searchParams.get("patches_sha") || undefined,
|
4479
|
-
validate_all: url.searchParams.get("validate_all") || undefined,
|
4480
|
-
validate_binary_files: url.searchParams.get("validate_binary_files") || undefined,
|
4481
|
-
validate_sources: url.searchParams.get("validate_sources") || undefined
|
4482
|
-
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
4483
|
-
} else if (method === "GET" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
4484
|
-
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.getPatches({
|
4485
|
-
authors: url.searchParams.getAll("author"),
|
4486
|
-
patchIds: url.searchParams.getAll("patch_id"),
|
4487
|
-
omitPatch: url.searchParams.get("omit_patch") || undefined
|
4488
|
-
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
4489
|
-
} else if (method === "DELETE" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
4490
|
-
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.deletePatches({
|
4491
|
-
id: url.searchParams.getAll("id")
|
4492
|
-
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
4493
|
-
} else if (method === "GET" && path.startsWith(FILES_PATH_PREFIX)) {
|
4494
|
-
const treePath = path.slice(FILES_PATH_PREFIX.length);
|
4495
|
-
return convert(await valServer.getFiles(treePath, {
|
4496
|
-
patch_id: url.searchParams.get("patch_id") || undefined
|
4497
|
-
}));
|
4498
5175
|
} else {
|
4499
|
-
return convert(
|
4500
|
-
|
4501
|
-
|
4502
|
-
|
4503
|
-
|
4504
|
-
|
4505
|
-
|
4506
|
-
|
4507
|
-
|
4508
|
-
|
5176
|
+
return convert(await getValServerResponse(path, req));
|
5177
|
+
}
|
5178
|
+
};
|
5179
|
+
}
|
5180
|
+
function formatZodErrorString(error) {
|
5181
|
+
const errors = fromZodError(error).toString();
|
5182
|
+
return errors.length > 640 ? `${errors.slice(0, 640)}...` : errors;
|
5183
|
+
}
|
5184
|
+
function zodErrorResult(error, message) {
|
5185
|
+
return {
|
5186
|
+
status: 400,
|
5187
|
+
json: {
|
5188
|
+
message: "Bad Request: " + message,
|
5189
|
+
details: {
|
5190
|
+
errors: formatZodErrorString(error)
|
5191
|
+
}
|
4509
5192
|
}
|
4510
5193
|
};
|
4511
5194
|
}
|
4512
5195
|
|
4513
5196
|
// TODO: is this naive implementation is too naive?
|
4514
|
-
function getCookies(req,
|
4515
|
-
var _req$headers
|
4516
|
-
|
4517
|
-
|
4518
|
-
|
4519
|
-
|
4520
|
-
|
4521
|
-
|
4522
|
-
|
5197
|
+
function getCookies(req, cookiesDef) {
|
5198
|
+
var _req$headers;
|
5199
|
+
const input = {};
|
5200
|
+
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("; ");
|
5201
|
+
for (const name of Object.keys(cookiesDef)) {
|
5202
|
+
const cookie = cookieParts === null || cookieParts === void 0 ? void 0 : cookieParts.find(cookie => cookie.startsWith(`${name}=`));
|
5203
|
+
const value = cookie ? decodeURIComponent(cookie === null || cookie === void 0 ? void 0 : cookie.split("=")[1]) : undefined;
|
5204
|
+
if (value) {
|
5205
|
+
input[name.trim()] = value;
|
5206
|
+
}
|
5207
|
+
}
|
5208
|
+
return z.object(cookiesDef).safeParse(input);
|
4523
5209
|
}
|
4524
5210
|
|
4525
5211
|
/**
|
@@ -4562,6 +5248,49 @@ class ValFSHost {
|
|
4562
5248
|
}
|
4563
5249
|
}
|
4564
5250
|
|
5251
|
+
async function extractImageMetadata(filename, input) {
|
5252
|
+
const imageSize = sizeOf(input);
|
5253
|
+
let mimeType = null;
|
5254
|
+
if (imageSize.type) {
|
5255
|
+
const possibleMimeType = `image/${imageSize.type}`;
|
5256
|
+
if (Internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
|
5257
|
+
mimeType = possibleMimeType;
|
5258
|
+
}
|
5259
|
+
const filenameBasedLookup = Internal.filenameToMimeType(filename);
|
5260
|
+
if (filenameBasedLookup) {
|
5261
|
+
mimeType = filenameBasedLookup;
|
5262
|
+
}
|
5263
|
+
}
|
5264
|
+
if (!mimeType) {
|
5265
|
+
mimeType = "application/octet-stream";
|
5266
|
+
}
|
5267
|
+
let {
|
5268
|
+
width,
|
5269
|
+
height
|
5270
|
+
} = imageSize;
|
5271
|
+
if (!width || !height) {
|
5272
|
+
width = 0;
|
5273
|
+
height = 0;
|
5274
|
+
}
|
5275
|
+
return {
|
5276
|
+
width,
|
5277
|
+
height,
|
5278
|
+
mimeType
|
5279
|
+
};
|
5280
|
+
}
|
5281
|
+
async function extractFileMetadata(filename,
|
5282
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
5283
|
+
_input // TODO: use buffer to determine mimetype
|
5284
|
+
) {
|
5285
|
+
let mimeType = Internal.filenameToMimeType(filename);
|
5286
|
+
if (!mimeType) {
|
5287
|
+
mimeType = "application/octet-stream";
|
5288
|
+
}
|
5289
|
+
return {
|
5290
|
+
mimeType
|
5291
|
+
};
|
5292
|
+
}
|
5293
|
+
|
4565
5294
|
function getValidationErrorFileRef(validationError) {
|
4566
5295
|
const maybeRef = validationError.value && typeof validationError.value === "object" && FILE_REF_PROP in validationError.value && typeof validationError.value[FILE_REF_PROP] === "string" ? validationError.value[FILE_REF_PROP] : undefined;
|
4567
5296
|
if (!maybeRef) {
|
@@ -4589,15 +5318,15 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4589
5318
|
throw Error("Cannot fix file without a file reference");
|
4590
5319
|
}
|
4591
5320
|
const filename = fsPath__default.join(config.projectRoot, fileRef);
|
4592
|
-
|
4593
|
-
return extractFileMetadata(fileRef
|
5321
|
+
fs.readFileSync(filename);
|
5322
|
+
return extractFileMetadata(fileRef);
|
4594
5323
|
}
|
4595
5324
|
const remainingErrors = [];
|
4596
5325
|
const patch = [];
|
4597
5326
|
for (const fix of validationError.fixes || []) {
|
4598
5327
|
if (fix === "image:replace-metadata" || fix === "image:add-metadata") {
|
4599
5328
|
const imageMetadata = await getImageMetadata();
|
4600
|
-
if (imageMetadata.width === undefined || imageMetadata.height === undefined
|
5329
|
+
if (imageMetadata.width === undefined || imageMetadata.height === undefined) {
|
4601
5330
|
remainingErrors.push({
|
4602
5331
|
...validationError,
|
4603
5332
|
message: "Failed to get image metadata",
|
@@ -4608,8 +5337,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4608
5337
|
const metadataIsCorrect =
|
4609
5338
|
// metadata is a prop that is an object
|
4610
5339
|
typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
|
4611
|
-
// sha256 is correct
|
4612
|
-
"sha256" in currentValue.metadata && currentValue.metadata.sha256 === imageMetadata.sha256 &&
|
4613
5340
|
// width is correct
|
4614
5341
|
"width" in currentValue.metadata && currentValue.metadata.width === imageMetadata.width &&
|
4615
5342
|
// height is correct
|
@@ -4626,18 +5353,11 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4626
5353
|
value: {
|
4627
5354
|
width: imageMetadata.width,
|
4628
5355
|
height: imageMetadata.height,
|
4629
|
-
sha256: imageMetadata.sha256,
|
4630
5356
|
mimeType: imageMetadata.mimeType
|
4631
5357
|
}
|
4632
5358
|
});
|
4633
5359
|
} else {
|
4634
5360
|
if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
|
4635
|
-
if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== imageMetadata.sha256) {
|
4636
|
-
remainingErrors.push({
|
4637
|
-
message: "Image metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + imageMetadata.sha256 + ".",
|
4638
|
-
fixes: undefined
|
4639
|
-
});
|
4640
|
-
}
|
4641
5361
|
if (!("width" in currentValue.metadata) || currentValue.metadata.width !== imageMetadata.width) {
|
4642
5362
|
remainingErrors.push({
|
4643
5363
|
message: "Image metadata width is incorrect! Found: " + ("width" in currentValue.metadata ? currentValue.metadata.width : "<empty>") + ". Expected: " + imageMetadata.width,
|
@@ -4672,14 +5392,13 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4672
5392
|
value: {
|
4673
5393
|
width: imageMetadata.width,
|
4674
5394
|
height: imageMetadata.height,
|
4675
|
-
sha256: imageMetadata.sha256,
|
4676
5395
|
mimeType: imageMetadata.mimeType
|
4677
5396
|
}
|
4678
5397
|
});
|
4679
5398
|
}
|
4680
5399
|
} else if (fix === "file:add-metadata" || fix === "file:check-metadata") {
|
4681
5400
|
const fileMetadata = await getFileMetadata();
|
4682
|
-
if (fileMetadata
|
5401
|
+
if (fileMetadata === undefined) {
|
4683
5402
|
remainingErrors.push({
|
4684
5403
|
...validationError,
|
4685
5404
|
message: "Failed to get image metadata",
|
@@ -4690,8 +5409,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4690
5409
|
const metadataIsCorrect =
|
4691
5410
|
// metadata is a prop that is an object
|
4692
5411
|
typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
|
4693
|
-
// sha256 is correct
|
4694
|
-
"sha256" in currentValue.metadata && currentValue.metadata.sha256 === fileMetadata.sha256 &&
|
4695
5412
|
// mimeType is correct
|
4696
5413
|
"mimeType" in currentValue.metadata && currentValue.metadata.mimeType === fileMetadata.mimeType;
|
4697
5414
|
|
@@ -4702,7 +5419,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4702
5419
|
op: "replace",
|
4703
5420
|
path: sourceToPatchPath(sourcePath).concat("metadata"),
|
4704
5421
|
value: {
|
4705
|
-
sha256: fileMetadata.sha256,
|
4706
5422
|
...(fileMetadata.mimeType ? {
|
4707
5423
|
mimeType: fileMetadata.mimeType
|
4708
5424
|
} : {})
|
@@ -4710,12 +5426,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4710
5426
|
});
|
4711
5427
|
} else {
|
4712
5428
|
if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
|
4713
|
-
if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== fileMetadata.sha256) {
|
4714
|
-
remainingErrors.push({
|
4715
|
-
message: "File metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + fileMetadata.sha256 + ".",
|
4716
|
-
fixes: undefined
|
4717
|
-
});
|
4718
|
-
}
|
4719
5429
|
if (!("mimeType" in currentValue.metadata) || currentValue.metadata.mimeType !== fileMetadata.mimeType) {
|
4720
5430
|
remainingErrors.push({
|
4721
5431
|
message: "File metadata mimeType is incorrect! Found: " + ("mimeType" in currentValue.metadata ? currentValue.metadata.mimeType : "<empty>") + ". Expected: " + fileMetadata.mimeType,
|
@@ -4736,7 +5446,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4736
5446
|
op: "add",
|
4737
5447
|
path: sourceToPatchPath(sourcePath).concat("metadata"),
|
4738
5448
|
value: {
|
4739
|
-
sha256: fileMetadata.sha256,
|
4740
5449
|
...(fileMetadata.mimeType ? {
|
4741
5450
|
mimeType: fileMetadata.mimeType
|
4742
5451
|
} : {})
|