@valbuild/server 0.64.0 → 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/ValRouter.d.ts +1 -1
- package/dist/declarations/src/ValServer.d.ts +4 -2
- package/dist/valbuild-server.cjs.d.ts +2 -2
- package/dist/valbuild-server.cjs.dev.js +797 -240
- package/dist/valbuild-server.cjs.prod.js +797 -240
- package/dist/valbuild-server.esm.js +797 -240
- package/package.json +5 -4
- package/dist/valbuild-server.cjs.d.ts.map +0 -1
@@ -1273,56 +1273,6 @@ function encodeJwt(payload, sessionKey) {
|
|
1273
1273
|
return `${jwtHeaderBase64}.${payloadBase64}.${crypto__default["default"].createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
|
1274
1274
|
}
|
1275
1275
|
|
1276
|
-
const textEncoder$2 = new TextEncoder();
|
1277
|
-
async function extractImageMetadata(filename, input) {
|
1278
|
-
const imageSize = sizeOf__default["default"](input);
|
1279
|
-
let mimeType = null;
|
1280
|
-
if (imageSize.type) {
|
1281
|
-
const possibleMimeType = `image/${imageSize.type}`;
|
1282
|
-
if (core.Internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
|
1283
|
-
mimeType = possibleMimeType;
|
1284
|
-
}
|
1285
|
-
const filenameBasedLookup = core.Internal.filenameToMimeType(filename);
|
1286
|
-
if (filenameBasedLookup) {
|
1287
|
-
mimeType = filenameBasedLookup;
|
1288
|
-
}
|
1289
|
-
}
|
1290
|
-
if (!mimeType) {
|
1291
|
-
mimeType = "application/octet-stream";
|
1292
|
-
}
|
1293
|
-
let {
|
1294
|
-
width,
|
1295
|
-
height
|
1296
|
-
} = imageSize;
|
1297
|
-
if (!width || !height) {
|
1298
|
-
width = 0;
|
1299
|
-
height = 0;
|
1300
|
-
}
|
1301
|
-
const sha256 = getSha256(mimeType, input);
|
1302
|
-
return {
|
1303
|
-
width,
|
1304
|
-
height,
|
1305
|
-
sha256,
|
1306
|
-
mimeType
|
1307
|
-
};
|
1308
|
-
}
|
1309
|
-
function getSha256(mimeType, input) {
|
1310
|
-
return core.Internal.getSHA256Hash(textEncoder$2.encode(
|
1311
|
-
// TODO: we should probably store the mimetype in the metadata and reuse it here
|
1312
|
-
`data:${mimeType};base64,${input.toString("base64")}`));
|
1313
|
-
}
|
1314
|
-
async function extractFileMetadata(filename, input) {
|
1315
|
-
let mimeType = core.Internal.filenameToMimeType(filename);
|
1316
|
-
if (!mimeType) {
|
1317
|
-
mimeType = "application/octet-stream";
|
1318
|
-
}
|
1319
|
-
const sha256 = getSha256(mimeType, input);
|
1320
|
-
return {
|
1321
|
-
sha256,
|
1322
|
-
mimeType
|
1323
|
-
};
|
1324
|
-
}
|
1325
|
-
|
1326
1276
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
1327
1277
|
const textEncoder$1 = new TextEncoder();
|
1328
1278
|
const jsonOps = new patch.JSONOps();
|
@@ -1351,15 +1301,20 @@ class ValOps {
|
|
1351
1301
|
this.modulesErrors = null;
|
1352
1302
|
}
|
1353
1303
|
hash(input) {
|
1354
|
-
|
1355
|
-
if (typeof input === "string") {
|
1356
|
-
str = input;
|
1357
|
-
} else {
|
1358
|
-
str = JSON.stringify(input);
|
1359
|
-
}
|
1360
|
-
return core.Internal.getSHA256Hash(textEncoder$1.encode(str));
|
1304
|
+
return core.Internal.getSHA256Hash(textEncoder$1.encode(input));
|
1361
1305
|
}
|
1362
1306
|
|
1307
|
+
// #region stat
|
1308
|
+
/**
|
1309
|
+
* Get the status from Val
|
1310
|
+
*
|
1311
|
+
* This works differently in ValOpsFS and ValOpsHttp:
|
1312
|
+
* - In ValOpsFS (for dev mode) works using long-polling operations since we cannot use WebSockets in the host Next.js server and we do not want to hammer the server with requests (though we could argue that it would be ok in dev, it is not up to our standards as a kick-ass CMS).
|
1313
|
+
* - In ValOpsHttp (in production) it returns a WebSocket URL so that the client can connect directly.
|
1314
|
+
*
|
1315
|
+
* The reason we do not use long polling in production is that Vercel (a very likely host for Next.js), bills by wall time and long polling would therefore be very expensive.
|
1316
|
+
*/
|
1317
|
+
|
1363
1318
|
// #region initTree
|
1364
1319
|
async initTree() {
|
1365
1320
|
if (this.baseSha === null || this.schemaSha === null || this.sources === null || this.schemas === null || this.modulesErrors === null) {
|
@@ -1416,17 +1371,25 @@ class ValOps {
|
|
1416
1371
|
addModuleError(`source in ${path} is undefined`, moduleIdx, path);
|
1417
1372
|
return;
|
1418
1373
|
}
|
1374
|
+
let serializedSchema;
|
1375
|
+
try {
|
1376
|
+
serializedSchema = schema.serialize();
|
1377
|
+
} catch (e) {
|
1378
|
+
const message = e instanceof Error ? e.message : JSON.stringify(e);
|
1379
|
+
addModuleError(`Could not serialize module: '${path}'. Error: ${message}`, moduleIdx, path);
|
1380
|
+
return;
|
1381
|
+
}
|
1419
1382
|
const pathM = path;
|
1420
1383
|
currentSources[pathM] = source;
|
1421
1384
|
currentSchemas[pathM] = schema;
|
1422
1385
|
// make sure the checks above is enough that this does not fail - even if val modules are not set up correctly
|
1423
|
-
baseSha
|
1386
|
+
baseSha = this.hash(baseSha + JSON.stringify({
|
1424
1387
|
path,
|
1425
|
-
schema:
|
1388
|
+
schema: serializedSchema,
|
1426
1389
|
source,
|
1427
1390
|
modulesErrors: currentModulesErrors
|
1428
|
-
});
|
1429
|
-
schemaSha
|
1391
|
+
}));
|
1392
|
+
schemaSha = this.hash(schemaSha + JSON.stringify(serializedSchema));
|
1430
1393
|
});
|
1431
1394
|
}
|
1432
1395
|
this.sources = currentSources;
|
@@ -1622,25 +1585,27 @@ class ValOps {
|
|
1622
1585
|
}
|
1623
1586
|
for (const [sourcePathS, validationErrors] of Object.entries(res)) {
|
1624
1587
|
const sourcePath = sourcePathS;
|
1625
|
-
|
1626
|
-
|
1627
|
-
if (
|
1628
|
-
|
1629
|
-
|
1630
|
-
|
1631
|
-
|
1632
|
-
|
1633
|
-
|
1634
|
-
|
1635
|
-
|
1636
|
-
errors[path]
|
1637
|
-
|
1638
|
-
|
1639
|
-
|
1640
|
-
|
1641
|
-
errors[path].validations[sourcePath]
|
1588
|
+
if (validationErrors) {
|
1589
|
+
for (const validationError of validationErrors) {
|
1590
|
+
if (isOnlyFileCheckValidationError(validationError)) {
|
1591
|
+
if (files[sourcePath]) {
|
1592
|
+
throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
|
1593
|
+
}
|
1594
|
+
const value = validationError.value;
|
1595
|
+
if (isFileSource(value)) {
|
1596
|
+
files[sourcePath] = value;
|
1597
|
+
}
|
1598
|
+
} else {
|
1599
|
+
if (!errors[path]) {
|
1600
|
+
errors[path] = {
|
1601
|
+
validations: {}
|
1602
|
+
};
|
1603
|
+
}
|
1604
|
+
if (!errors[path].validations[sourcePath]) {
|
1605
|
+
errors[path].validations[sourcePath] = [];
|
1606
|
+
}
|
1607
|
+
errors[path].validations[sourcePath].push(validationError);
|
1642
1608
|
}
|
1643
|
-
errors[path].validations[sourcePath].push(validationError);
|
1644
1609
|
}
|
1645
1610
|
}
|
1646
1611
|
}
|
@@ -1828,8 +1793,22 @@ class ValOps {
|
|
1828
1793
|
const patchRes = patch.applyPatch(tsSourceFile, tsOps, sourceFileOps);
|
1829
1794
|
if (fp.result.isErr(patchRes)) {
|
1830
1795
|
if (Array.isArray(patchRes.error)) {
|
1796
|
+
for (const error of patchRes.error) {
|
1797
|
+
console.error("Could not patch", JSON.stringify({
|
1798
|
+
path,
|
1799
|
+
patchId,
|
1800
|
+
error,
|
1801
|
+
sourceFileOps
|
1802
|
+
}, null, 2));
|
1803
|
+
}
|
1831
1804
|
errors.push(...patchRes.error);
|
1832
1805
|
} else {
|
1806
|
+
console.error("Could not patch", JSON.stringify({
|
1807
|
+
path,
|
1808
|
+
patchId,
|
1809
|
+
error: patchRes.error,
|
1810
|
+
sourceFileOps
|
1811
|
+
}, null, 2));
|
1833
1812
|
errors.push(patchRes.error);
|
1834
1813
|
}
|
1835
1814
|
triedPatches.push(patchId);
|
@@ -1923,16 +1902,23 @@ class ValOps {
|
|
1923
1902
|
}
|
1924
1903
|
|
1925
1904
|
// #region createPatch
|
1926
|
-
async createPatch(path, patch$1, authorId) {
|
1927
|
-
const
|
1928
|
-
|
1929
|
-
|
1930
|
-
|
1931
|
-
|
1905
|
+
async createPatch(path, patchAnalysis, patch$1, authorId) {
|
1906
|
+
const initTree = await this.initTree();
|
1907
|
+
const schemas = initTree.schemas;
|
1908
|
+
const moduleErrors = initTree.moduleErrors;
|
1909
|
+
let sources = initTree.sources;
|
1910
|
+
if (patchAnalysis) {
|
1911
|
+
const tree = await this.getTree(patchAnalysis);
|
1912
|
+
sources = {
|
1913
|
+
...sources,
|
1914
|
+
...tree.sources
|
1915
|
+
};
|
1916
|
+
}
|
1932
1917
|
const source = sources[path];
|
1933
1918
|
const schema = schemas[path];
|
1934
1919
|
const moduleError = moduleErrors.find(e => e.path === path);
|
1935
1920
|
if (moduleError) {
|
1921
|
+
console.error(`Cannot patch. Module at path: '${path}' has fatal errors: "${moduleError.message}"`);
|
1936
1922
|
return {
|
1937
1923
|
error: {
|
1938
1924
|
message: `Cannot patch. Module at path: '${path}' has fatal errors: ` + moduleErrors.map(m => `"${m.message}"`).join(" and ")
|
@@ -1940,6 +1926,7 @@ class ValOps {
|
|
1940
1926
|
};
|
1941
1927
|
}
|
1942
1928
|
if (!source) {
|
1929
|
+
console.error(`Cannot patch. Module source at path: '${path}' does not exist`);
|
1943
1930
|
return {
|
1944
1931
|
error: {
|
1945
1932
|
message: `Cannot patch. Module source at path: '${path}' does not exist`
|
@@ -1947,6 +1934,7 @@ class ValOps {
|
|
1947
1934
|
};
|
1948
1935
|
}
|
1949
1936
|
if (!schema) {
|
1937
|
+
console.error(`Cannot patch. Module schema at path: '${path}' does not exist`);
|
1950
1938
|
return {
|
1951
1939
|
error: {
|
1952
1940
|
message: `Cannot patch. Module schema at path: '${path}' does not exist`
|
@@ -1964,10 +1952,12 @@ class ValOps {
|
|
1964
1952
|
filePath
|
1965
1953
|
} = op;
|
1966
1954
|
if (files[filePath]) {
|
1955
|
+
console.error(`Cannot have multiple files with same path in same patch. Path: ${filePath}`);
|
1967
1956
|
files[filePath] = {
|
1968
1957
|
error: new patch.PatchError("Cannot have multiple files with same path in same patch")
|
1969
1958
|
};
|
1970
1959
|
} else if (typeof value !== "string") {
|
1960
|
+
console.error(`Value is not a string. Path: ${filePath}. Value: ${value}`);
|
1971
1961
|
files[filePath] = {
|
1972
1962
|
error: new patch.PatchError("Value is not a string")
|
1973
1963
|
};
|
@@ -1983,15 +1973,14 @@ class ValOps {
|
|
1983
1973
|
path: op.path,
|
1984
1974
|
filePath,
|
1985
1975
|
nestedFilePath: op.nestedFilePath,
|
1986
|
-
value:
|
1987
|
-
sha256
|
1988
|
-
}
|
1976
|
+
value: sha256
|
1989
1977
|
});
|
1990
1978
|
}
|
1991
1979
|
}
|
1992
1980
|
}
|
1993
1981
|
const saveRes = await this.saveSourceFilePatch(path, sourceFileOps, authorId);
|
1994
1982
|
if (saveRes.error) {
|
1983
|
+
console.error(`Could not save source file patch at path: '${path}'. Error: ${saveRes.error.message}`);
|
1995
1984
|
return {
|
1996
1985
|
error: saveRes.error
|
1997
1986
|
};
|
@@ -2015,17 +2004,20 @@ class ValOps {
|
|
2015
2004
|
? "image" : schemaAtPath instanceof core.FileSchema ? "file" : schemaAtPath.serialize().type;
|
2016
2005
|
} catch (e) {
|
2017
2006
|
if (e instanceof Error) {
|
2007
|
+
console.error(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`);
|
2018
2008
|
return {
|
2019
2009
|
filePath,
|
2020
2010
|
error: new patch.PatchError(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`)
|
2021
2011
|
};
|
2022
2012
|
}
|
2013
|
+
console.error(`Could not resolve file type at: ${modulePath}. Unknown error.`);
|
2023
2014
|
return {
|
2024
2015
|
filePath,
|
2025
2016
|
error: new patch.PatchError(`Could not resolve file type at: ${modulePath}. Unknown error.`)
|
2026
2017
|
};
|
2027
2018
|
}
|
2028
2019
|
if (type !== "image" && type !== "file") {
|
2020
|
+
console.error("Unknown file type (resolved from schema): " + type);
|
2029
2021
|
return {
|
2030
2022
|
filePath,
|
2031
2023
|
error: new patch.PatchError("Unknown file type (resolved from schema): " + type)
|
@@ -2033,6 +2025,7 @@ class ValOps {
|
|
2033
2025
|
}
|
2034
2026
|
const mimeType = getMimeTypeFromBase64(data.value);
|
2035
2027
|
if (!mimeType) {
|
2028
|
+
console.error("Could not get mimeType from base 64 encoded value");
|
2036
2029
|
return {
|
2037
2030
|
filePath,
|
2038
2031
|
error: new patch.PatchError("Could not get mimeType from base 64 encoded value. First chars were: " + data.value.slice(0, 20))
|
@@ -2040,6 +2033,7 @@ class ValOps {
|
|
2040
2033
|
}
|
2041
2034
|
const buffer = bufferFromDataUrl(data.value);
|
2042
2035
|
if (!buffer) {
|
2036
|
+
console.error("Could not create buffer from base 64 encoded value");
|
2043
2037
|
return {
|
2044
2038
|
filePath,
|
2045
2039
|
error: new patch.PatchError("Could not create buffer from base 64 encoded value")
|
@@ -2047,6 +2041,7 @@ class ValOps {
|
|
2047
2041
|
}
|
2048
2042
|
const metadataOps = createMetadataFromBuffer(type, mimeType, buffer);
|
2049
2043
|
if (metadataOps.errors) {
|
2044
|
+
console.error(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`);
|
2050
2045
|
return {
|
2051
2046
|
filePath,
|
2052
2047
|
error: new patch.PatchError(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`)
|
@@ -2068,6 +2063,14 @@ class ValOps {
|
|
2068
2063
|
};
|
2069
2064
|
}
|
2070
2065
|
}));
|
2066
|
+
const errors = saveFileRes.filter(f => !!f.error);
|
2067
|
+
if (errors.length > 0) {
|
2068
|
+
return {
|
2069
|
+
error: {
|
2070
|
+
message: "Could not save patch: " + errors.map(e => e.error.message).join(", ")
|
2071
|
+
}
|
2072
|
+
};
|
2073
|
+
}
|
2071
2074
|
return {
|
2072
2075
|
patchId,
|
2073
2076
|
files: saveFileRes,
|
@@ -2092,14 +2095,13 @@ function isFileSource(value) {
|
|
2092
2095
|
}
|
2093
2096
|
function getFieldsForType(type) {
|
2094
2097
|
if (type === "file") {
|
2095
|
-
return ["
|
2098
|
+
return ["mimeType"];
|
2096
2099
|
} else if (type === "image") {
|
2097
|
-
return ["
|
2100
|
+
return ["mimeType", "height", "width"];
|
2098
2101
|
}
|
2099
2102
|
throw new Error("Unknown type: " + type);
|
2100
2103
|
}
|
2101
2104
|
function createMetadataFromBuffer(type, mimeType, buffer) {
|
2102
|
-
const sha256 = getSha256(mimeType, buffer);
|
2103
2105
|
const errors = [];
|
2104
2106
|
let availableMetadata;
|
2105
2107
|
if (type === "image") {
|
@@ -2108,7 +2110,7 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
|
|
2108
2110
|
height,
|
2109
2111
|
type
|
2110
2112
|
} = sizeOf__default["default"](buffer);
|
2111
|
-
const normalizedType = type === "jpg" ? "jpeg" : type;
|
2113
|
+
const normalizedType = type === "jpg" ? "jpeg" : type === "svg" ? "svg+xml" : type;
|
2112
2114
|
if (type !== undefined && `image/${normalizedType}` !== mimeType) {
|
2113
2115
|
return {
|
2114
2116
|
errors: [{
|
@@ -2117,14 +2119,12 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
|
|
2117
2119
|
};
|
2118
2120
|
}
|
2119
2121
|
availableMetadata = {
|
2120
|
-
sha256: sha256,
|
2121
2122
|
mimeType,
|
2122
2123
|
height,
|
2123
2124
|
width
|
2124
2125
|
};
|
2125
2126
|
} else {
|
2126
2127
|
availableMetadata = {
|
2127
|
-
sha256: sha256,
|
2128
2128
|
mimeType
|
2129
2129
|
};
|
2130
2130
|
}
|
@@ -2244,9 +2244,7 @@ const OperationT = z__default["default"].discriminatedUnion("op", [z__default["d
|
|
2244
2244
|
path: z__default["default"].array(z__default["default"].string()),
|
2245
2245
|
filePath: z__default["default"].string(),
|
2246
2246
|
nestedFilePath: z__default["default"].array(z__default["default"].string()).optional(),
|
2247
|
-
value: z__default["default"].
|
2248
|
-
sha256: z__default["default"].string()
|
2249
|
-
})])
|
2247
|
+
value: z__default["default"].string()
|
2250
2248
|
}).strict()]);
|
2251
2249
|
const Patch = z__default["default"].array(OperationT);
|
2252
2250
|
|
@@ -2260,6 +2258,161 @@ class ValOpsFS extends ValOps {
|
|
2260
2258
|
async onInit() {
|
2261
2259
|
// do nothing
|
2262
2260
|
}
|
2261
|
+
async getStat(params) {
|
2262
|
+
// In ValOpsFS, we don't have a websocket server to listen to file changes so we use long-polling.
|
2263
|
+
// If a file that Val depends on changes, we break the connection and tell the client to request again to get the latest values.
|
2264
|
+
try {
|
2265
|
+
var _this$options, _this$options2;
|
2266
|
+
const currentBaseSha = await this.getBaseSha();
|
2267
|
+
const currentSchemaSha = await this.getSchemaSha();
|
2268
|
+
const moduleFilePaths = Object.keys(await this.getSchemas());
|
2269
|
+
const patchData = await this.readPatches();
|
2270
|
+
const patches = [];
|
2271
|
+
// TODO: use proper patch sequences when available:
|
2272
|
+
for (const [patchId] of Object.entries(patchData.patches).sort(([, a], [, b]) => {
|
2273
|
+
return a.createdAt.localeCompare(b.createdAt, undefined);
|
2274
|
+
})) {
|
2275
|
+
patches.push(patchId);
|
2276
|
+
}
|
2277
|
+
// something changed: return immediately
|
2278
|
+
const didChange = !params || currentBaseSha !== params.baseSha || currentSchemaSha !== params.schemaSha || patches.length !== params.patches.length || patches.some((p, i) => p !== params.patches[i]);
|
2279
|
+
if (didChange) {
|
2280
|
+
return {
|
2281
|
+
type: "did-change",
|
2282
|
+
baseSha: currentBaseSha,
|
2283
|
+
schemaSha: currentSchemaSha,
|
2284
|
+
patches
|
2285
|
+
};
|
2286
|
+
}
|
2287
|
+
let fsWatcher = null;
|
2288
|
+
let stopPolling = false;
|
2289
|
+
const didDirectoryChangeUsingPolling = (dir, interval, setHandle) => {
|
2290
|
+
const mtimeInDir = {};
|
2291
|
+
if (fs__default["default"].existsSync(dir)) {
|
2292
|
+
for (const file of fs__default["default"].readdirSync(dir)) {
|
2293
|
+
mtimeInDir[file] = fs__default["default"].statSync(fsPath__namespace["default"].join(dir, file)).mtime.getTime();
|
2294
|
+
}
|
2295
|
+
}
|
2296
|
+
return new Promise(resolve => {
|
2297
|
+
const go = resolve => {
|
2298
|
+
const start = Date.now();
|
2299
|
+
if (fs__default["default"].existsSync(dir)) {
|
2300
|
+
const subDirs = fs__default["default"].readdirSync(dir);
|
2301
|
+
// amount of files changed
|
2302
|
+
if (subDirs.length !== Object.keys(mtimeInDir).length) {
|
2303
|
+
resolve("request-again");
|
2304
|
+
}
|
2305
|
+
for (const file of fs__default["default"].readdirSync(dir)) {
|
2306
|
+
const mtime = fs__default["default"].statSync(fsPath__namespace["default"].join(dir, file)).mtime.getTime();
|
2307
|
+
if (mtime !== mtimeInDir[file]) {
|
2308
|
+
resolve("request-again");
|
2309
|
+
}
|
2310
|
+
}
|
2311
|
+
} else {
|
2312
|
+
// dir had files, but now is deleted
|
2313
|
+
if (Object.keys(mtimeInDir).length > 0) {
|
2314
|
+
resolve("request-again");
|
2315
|
+
}
|
2316
|
+
}
|
2317
|
+
if (Date.now() - start > interval) {
|
2318
|
+
console.warn("Val: polling interval of patches exceeded");
|
2319
|
+
}
|
2320
|
+
if (stopPolling) {
|
2321
|
+
return;
|
2322
|
+
}
|
2323
|
+
setHandle(setTimeout(() => go(resolve), interval));
|
2324
|
+
};
|
2325
|
+
setHandle(setTimeout(() => go(resolve), interval));
|
2326
|
+
});
|
2327
|
+
};
|
2328
|
+
const didFilesChangeUsingPolling = (files, interval, setHandle) => {
|
2329
|
+
const mtimes = {};
|
2330
|
+
for (const file of files) {
|
2331
|
+
if (fs__default["default"].existsSync(file)) {
|
2332
|
+
mtimes[file] = fs__default["default"].statSync(file).mtime.getTime();
|
2333
|
+
} else {
|
2334
|
+
mtimes[file] = -1;
|
2335
|
+
}
|
2336
|
+
}
|
2337
|
+
return new Promise(resolve => {
|
2338
|
+
const go = resolve => {
|
2339
|
+
const start = Date.now();
|
2340
|
+
for (const file of files) {
|
2341
|
+
const mtime = fs__default["default"].existsSync(file) ? fs__default["default"].statSync(file).mtime.getTime() : -1;
|
2342
|
+
if (mtime !== mtimes[file]) {
|
2343
|
+
resolve("request-again");
|
2344
|
+
}
|
2345
|
+
}
|
2346
|
+
if (Date.now() - start > interval) {
|
2347
|
+
console.warn("Val: polling interval of files exceeded");
|
2348
|
+
}
|
2349
|
+
setHandle(setTimeout(() => go(resolve), interval));
|
2350
|
+
};
|
2351
|
+
if (stopPolling) {
|
2352
|
+
return;
|
2353
|
+
}
|
2354
|
+
setHandle(setTimeout(() => go(resolve), interval));
|
2355
|
+
});
|
2356
|
+
};
|
2357
|
+
const statFilePollingInterval = ((_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.statFilePollingInterval) || 250; // relatively low interval, but there would typically not be that many files (less than 1000 at the very least) - hopefully if we have customers with more files than that, we also have devs working on Val that easily can fix this :) Besides this is just the default
|
2358
|
+
const disableFilePolling = ((_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.disableFilePolling) || false;
|
2359
|
+
let patchesDirHandle;
|
2360
|
+
let valFilesIntervalHandle;
|
2361
|
+
const type = await Promise.race([
|
2362
|
+
// we poll the patches directory for changes since fs.watch does not work reliably on all system (in particular on WSL) and just checking the patches dir is relatively cheap
|
2363
|
+
disableFilePolling ? new Promise(() => {}) : didDirectoryChangeUsingPolling(this.getPatchesDir(), statFilePollingInterval, handle => {
|
2364
|
+
patchesDirHandle = handle;
|
2365
|
+
}),
|
2366
|
+
// we poll the files that Val depends on for changes
|
2367
|
+
disableFilePolling ? new Promise(() => {}) : didFilesChangeUsingPolling([fsPath__namespace["default"].join(this.rootDir, "val.config.ts"), fsPath__namespace["default"].join(this.rootDir, "val.modules.ts"), fsPath__namespace["default"].join(this.rootDir, "val.config.js"), fsPath__namespace["default"].join(this.rootDir, "val.modules.js"), ...moduleFilePaths.map(p => fsPath__namespace["default"].join(this.rootDir, p))], statFilePollingInterval, handle => {
|
2368
|
+
valFilesIntervalHandle = handle;
|
2369
|
+
}), new Promise(resolve => {
|
2370
|
+
fsWatcher = fs__default["default"].watch(this.rootDir, {
|
2371
|
+
recursive: true
|
2372
|
+
}, (eventType, filename) => {
|
2373
|
+
if (!filename) {
|
2374
|
+
return;
|
2375
|
+
}
|
2376
|
+
const isChange = filename.startsWith(this.getPatchesDir().slice(this.rootDir.length + 1)) || filename.endsWith(".val.ts") || filename.endsWith(".val.js") || filename.endsWith("val.config.ts") || filename.endsWith("val.config.js") || filename.endsWith("val.modules.ts") || filename.endsWith("val.modules.js");
|
2377
|
+
if (isChange) {
|
2378
|
+
// a file that Val depends on just changed or a patch was created, break connection and request stat again to get the new values
|
2379
|
+
resolve("request-again");
|
2380
|
+
}
|
2381
|
+
});
|
2382
|
+
}), new Promise(resolve => {
|
2383
|
+
var _this$options3;
|
2384
|
+
return setTimeout(() => resolve("no-change"), ((_this$options3 = this.options) === null || _this$options3 === void 0 ? void 0 : _this$options3.statPollingInterval) || 20000);
|
2385
|
+
})]).finally(() => {
|
2386
|
+
if (fsWatcher) {
|
2387
|
+
fsWatcher.close();
|
2388
|
+
}
|
2389
|
+
stopPolling = true;
|
2390
|
+
clearInterval(patchesDirHandle);
|
2391
|
+
clearInterval(valFilesIntervalHandle);
|
2392
|
+
});
|
2393
|
+
return {
|
2394
|
+
type,
|
2395
|
+
baseSha: currentBaseSha,
|
2396
|
+
schemaSha: currentSchemaSha,
|
2397
|
+
patches
|
2398
|
+
};
|
2399
|
+
} catch (err) {
|
2400
|
+
if (err instanceof Error) {
|
2401
|
+
return {
|
2402
|
+
type: "error",
|
2403
|
+
error: {
|
2404
|
+
message: err.message
|
2405
|
+
}
|
2406
|
+
};
|
2407
|
+
}
|
2408
|
+
return {
|
2409
|
+
type: "error",
|
2410
|
+
error: {
|
2411
|
+
message: "Unknown error (getStat)"
|
2412
|
+
}
|
2413
|
+
};
|
2414
|
+
}
|
2415
|
+
}
|
2263
2416
|
async readPatches(includes) {
|
2264
2417
|
const patchesCacheDir = this.getPatchesDir();
|
2265
2418
|
let patchJsonFiles = [];
|
@@ -2268,8 +2421,8 @@ class ValOpsFS extends ValOps {
|
|
2268
2421
|
}
|
2269
2422
|
const patches = {};
|
2270
2423
|
const errors = {};
|
2271
|
-
const
|
2272
|
-
for (const patchIdNum of
|
2424
|
+
const parsedPatchIds = patchJsonFiles.map(file => parseInt(fsPath__namespace["default"].basename(fsPath__namespace["default"].dirname(file)), 10)).sort();
|
2425
|
+
for (const patchIdNum of parsedPatchIds) {
|
2273
2426
|
if (Number.isNaN(patchIdNum)) {
|
2274
2427
|
throw new Error("Could not parse patch id from file name. Files found: " + patchJsonFiles.join(", "));
|
2275
2428
|
}
|
@@ -2310,6 +2463,12 @@ class ValOpsFS extends ValOps {
|
|
2310
2463
|
errors: allErrors,
|
2311
2464
|
patches: allPatches
|
2312
2465
|
} = await this.readPatches(filters.patchIds);
|
2466
|
+
if (allErrors && Object.keys(allErrors).length > 0) {
|
2467
|
+
for (const [patchId, error] of Object.entries(allErrors)) {
|
2468
|
+
console.error("Error reading patch", patchId, error);
|
2469
|
+
errors[patchId] = error;
|
2470
|
+
}
|
2471
|
+
}
|
2313
2472
|
for (const [patchIdS, patch] of Object.entries(allPatches)) {
|
2314
2473
|
const patchId = patchIdS;
|
2315
2474
|
if (filters.authors && !(patch.authorId === null || filters.authors.includes(patch.authorId))) {
|
@@ -2325,10 +2484,6 @@ class ValOpsFS extends ValOps {
|
|
2325
2484
|
authorId: patch.authorId,
|
2326
2485
|
appliedAt: patch.appliedAt
|
2327
2486
|
};
|
2328
|
-
const error = allErrors && allErrors[patchId];
|
2329
|
-
if (error) {
|
2330
|
-
errors[patchId] = error;
|
2331
|
-
}
|
2332
2487
|
}
|
2333
2488
|
if (errors && Object.keys(errors).length > 0) {
|
2334
2489
|
return {
|
@@ -2672,11 +2827,11 @@ class ValOpsFS extends ValOps {
|
|
2672
2827
|
getPatchDir(patchId) {
|
2673
2828
|
return fsPath__namespace["default"].join(this.getPatchesDir(), patchId);
|
2674
2829
|
}
|
2675
|
-
getBinaryFilePath(
|
2676
|
-
return fsPath__namespace["default"].join(this.getPatchDir(patchId), "files",
|
2830
|
+
getBinaryFilePath(filePath, patchId) {
|
2831
|
+
return fsPath__namespace["default"].join(this.getPatchDir(patchId), "files", filePath, fsPath__namespace["default"].basename(filePath));
|
2677
2832
|
}
|
2678
|
-
getBinaryFileMetadataPath(
|
2679
|
-
return fsPath__namespace["default"].join(this.getPatchDir(patchId), "files",
|
2833
|
+
getBinaryFileMetadataPath(filePath, patchId) {
|
2834
|
+
return fsPath__namespace["default"].join(this.getPatchDir(patchId), "files", filePath, "metadata.json");
|
2680
2835
|
}
|
2681
2836
|
getPatchFilePath(patchId) {
|
2682
2837
|
return fsPath__namespace["default"].join(this.getPatchDir(patchId), "patch.json");
|
@@ -2749,12 +2904,10 @@ const BaseSha = z.z.string().refine(s => !!s); // TODO: validate
|
|
2749
2904
|
const AuthorId = z.z.string().refine(s => !!s); // TODO: validate
|
2750
2905
|
const ModuleFilePath = z.z.string().refine(s => !!s); // TODO: validate
|
2751
2906
|
const Metadata = z.z.union([z.z.object({
|
2752
|
-
sha256: z.z.string(),
|
2753
2907
|
mimeType: z.z.string(),
|
2754
2908
|
width: z.z.number(),
|
2755
2909
|
height: z.z.number()
|
2756
2910
|
}), z.z.object({
|
2757
|
-
sha256: z.z.string(),
|
2758
2911
|
mimeType: z.z.string()
|
2759
2912
|
})]);
|
2760
2913
|
const MetadataRes = z.z.object({
|
@@ -2826,7 +2979,9 @@ const CommitResponse = z.z.object({
|
|
2826
2979
|
branch: z.z.string()
|
2827
2980
|
});
|
2828
2981
|
class ValOpsHttp extends ValOps {
|
2829
|
-
constructor(hostUrl, project, commitSha,
|
2982
|
+
constructor(hostUrl, project, commitSha,
|
2983
|
+
// TODO: CommitSha
|
2984
|
+
branch, apiKey, valModules, options) {
|
2830
2985
|
super(valModules, options);
|
2831
2986
|
this.hostUrl = hostUrl;
|
2832
2987
|
this.project = project;
|
@@ -2840,7 +2995,150 @@ class ValOpsHttp extends ValOps {
|
|
2840
2995
|
async onInit() {
|
2841
2996
|
// TODO: unused for now. Implement or remove
|
2842
2997
|
}
|
2998
|
+
async getStat(params) {
|
2999
|
+
if (!(params !== null && params !== void 0 && params.profileId)) {
|
3000
|
+
return {
|
3001
|
+
type: "error",
|
3002
|
+
error: {
|
3003
|
+
message: "No profileId provided"
|
3004
|
+
}
|
3005
|
+
};
|
3006
|
+
}
|
3007
|
+
const currentBaseSha = await this.getBaseSha();
|
3008
|
+
const currentSchemaSha = await this.getSchemaSha();
|
3009
|
+
const patchData = await this.fetchPatches({
|
3010
|
+
omitPatch: true,
|
3011
|
+
authors: undefined,
|
3012
|
+
patchIds: undefined,
|
3013
|
+
moduleFilePaths: undefined
|
3014
|
+
});
|
3015
|
+
const patches = [];
|
3016
|
+
// TODO: use proper patch sequences when available:
|
3017
|
+
for (const [patchId] of Object.entries(patchData.patches).sort(([, a], [, b]) => {
|
3018
|
+
return a.createdAt.localeCompare(b.createdAt, undefined);
|
3019
|
+
})) {
|
3020
|
+
patches.push(patchId);
|
3021
|
+
}
|
3022
|
+
const webSocketNonceRes = await this.getWebSocketNonce(params.profileId);
|
3023
|
+
if (webSocketNonceRes.status === "error") {
|
3024
|
+
return {
|
3025
|
+
type: "error",
|
3026
|
+
error: webSocketNonceRes.error
|
3027
|
+
};
|
3028
|
+
}
|
3029
|
+
const {
|
3030
|
+
nonce,
|
3031
|
+
url
|
3032
|
+
} = webSocketNonceRes.data;
|
3033
|
+
return {
|
3034
|
+
type: "use-websocket",
|
3035
|
+
url,
|
3036
|
+
nonce,
|
3037
|
+
baseSha: currentBaseSha,
|
3038
|
+
schemaSha: currentSchemaSha,
|
3039
|
+
patches,
|
3040
|
+
commitSha: this.commitSha
|
3041
|
+
};
|
3042
|
+
}
|
3043
|
+
async getWebSocketNonce(profileId) {
|
3044
|
+
return fetch(`${this.hostUrl}/v1/${this.project}/websocket/nonces`, {
|
3045
|
+
method: "POST",
|
3046
|
+
body: JSON.stringify({
|
3047
|
+
branch: this.branch,
|
3048
|
+
profileId
|
3049
|
+
}),
|
3050
|
+
headers: {
|
3051
|
+
...this.authHeaders,
|
3052
|
+
"Content-Type": "application/json"
|
3053
|
+
}
|
3054
|
+
}).then(async res => {
|
3055
|
+
if (res.ok) {
|
3056
|
+
const json = await res.json();
|
3057
|
+
if (typeof json.nonce !== "string" || typeof json.url !== "string") {
|
3058
|
+
return {
|
3059
|
+
status: "error",
|
3060
|
+
error: {
|
3061
|
+
message: "Invalid nonce response: " + JSON.stringify(json)
|
3062
|
+
}
|
3063
|
+
};
|
3064
|
+
}
|
3065
|
+
if (!json.url.startsWith("ws://") && !json.url.startsWith("wss://")) {
|
3066
|
+
return {
|
3067
|
+
status: "error",
|
3068
|
+
error: {
|
3069
|
+
message: "Invalid websocket url: " + json.url
|
3070
|
+
}
|
3071
|
+
};
|
3072
|
+
}
|
3073
|
+
return {
|
3074
|
+
status: "success",
|
3075
|
+
data: {
|
3076
|
+
nonce: json.nonce,
|
3077
|
+
url: json.url
|
3078
|
+
}
|
3079
|
+
};
|
3080
|
+
}
|
3081
|
+
return {
|
3082
|
+
status: "error",
|
3083
|
+
error: {
|
3084
|
+
message: "Could not get nonce. HTTP error: " + res.status + " " + res.statusText
|
3085
|
+
}
|
3086
|
+
};
|
3087
|
+
}).catch(e => {
|
3088
|
+
console.error("Could not get nonce (connection error?):", e instanceof Error ? e.message : e.toString());
|
3089
|
+
return {
|
3090
|
+
status: "error",
|
3091
|
+
error: {
|
3092
|
+
message: "Could not get nonce. Error: " + (e instanceof Error ? e.message : e.toString())
|
3093
|
+
}
|
3094
|
+
};
|
3095
|
+
});
|
3096
|
+
}
|
2843
3097
|
async fetchPatches(filters) {
|
3098
|
+
// Split patchIds into chunks to avoid too long query strings
|
3099
|
+
// NOTE: fetching patches results are cached, so this should reduce the pressure on the server
|
3100
|
+
const chunkSize = 100;
|
3101
|
+
const patchIds = filters.patchIds || [];
|
3102
|
+
const patchIdChunks = [];
|
3103
|
+
for (let i = 0; i < patchIds.length; i += chunkSize) {
|
3104
|
+
patchIdChunks.push(patchIds.slice(i, i + chunkSize));
|
3105
|
+
}
|
3106
|
+
let allPatches = {};
|
3107
|
+
let allErrors = {};
|
3108
|
+
if (patchIds === undefined || patchIds.length === 0) {
|
3109
|
+
return this.fetchPatchesInternal({
|
3110
|
+
patchIds: patchIds,
|
3111
|
+
authors: filters.authors,
|
3112
|
+
moduleFilePaths: filters.moduleFilePaths,
|
3113
|
+
omitPatch: filters.omitPatch
|
3114
|
+
});
|
3115
|
+
}
|
3116
|
+
for (const res of await Promise.all(patchIdChunks.map(patchIdChunk => this.fetchPatchesInternal({
|
3117
|
+
patchIds: patchIdChunk,
|
3118
|
+
authors: filters.authors,
|
3119
|
+
moduleFilePaths: filters.moduleFilePaths,
|
3120
|
+
omitPatch: filters.omitPatch
|
3121
|
+
})))) {
|
3122
|
+
if ("error" in res) {
|
3123
|
+
return res;
|
3124
|
+
}
|
3125
|
+
allPatches = {
|
3126
|
+
...allPatches,
|
3127
|
+
...res.patches
|
3128
|
+
};
|
3129
|
+
if (res.errors) {
|
3130
|
+
allErrors = {
|
3131
|
+
...allErrors,
|
3132
|
+
...res.errors
|
3133
|
+
};
|
3134
|
+
}
|
3135
|
+
}
|
3136
|
+
return {
|
3137
|
+
patches: allPatches,
|
3138
|
+
errors: Object.keys(allErrors).length > 0 ? allErrors : undefined
|
3139
|
+
};
|
3140
|
+
}
|
3141
|
+
async fetchPatchesInternal(filters) {
|
2844
3142
|
const params = [];
|
2845
3143
|
params.push(["branch", this.branch]);
|
2846
3144
|
if (filters.patchIds) {
|
@@ -3089,7 +3387,7 @@ class ValOpsHttp extends ValOps {
|
|
3089
3387
|
if (!file) {
|
3090
3388
|
return null;
|
3091
3389
|
}
|
3092
|
-
return
|
3390
|
+
return bufferFromDataUrl(file.value) ?? null;
|
3093
3391
|
}
|
3094
3392
|
async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
|
3095
3393
|
const params = new URLSearchParams();
|
@@ -3197,6 +3495,7 @@ class ValOpsHttp extends ValOps {
|
|
3197
3495
|
}
|
3198
3496
|
async commit(prepared, message, committer, newBranch) {
|
3199
3497
|
try {
|
3498
|
+
var _res$headers$get;
|
3200
3499
|
const existingBranch = this.branch;
|
3201
3500
|
const res = await fetch(`${this.hostUrl}/v1/${this.project}/commit`, {
|
3202
3501
|
method: "POST",
|
@@ -3232,6 +3531,22 @@ class ValOpsHttp extends ValOps {
|
|
3232
3531
|
}
|
3233
3532
|
};
|
3234
3533
|
}
|
3534
|
+
if ((_res$headers$get = res.headers.get("Content-Type")) !== null && _res$headers$get !== void 0 && _res$headers$get.includes("application/json")) {
|
3535
|
+
const json = await res.json();
|
3536
|
+
if (json.isNotFastForward) {
|
3537
|
+
return {
|
3538
|
+
isNotFastForward: true,
|
3539
|
+
error: {
|
3540
|
+
message: "Could not commit. Not a fast-forward commit"
|
3541
|
+
}
|
3542
|
+
};
|
3543
|
+
}
|
3544
|
+
return {
|
3545
|
+
error: {
|
3546
|
+
message: json.message
|
3547
|
+
}
|
3548
|
+
};
|
3549
|
+
}
|
3235
3550
|
return {
|
3236
3551
|
error: {
|
3237
3552
|
message: "Could not commit. HTTP error: " + res.status + " " + res.statusText
|
@@ -3252,12 +3567,14 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3252
3567
|
let serverOps;
|
3253
3568
|
if (options.mode === "fs") {
|
3254
3569
|
serverOps = new ValOpsFS(options.cwd, valModules, {
|
3255
|
-
formatter: options.formatter
|
3570
|
+
formatter: options.formatter,
|
3571
|
+
config: options.config
|
3256
3572
|
});
|
3257
3573
|
} else if (options.mode === "http") {
|
3258
3574
|
serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
|
3259
3575
|
formatter: options.formatter,
|
3260
|
-
root: options.root
|
3576
|
+
root: options.root,
|
3577
|
+
config: options.config
|
3261
3578
|
});
|
3262
3579
|
} else {
|
3263
3580
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
@@ -3375,12 +3692,116 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3375
3692
|
};
|
3376
3693
|
}
|
3377
3694
|
};
|
3695
|
+
const authorize = async redirectTo => {
|
3696
|
+
const token = crypto.randomUUID();
|
3697
|
+
const redirectUrl = new URL(redirectTo);
|
3698
|
+
const appAuthorizeUrl = getAuthorizeUrl(`${redirectUrl.origin}/${options.route}`, token);
|
3699
|
+
await callbacks.onEnable(true);
|
3700
|
+
return {
|
3701
|
+
cookies: {
|
3702
|
+
[internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
|
3703
|
+
[internal.VAL_STATE_COOKIE]: {
|
3704
|
+
value: createStateCookie({
|
3705
|
+
redirect_to: redirectTo,
|
3706
|
+
token
|
3707
|
+
}),
|
3708
|
+
options: {
|
3709
|
+
httpOnly: true,
|
3710
|
+
sameSite: "lax",
|
3711
|
+
expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
|
3712
|
+
}
|
3713
|
+
}
|
3714
|
+
},
|
3715
|
+
status: 302,
|
3716
|
+
redirectTo: appAuthorizeUrl
|
3717
|
+
};
|
3718
|
+
};
|
3378
3719
|
return {
|
3379
|
-
|
3720
|
+
"/draft/enable": {
|
3721
|
+
GET: async req => {
|
3722
|
+
const cookies = req.cookies;
|
3723
|
+
const auth = getAuth(cookies);
|
3724
|
+
if (auth.error) {
|
3725
|
+
return {
|
3726
|
+
status: 401,
|
3727
|
+
json: {
|
3728
|
+
message: auth.error
|
3729
|
+
}
|
3730
|
+
};
|
3731
|
+
}
|
3732
|
+
const query = req.query;
|
3733
|
+
const redirectToRes = getRedirectUrl(query, options.valEnableRedirectUrl);
|
3734
|
+
if (typeof redirectToRes !== "string") {
|
3735
|
+
return redirectToRes;
|
3736
|
+
}
|
3737
|
+
await callbacks.onEnable(true);
|
3738
|
+
return {
|
3739
|
+
status: 302,
|
3740
|
+
redirectTo: redirectToRes
|
3741
|
+
};
|
3742
|
+
}
|
3743
|
+
},
|
3744
|
+
"/draft/disable": {
|
3745
|
+
GET: async req => {
|
3746
|
+
const cookies = req.cookies;
|
3747
|
+
const auth = getAuth(cookies);
|
3748
|
+
if (auth.error) {
|
3749
|
+
return {
|
3750
|
+
status: 401,
|
3751
|
+
json: {
|
3752
|
+
message: auth.error
|
3753
|
+
}
|
3754
|
+
};
|
3755
|
+
}
|
3756
|
+
const query = req.query;
|
3757
|
+
const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
|
3758
|
+
if (typeof redirectToRes !== "string") {
|
3759
|
+
return redirectToRes;
|
3760
|
+
}
|
3761
|
+
await callbacks.onDisable(true);
|
3762
|
+
return {
|
3763
|
+
status: 302,
|
3764
|
+
redirectTo: redirectToRes
|
3765
|
+
};
|
3766
|
+
}
|
3767
|
+
},
|
3768
|
+
"/draft/stat": {
|
3769
|
+
GET: async req => {
|
3770
|
+
const cookies = req.cookies;
|
3771
|
+
const auth = getAuth(cookies);
|
3772
|
+
if (auth.error) {
|
3773
|
+
return {
|
3774
|
+
status: 401,
|
3775
|
+
json: {
|
3776
|
+
message: auth.error
|
3777
|
+
}
|
3778
|
+
};
|
3779
|
+
}
|
3780
|
+
return {
|
3781
|
+
status: 200,
|
3782
|
+
json: {
|
3783
|
+
draftMode: await callbacks.isEnabled()
|
3784
|
+
}
|
3785
|
+
};
|
3786
|
+
}
|
3787
|
+
},
|
3380
3788
|
"/enable": {
|
3381
3789
|
GET: async req => {
|
3790
|
+
const cookies = req.cookies;
|
3791
|
+
const auth = getAuth(cookies);
|
3382
3792
|
const query = req.query;
|
3383
3793
|
const redirectToRes = getRedirectUrl(query, options.valEnableRedirectUrl);
|
3794
|
+
if (auth.error) {
|
3795
|
+
if (typeof redirectToRes === "string") {
|
3796
|
+
return authorize(redirectToRes);
|
3797
|
+
}
|
3798
|
+
return {
|
3799
|
+
status: 401,
|
3800
|
+
json: {
|
3801
|
+
message: auth.error
|
3802
|
+
}
|
3803
|
+
};
|
3804
|
+
}
|
3384
3805
|
if (typeof redirectToRes !== "string") {
|
3385
3806
|
return redirectToRes;
|
3386
3807
|
}
|
@@ -3396,6 +3817,16 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3396
3817
|
},
|
3397
3818
|
"/disable": {
|
3398
3819
|
GET: async req => {
|
3820
|
+
const cookies = req.cookies;
|
3821
|
+
const auth = getAuth(cookies);
|
3822
|
+
if (auth.error) {
|
3823
|
+
return {
|
3824
|
+
status: 401,
|
3825
|
+
json: {
|
3826
|
+
message: auth.error
|
3827
|
+
}
|
3828
|
+
};
|
3829
|
+
}
|
3399
3830
|
const query = req.query;
|
3400
3831
|
const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
|
3401
3832
|
if (typeof redirectToRes !== "string") {
|
@@ -3413,6 +3844,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3413
3844
|
};
|
3414
3845
|
}
|
3415
3846
|
},
|
3847
|
+
//#region auth
|
3416
3848
|
"/authorize": {
|
3417
3849
|
GET: async req => {
|
3418
3850
|
const query = req.query;
|
@@ -3424,28 +3856,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3424
3856
|
}
|
3425
3857
|
};
|
3426
3858
|
}
|
3427
|
-
const
|
3428
|
-
|
3429
|
-
const appAuthorizeUrl = getAuthorizeUrl(`${redirectUrl.origin}/${options.route}`, token);
|
3430
|
-
await callbacks.onEnable(true);
|
3431
|
-
return {
|
3432
|
-
cookies: {
|
3433
|
-
[internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
|
3434
|
-
[internal.VAL_STATE_COOKIE]: {
|
3435
|
-
value: createStateCookie({
|
3436
|
-
redirect_to: query.redirect_to,
|
3437
|
-
token
|
3438
|
-
}),
|
3439
|
-
options: {
|
3440
|
-
httpOnly: true,
|
3441
|
-
sameSite: "lax",
|
3442
|
-
expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
|
3443
|
-
}
|
3444
|
-
}
|
3445
|
-
},
|
3446
|
-
status: 302,
|
3447
|
-
redirectTo: appAuthorizeUrl
|
3448
|
-
};
|
3859
|
+
const redirectTo = query.redirect_to;
|
3860
|
+
return authorize(redirectTo);
|
3449
3861
|
}
|
3450
3862
|
},
|
3451
3863
|
"/callback": {
|
@@ -3617,6 +4029,46 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3617
4029
|
};
|
3618
4030
|
}
|
3619
4031
|
},
|
4032
|
+
//#region stat
|
4033
|
+
"/stat": {
|
4034
|
+
POST: async req => {
|
4035
|
+
const cookies = req.cookies;
|
4036
|
+
const auth = getAuth(cookies);
|
4037
|
+
if (auth.error) {
|
4038
|
+
return {
|
4039
|
+
status: 401,
|
4040
|
+
json: {
|
4041
|
+
message: auth.error
|
4042
|
+
}
|
4043
|
+
};
|
4044
|
+
}
|
4045
|
+
if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
|
4046
|
+
return {
|
4047
|
+
status: 401,
|
4048
|
+
json: {
|
4049
|
+
message: "Unauthorized"
|
4050
|
+
}
|
4051
|
+
};
|
4052
|
+
}
|
4053
|
+
const currentStat = await serverOps.getStat({
|
4054
|
+
...req.body,
|
4055
|
+
profileId: "id" in auth ? auth.id : undefined
|
4056
|
+
});
|
4057
|
+
if (currentStat.type === "error") {
|
4058
|
+
return {
|
4059
|
+
status: 500,
|
4060
|
+
json: currentStat.error
|
4061
|
+
};
|
4062
|
+
}
|
4063
|
+
return {
|
4064
|
+
status: 200,
|
4065
|
+
json: {
|
4066
|
+
...currentStat,
|
4067
|
+
config: options.config
|
4068
|
+
}
|
4069
|
+
};
|
4070
|
+
}
|
4071
|
+
},
|
3620
4072
|
//#region patches
|
3621
4073
|
"/patches/~": {
|
3622
4074
|
GET: async req => {
|
@@ -3711,7 +4163,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3711
4163
|
};
|
3712
4164
|
}
|
3713
4165
|
},
|
3714
|
-
//#region
|
4166
|
+
//#region schema
|
3715
4167
|
"/schema": {
|
3716
4168
|
GET: async req => {
|
3717
4169
|
const cookies = req.cookies;
|
@@ -3738,7 +4190,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3738
4190
|
return {
|
3739
4191
|
status: 500,
|
3740
4192
|
json: {
|
3741
|
-
message:
|
4193
|
+
message: `Got errors while fetching modules: ${moduleErrors.filter(error => error).map(error => error.message).join(", ")}`,
|
3742
4194
|
details: moduleErrors
|
3743
4195
|
}
|
3744
4196
|
};
|
@@ -3746,9 +4198,22 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3746
4198
|
const schemaSha = await serverOps.getSchemaSha();
|
3747
4199
|
const schemas = await serverOps.getSchemas();
|
3748
4200
|
const serializedSchemas = {};
|
3749
|
-
|
3750
|
-
const
|
3751
|
-
|
4201
|
+
try {
|
4202
|
+
for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
|
4203
|
+
const moduleFilePath = moduleFilePathS;
|
4204
|
+
serializedSchemas[moduleFilePath] = schema.serialize();
|
4205
|
+
}
|
4206
|
+
} catch (e) {
|
4207
|
+
console.error("Val: Failed to serialize schemas", e);
|
4208
|
+
return {
|
4209
|
+
status: 500,
|
4210
|
+
json: {
|
4211
|
+
message: "Failed to serialize schemas",
|
4212
|
+
details: [{
|
4213
|
+
message: e instanceof Error ? e.message : JSON.stringify(e)
|
4214
|
+
}]
|
4215
|
+
}
|
4216
|
+
};
|
3752
4217
|
}
|
3753
4218
|
return {
|
3754
4219
|
status: 200,
|
@@ -3759,7 +4224,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3759
4224
|
};
|
3760
4225
|
}
|
3761
4226
|
},
|
3762
|
-
|
4227
|
+
// #region sources
|
4228
|
+
"/sources": {
|
3763
4229
|
PUT: async req => {
|
3764
4230
|
var _body$patchIds;
|
3765
4231
|
const query = req.query;
|
@@ -3796,8 +4262,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3796
4262
|
}
|
3797
4263
|
let tree;
|
3798
4264
|
let patchAnalysis = null;
|
3799
|
-
let
|
3800
|
-
if (body !== null && body !== void 0 && body.patchIds && (body === null || body === void 0 || (_body$patchIds = body.patchIds) === null || _body$patchIds === void 0 ? void 0 : _body$patchIds.length) > 0 || body !== null && body !== void 0 && body.
|
4265
|
+
let newPatchIds = undefined;
|
4266
|
+
if (body !== null && body !== void 0 && body.patchIds && (body === null || body === void 0 || (_body$patchIds = body.patchIds) === null || _body$patchIds === void 0 ? void 0 : _body$patchIds.length) > 0 || body !== null && body !== void 0 && body.addPatches) {
|
3801
4267
|
// TODO: validate patches_sha
|
3802
4268
|
const patchIds = body === null || body === void 0 ? void 0 : body.patchIds;
|
3803
4269
|
const patchOps = patchIds && patchIds.length > 0 ? await serverOps.fetchPatches({
|
@@ -3806,6 +4272,15 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3806
4272
|
}) : {
|
3807
4273
|
patches: {}
|
3808
4274
|
};
|
4275
|
+
if (patchOps.error) {
|
4276
|
+
return {
|
4277
|
+
status: 400,
|
4278
|
+
json: {
|
4279
|
+
message: "Failed to fetch patches: " + patchOps.error.message,
|
4280
|
+
details: []
|
4281
|
+
}
|
4282
|
+
};
|
4283
|
+
}
|
3809
4284
|
let patchErrors = undefined;
|
3810
4285
|
for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
|
3811
4286
|
const patchId = patchIdS;
|
@@ -3816,45 +4291,43 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3816
4291
|
message: error.message
|
3817
4292
|
};
|
3818
4293
|
}
|
3819
|
-
|
3820
|
-
|
3821
|
-
|
3822
|
-
const
|
3823
|
-
|
3824
|
-
|
3825
|
-
|
3826
|
-
|
3827
|
-
|
3828
|
-
|
3829
|
-
|
3830
|
-
|
4294
|
+
// TODO: errors
|
4295
|
+
patchAnalysis = serverOps.analyzePatches(patchOps.patches);
|
4296
|
+
if (body !== null && body !== void 0 && body.addPatches) {
|
4297
|
+
for (const addPatch of body.addPatches) {
|
4298
|
+
const newPatchModuleFilePath = addPatch.path;
|
4299
|
+
const newPatchOps = addPatch.patch;
|
4300
|
+
const authorId = "id" in auth ? auth.id : null;
|
4301
|
+
const createPatchRes = await serverOps.createPatch(newPatchModuleFilePath, {
|
4302
|
+
...patchAnalysis,
|
4303
|
+
...patchOps
|
4304
|
+
}, newPatchOps, authorId);
|
4305
|
+
if (createPatchRes.error) {
|
4306
|
+
return {
|
4307
|
+
status: 500,
|
4308
|
+
json: {
|
4309
|
+
message: "Failed to create patch: " + createPatchRes.error.message,
|
4310
|
+
details: createPatchRes.error
|
4311
|
+
}
|
4312
|
+
};
|
4313
|
+
}
|
4314
|
+
if (!newPatchIds) {
|
4315
|
+
newPatchIds = [createPatchRes.patchId];
|
4316
|
+
} else {
|
4317
|
+
newPatchIds.push(createPatchRes.patchId);
|
4318
|
+
}
|
4319
|
+
patchOps.patches[createPatchRes.patchId] = {
|
4320
|
+
path: newPatchModuleFilePath,
|
4321
|
+
patch: newPatchOps,
|
4322
|
+
authorId,
|
4323
|
+
createdAt: createPatchRes.createdAt,
|
4324
|
+
appliedAt: null
|
3831
4325
|
};
|
4326
|
+
patchAnalysis.patchesByModule[newPatchModuleFilePath] = [...(patchAnalysis.patchesByModule[newPatchModuleFilePath] || []), {
|
4327
|
+
patchId: createPatchRes.patchId
|
4328
|
+
}];
|
3832
4329
|
}
|
3833
|
-
// TODO: evaluate if we need this: seems wrong to delete patches that are not applied
|
3834
|
-
// for (const fileRes of createPatchRes.files) {
|
3835
|
-
// if (fileRes.error) {
|
3836
|
-
// // clean up broken patch:
|
3837
|
-
// await this.serverOps.deletePatches([createPatchRes.patchId]);
|
3838
|
-
// return {
|
3839
|
-
// status: 500,
|
3840
|
-
// json: {
|
3841
|
-
// message: "Failed to create patch",
|
3842
|
-
// details: fileRes.error,
|
3843
|
-
// },
|
3844
|
-
// };
|
3845
|
-
// }
|
3846
|
-
// }
|
3847
|
-
newPatchId = createPatchRes.patchId;
|
3848
|
-
patchOps.patches[createPatchRes.patchId] = {
|
3849
|
-
path: newPatchModuleFilePath,
|
3850
|
-
patch: newPatchOps,
|
3851
|
-
authorId,
|
3852
|
-
createdAt: createPatchRes.createdAt,
|
3853
|
-
appliedAt: null
|
3854
|
-
};
|
3855
4330
|
}
|
3856
|
-
// TODO: errors
|
3857
|
-
patchAnalysis = serverOps.analyzePatches(patchOps.patches);
|
3858
4331
|
tree = {
|
3859
4332
|
...(await serverOps.getTree({
|
3860
4333
|
...patchAnalysis,
|
@@ -3877,27 +4350,13 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3877
4350
|
} else {
|
3878
4351
|
tree = await serverOps.getTree();
|
3879
4352
|
}
|
3880
|
-
|
3881
|
-
|
3882
|
-
|
3883
|
-
|
3884
|
-
json: {
|
3885
|
-
type: "patch-error",
|
3886
|
-
errors: Object.fromEntries(Object.entries(tree.errors).map(([key, value]) => [key, value.map(error => ({
|
3887
|
-
patchId: error.patchId,
|
3888
|
-
skipped: error.skipped,
|
3889
|
-
error: {
|
3890
|
-
message: error.error.message
|
3891
|
-
}
|
3892
|
-
}))])),
|
3893
|
-
message: "One or more patches failed to be applied"
|
3894
|
-
}
|
3895
|
-
};
|
3896
|
-
return res;
|
3897
|
-
}
|
4353
|
+
let sourcesValidation = {
|
4354
|
+
errors: {},
|
4355
|
+
files: {}
|
4356
|
+
};
|
3898
4357
|
if (query.validate_sources || query.validate_binary_files) {
|
3899
4358
|
const schemas = await serverOps.getSchemas();
|
3900
|
-
|
4359
|
+
sourcesValidation = await serverOps.validateSources(schemas, tree.sources);
|
3901
4360
|
|
3902
4361
|
// TODO: send validation errors
|
3903
4362
|
if (query.validate_binary_files) {
|
@@ -3909,20 +4368,41 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3909
4368
|
for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
|
3910
4369
|
const moduleFilePath = moduleFilePathS;
|
3911
4370
|
if (moduleFilePath.startsWith(treePath)) {
|
4371
|
+
var _sourcesValidation$er;
|
3912
4372
|
modules[moduleFilePath] = {
|
3913
4373
|
source: module,
|
3914
4374
|
patches: patchAnalysis && patchAnalysis.patchesByModule[moduleFilePath] ? {
|
3915
4375
|
applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
|
3916
|
-
} : undefined
|
4376
|
+
} : undefined,
|
4377
|
+
validationErrors: (_sourcesValidation$er = sourcesValidation.errors[moduleFilePath]) === null || _sourcesValidation$er === void 0 ? void 0 : _sourcesValidation$er.validations
|
3917
4378
|
};
|
3918
4379
|
}
|
3919
4380
|
}
|
4381
|
+
if (tree.errors && Object.keys(tree.errors).length > 0) {
|
4382
|
+
const res = {
|
4383
|
+
status: 400,
|
4384
|
+
json: {
|
4385
|
+
type: "patch-error",
|
4386
|
+
schemaSha,
|
4387
|
+
modules,
|
4388
|
+
errors: Object.fromEntries(Object.entries(tree.errors).map(([key, value]) => [key, value.map(error => ({
|
4389
|
+
patchId: error.patchId,
|
4390
|
+
skipped: error.skipped,
|
4391
|
+
error: {
|
4392
|
+
message: error.error.message
|
4393
|
+
}
|
4394
|
+
}))])),
|
4395
|
+
message: "One or more patches failed to be applied"
|
4396
|
+
}
|
4397
|
+
};
|
4398
|
+
return res;
|
4399
|
+
}
|
3920
4400
|
const res = {
|
3921
4401
|
status: 200,
|
3922
4402
|
json: {
|
3923
4403
|
schemaSha,
|
3924
4404
|
modules,
|
3925
|
-
|
4405
|
+
newPatchIds
|
3926
4406
|
}
|
3927
4407
|
};
|
3928
4408
|
return res;
|
@@ -3968,10 +4448,10 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3968
4448
|
...patches
|
3969
4449
|
});
|
3970
4450
|
if (preparedCommit.hasErrors) {
|
3971
|
-
console.error("Failed to create commit", {
|
4451
|
+
console.error("Failed to create commit", JSON.stringify({
|
3972
4452
|
sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
|
3973
4453
|
binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
|
3974
|
-
});
|
4454
|
+
}, null, 2));
|
3975
4455
|
return {
|
3976
4456
|
status: 400,
|
3977
4457
|
json: {
|
@@ -3987,13 +4467,34 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3987
4467
|
}
|
3988
4468
|
if (serverOps instanceof ValOpsFS) {
|
3989
4469
|
await serverOps.saveFiles(preparedCommit);
|
4470
|
+
await serverOps.deletePatches(patchIds);
|
3990
4471
|
return {
|
3991
4472
|
status: 200,
|
3992
4473
|
json: {} // TODO:
|
3993
4474
|
};
|
3994
4475
|
} else if (serverOps instanceof ValOpsHttp) {
|
3995
4476
|
if (auth.error === undefined && auth.id) {
|
3996
|
-
await serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
|
4477
|
+
const commitRes = await serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
|
4478
|
+
if (commitRes.error) {
|
4479
|
+
console.error("Failed to commit", commitRes.error);
|
4480
|
+
if ("isNotFastForward" in commitRes && commitRes.isNotFastForward) {
|
4481
|
+
return {
|
4482
|
+
status: 409,
|
4483
|
+
json: {
|
4484
|
+
isNotFastForward: true,
|
4485
|
+
message: "Cannot commit: this is not the latest version of this branch"
|
4486
|
+
}
|
4487
|
+
};
|
4488
|
+
}
|
4489
|
+
return {
|
4490
|
+
status: 400,
|
4491
|
+
json: {
|
4492
|
+
message: commitRes.error.message,
|
4493
|
+
details: []
|
4494
|
+
}
|
4495
|
+
};
|
4496
|
+
}
|
4497
|
+
// TODO: serverOps.markApplied(patchIds);
|
3997
4498
|
return {
|
3998
4499
|
status: 200,
|
3999
4500
|
json: {} // TODO:
|
@@ -4024,15 +4525,24 @@ const ValServer = (valModules, options, callbacks) => {
|
|
4024
4525
|
// 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
|
4025
4526
|
// Thus: attack surface + ease of attack + benefit = low probability of attack
|
4026
4527
|
// If we couldn't argue that patch ids are secret enough, then this would be a problem.
|
4528
|
+
let cacheControl;
|
4027
4529
|
let fileBuffer;
|
4530
|
+
let mimeType;
|
4028
4531
|
if (query.patch_id) {
|
4029
4532
|
fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
|
4533
|
+
mimeType = core.Internal.filenameToMimeType(filePath);
|
4534
|
+
cacheControl = "public, max-age=20000, immutable";
|
4030
4535
|
} else {
|
4031
4536
|
fileBuffer = await serverOps.getBinaryFile(filePath);
|
4032
4537
|
}
|
4033
4538
|
if (fileBuffer) {
|
4034
4539
|
return {
|
4035
4540
|
status: 200,
|
4541
|
+
headers: {
|
4542
|
+
// TODO: we could use ETag and return 304 instead
|
4543
|
+
"Content-Type": mimeType || "application/octet-stream",
|
4544
|
+
"Cache-Control": cacheControl || "public, max-age=0, must-revalidate"
|
4545
|
+
},
|
4036
4546
|
body: bufferToReadableStream(fileBuffer)
|
4037
4547
|
};
|
4038
4548
|
} else {
|
@@ -4352,14 +4862,14 @@ function guessMimeTypeFromPath(filePath) {
|
|
4352
4862
|
return null;
|
4353
4863
|
}
|
4354
4864
|
|
4355
|
-
async function createValServer(valModules, route, opts, callbacks, formatter) {
|
4356
|
-
const valServerConfig = await initHandlerOptions(route, opts);
|
4865
|
+
async function createValServer(valModules, route, opts, config, callbacks, formatter) {
|
4866
|
+
const valServerConfig = await initHandlerOptions(route, opts, config);
|
4357
4867
|
return ValServer(valModules, {
|
4358
4868
|
formatter,
|
4359
4869
|
...valServerConfig
|
4360
4870
|
}, callbacks);
|
4361
4871
|
}
|
4362
|
-
async function initHandlerOptions(route, opts) {
|
4872
|
+
async function initHandlerOptions(route, opts, config) {
|
4363
4873
|
const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
|
4364
4874
|
const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
|
4365
4875
|
const isProxyMode = opts.mode === "proxy" || opts.mode === undefined && (maybeApiKey || maybeValSecret);
|
@@ -4404,7 +4914,8 @@ async function initHandlerOptions(route, opts) {
|
|
4404
4914
|
valEnableRedirectUrl,
|
4405
4915
|
valDisableRedirectUrl,
|
4406
4916
|
valContentUrl,
|
4407
|
-
valBuildUrl
|
4917
|
+
valBuildUrl,
|
4918
|
+
config
|
4408
4919
|
};
|
4409
4920
|
} else {
|
4410
4921
|
const cwd = process.cwd();
|
@@ -4418,7 +4929,8 @@ async function initHandlerOptions(route, opts) {
|
|
4418
4929
|
valBuildUrl,
|
4419
4930
|
apiKey: maybeApiKey,
|
4420
4931
|
valSecret: maybeValSecret,
|
4421
|
-
project: maybeValProject
|
4932
|
+
project: maybeValProject,
|
4933
|
+
config
|
4422
4934
|
};
|
4423
4935
|
}
|
4424
4936
|
}
|
@@ -4587,12 +5099,25 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
4587
5099
|
}
|
4588
5100
|
};
|
4589
5101
|
}
|
4590
|
-
|
4591
|
-
|
4592
|
-
|
4593
|
-
|
4594
|
-
|
4595
|
-
|
5102
|
+
let bodyRes;
|
5103
|
+
try {
|
5104
|
+
bodyRes = reqDefinition.body ? reqDefinition.body.safeParse(await req.json()) : {
|
5105
|
+
success: true,
|
5106
|
+
data: {}
|
5107
|
+
};
|
5108
|
+
if (!bodyRes.success) {
|
5109
|
+
return zodErrorResult(bodyRes.error, "invalid body data");
|
5110
|
+
}
|
5111
|
+
} catch (e) {
|
5112
|
+
return {
|
5113
|
+
status: 400,
|
5114
|
+
json: {
|
5115
|
+
message: "Could not parse request body",
|
5116
|
+
details: {
|
5117
|
+
error: JSON.stringify(e)
|
5118
|
+
}
|
5119
|
+
}
|
5120
|
+
};
|
4596
5121
|
}
|
4597
5122
|
const cookiesRes = reqDefinition.cookies ? getCookies(req, reqDefinition.cookies) : {
|
4598
5123
|
success: true,
|
@@ -4623,7 +5148,7 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
4623
5148
|
}
|
4624
5149
|
// convert boolean to union of literals true and false so we can parse it as a string
|
4625
5150
|
if (innerType instanceof z.z.ZodBoolean) {
|
4626
|
-
innerType = z.z.union([z.z.literal("true"), z.z.literal("false")]).transform(arg =>
|
5151
|
+
innerType = z.z.union([z.z.literal("true"), z.z.literal("false")]).transform(arg => arg === "true");
|
4627
5152
|
}
|
4628
5153
|
// re-build rules:
|
4629
5154
|
let arrayCompatibleRule = innerType;
|
@@ -4648,6 +5173,15 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
4648
5173
|
query,
|
4649
5174
|
path
|
4650
5175
|
});
|
5176
|
+
if (res.status === 500) {
|
5177
|
+
var _res$json;
|
5178
|
+
return {
|
5179
|
+
status: 500,
|
5180
|
+
json: {
|
5181
|
+
message: ((_res$json = res.json) === null || _res$json === void 0 ? void 0 : _res$json.message) || "Internal Server Error"
|
5182
|
+
}
|
5183
|
+
};
|
5184
|
+
}
|
4651
5185
|
const resDef = apiEndpoint.res;
|
4652
5186
|
if (resDef) {
|
4653
5187
|
const responseResult = resDef.safeParse(res);
|
@@ -4744,6 +5278,49 @@ class ValFSHost {
|
|
4744
5278
|
}
|
4745
5279
|
}
|
4746
5280
|
|
5281
|
+
async function extractImageMetadata(filename, input) {
|
5282
|
+
const imageSize = sizeOf__default["default"](input);
|
5283
|
+
let mimeType = null;
|
5284
|
+
if (imageSize.type) {
|
5285
|
+
const possibleMimeType = `image/${imageSize.type}`;
|
5286
|
+
if (core.Internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
|
5287
|
+
mimeType = possibleMimeType;
|
5288
|
+
}
|
5289
|
+
const filenameBasedLookup = core.Internal.filenameToMimeType(filename);
|
5290
|
+
if (filenameBasedLookup) {
|
5291
|
+
mimeType = filenameBasedLookup;
|
5292
|
+
}
|
5293
|
+
}
|
5294
|
+
if (!mimeType) {
|
5295
|
+
mimeType = "application/octet-stream";
|
5296
|
+
}
|
5297
|
+
let {
|
5298
|
+
width,
|
5299
|
+
height
|
5300
|
+
} = imageSize;
|
5301
|
+
if (!width || !height) {
|
5302
|
+
width = 0;
|
5303
|
+
height = 0;
|
5304
|
+
}
|
5305
|
+
return {
|
5306
|
+
width,
|
5307
|
+
height,
|
5308
|
+
mimeType
|
5309
|
+
};
|
5310
|
+
}
|
5311
|
+
async function extractFileMetadata(filename,
|
5312
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
5313
|
+
_input // TODO: use buffer to determine mimetype
|
5314
|
+
) {
|
5315
|
+
let mimeType = core.Internal.filenameToMimeType(filename);
|
5316
|
+
if (!mimeType) {
|
5317
|
+
mimeType = "application/octet-stream";
|
5318
|
+
}
|
5319
|
+
return {
|
5320
|
+
mimeType
|
5321
|
+
};
|
5322
|
+
}
|
5323
|
+
|
4747
5324
|
function getValidationErrorFileRef(validationError) {
|
4748
5325
|
const maybeRef = validationError.value && typeof validationError.value === "object" && core.FILE_REF_PROP in validationError.value && typeof validationError.value[core.FILE_REF_PROP] === "string" ? validationError.value[core.FILE_REF_PROP] : undefined;
|
4749
5326
|
if (!maybeRef) {
|
@@ -4771,15 +5348,15 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4771
5348
|
throw Error("Cannot fix file without a file reference");
|
4772
5349
|
}
|
4773
5350
|
const filename = fsPath__namespace["default"].join(config.projectRoot, fileRef);
|
4774
|
-
|
4775
|
-
return extractFileMetadata(fileRef
|
5351
|
+
fs__default["default"].readFileSync(filename);
|
5352
|
+
return extractFileMetadata(fileRef);
|
4776
5353
|
}
|
4777
5354
|
const remainingErrors = [];
|
4778
5355
|
const patch$1 = [];
|
4779
5356
|
for (const fix of validationError.fixes || []) {
|
4780
5357
|
if (fix === "image:replace-metadata" || fix === "image:add-metadata") {
|
4781
5358
|
const imageMetadata = await getImageMetadata();
|
4782
|
-
if (imageMetadata.width === undefined || imageMetadata.height === undefined
|
5359
|
+
if (imageMetadata.width === undefined || imageMetadata.height === undefined) {
|
4783
5360
|
remainingErrors.push({
|
4784
5361
|
...validationError,
|
4785
5362
|
message: "Failed to get image metadata",
|
@@ -4790,8 +5367,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4790
5367
|
const metadataIsCorrect =
|
4791
5368
|
// metadata is a prop that is an object
|
4792
5369
|
typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
|
4793
|
-
// sha256 is correct
|
4794
|
-
"sha256" in currentValue.metadata && currentValue.metadata.sha256 === imageMetadata.sha256 &&
|
4795
5370
|
// width is correct
|
4796
5371
|
"width" in currentValue.metadata && currentValue.metadata.width === imageMetadata.width &&
|
4797
5372
|
// height is correct
|
@@ -4808,18 +5383,11 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4808
5383
|
value: {
|
4809
5384
|
width: imageMetadata.width,
|
4810
5385
|
height: imageMetadata.height,
|
4811
|
-
sha256: imageMetadata.sha256,
|
4812
5386
|
mimeType: imageMetadata.mimeType
|
4813
5387
|
}
|
4814
5388
|
});
|
4815
5389
|
} else {
|
4816
5390
|
if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
|
4817
|
-
if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== imageMetadata.sha256) {
|
4818
|
-
remainingErrors.push({
|
4819
|
-
message: "Image metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + imageMetadata.sha256 + ".",
|
4820
|
-
fixes: undefined
|
4821
|
-
});
|
4822
|
-
}
|
4823
5391
|
if (!("width" in currentValue.metadata) || currentValue.metadata.width !== imageMetadata.width) {
|
4824
5392
|
remainingErrors.push({
|
4825
5393
|
message: "Image metadata width is incorrect! Found: " + ("width" in currentValue.metadata ? currentValue.metadata.width : "<empty>") + ". Expected: " + imageMetadata.width,
|
@@ -4854,14 +5422,13 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4854
5422
|
value: {
|
4855
5423
|
width: imageMetadata.width,
|
4856
5424
|
height: imageMetadata.height,
|
4857
|
-
sha256: imageMetadata.sha256,
|
4858
5425
|
mimeType: imageMetadata.mimeType
|
4859
5426
|
}
|
4860
5427
|
});
|
4861
5428
|
}
|
4862
5429
|
} else if (fix === "file:add-metadata" || fix === "file:check-metadata") {
|
4863
5430
|
const fileMetadata = await getFileMetadata();
|
4864
|
-
if (fileMetadata
|
5431
|
+
if (fileMetadata === undefined) {
|
4865
5432
|
remainingErrors.push({
|
4866
5433
|
...validationError,
|
4867
5434
|
message: "Failed to get image metadata",
|
@@ -4872,8 +5439,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4872
5439
|
const metadataIsCorrect =
|
4873
5440
|
// metadata is a prop that is an object
|
4874
5441
|
typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
|
4875
|
-
// sha256 is correct
|
4876
|
-
"sha256" in currentValue.metadata && currentValue.metadata.sha256 === fileMetadata.sha256 &&
|
4877
5442
|
// mimeType is correct
|
4878
5443
|
"mimeType" in currentValue.metadata && currentValue.metadata.mimeType === fileMetadata.mimeType;
|
4879
5444
|
|
@@ -4884,7 +5449,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4884
5449
|
op: "replace",
|
4885
5450
|
path: patch.sourceToPatchPath(sourcePath).concat("metadata"),
|
4886
5451
|
value: {
|
4887
|
-
sha256: fileMetadata.sha256,
|
4888
5452
|
...(fileMetadata.mimeType ? {
|
4889
5453
|
mimeType: fileMetadata.mimeType
|
4890
5454
|
} : {})
|
@@ -4892,12 +5456,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4892
5456
|
});
|
4893
5457
|
} else {
|
4894
5458
|
if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
|
4895
|
-
if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== fileMetadata.sha256) {
|
4896
|
-
remainingErrors.push({
|
4897
|
-
message: "File metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + fileMetadata.sha256 + ".",
|
4898
|
-
fixes: undefined
|
4899
|
-
});
|
4900
|
-
}
|
4901
5459
|
if (!("mimeType" in currentValue.metadata) || currentValue.metadata.mimeType !== fileMetadata.mimeType) {
|
4902
5460
|
remainingErrors.push({
|
4903
5461
|
message: "File metadata mimeType is incorrect! Found: " + ("mimeType" in currentValue.metadata ? currentValue.metadata.mimeType : "<empty>") + ". Expected: " + fileMetadata.mimeType,
|
@@ -4918,7 +5476,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4918
5476
|
op: "add",
|
4919
5477
|
path: patch.sourceToPatchPath(sourcePath).concat("metadata"),
|
4920
5478
|
value: {
|
4921
|
-
sha256: fileMetadata.sha256,
|
4922
5479
|
...(fileMetadata.mimeType ? {
|
4923
5480
|
mimeType: fileMetadata.mimeType
|
4924
5481
|
} : {})
|