@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
@@ -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 (Internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
|
1253
|
-
mimeType = possibleMimeType;
|
1254
|
-
}
|
1255
|
-
const filenameBasedLookup = Internal.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 = Internal.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;
|
@@ -1592,25 +1555,27 @@ class ValOps {
|
|
1592
1555
|
}
|
1593
1556
|
for (const [sourcePathS, validationErrors] of Object.entries(res)) {
|
1594
1557
|
const sourcePath = sourcePathS;
|
1595
|
-
|
1596
|
-
|
1597
|
-
if (
|
1598
|
-
|
1599
|
-
|
1600
|
-
|
1601
|
-
|
1602
|
-
|
1603
|
-
|
1604
|
-
|
1605
|
-
|
1606
|
-
errors[path]
|
1607
|
-
|
1608
|
-
|
1609
|
-
|
1610
|
-
|
1611
|
-
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);
|
1612
1578
|
}
|
1613
|
-
errors[path].validations[sourcePath].push(validationError);
|
1614
1579
|
}
|
1615
1580
|
}
|
1616
1581
|
}
|
@@ -1798,8 +1763,22 @@ class ValOps {
|
|
1798
1763
|
const patchRes = applyPatch(tsSourceFile, tsOps, sourceFileOps);
|
1799
1764
|
if (result.isErr(patchRes)) {
|
1800
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
|
+
}
|
1801
1774
|
errors.push(...patchRes.error);
|
1802
1775
|
} else {
|
1776
|
+
console.error("Could not patch", JSON.stringify({
|
1777
|
+
path,
|
1778
|
+
patchId,
|
1779
|
+
error: patchRes.error,
|
1780
|
+
sourceFileOps
|
1781
|
+
}, null, 2));
|
1803
1782
|
errors.push(patchRes.error);
|
1804
1783
|
}
|
1805
1784
|
triedPatches.push(patchId);
|
@@ -1893,16 +1872,23 @@ class ValOps {
|
|
1893
1872
|
}
|
1894
1873
|
|
1895
1874
|
// #region createPatch
|
1896
|
-
async createPatch(path, patch, authorId) {
|
1897
|
-
const
|
1898
|
-
|
1899
|
-
|
1900
|
-
|
1901
|
-
|
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
|
+
}
|
1902
1887
|
const source = sources[path];
|
1903
1888
|
const schema = schemas[path];
|
1904
1889
|
const moduleError = moduleErrors.find(e => e.path === path);
|
1905
1890
|
if (moduleError) {
|
1891
|
+
console.error(`Cannot patch. Module at path: '${path}' has fatal errors: "${moduleError.message}"`);
|
1906
1892
|
return {
|
1907
1893
|
error: {
|
1908
1894
|
message: `Cannot patch. Module at path: '${path}' has fatal errors: ` + moduleErrors.map(m => `"${m.message}"`).join(" and ")
|
@@ -1910,6 +1896,7 @@ class ValOps {
|
|
1910
1896
|
};
|
1911
1897
|
}
|
1912
1898
|
if (!source) {
|
1899
|
+
console.error(`Cannot patch. Module source at path: '${path}' does not exist`);
|
1913
1900
|
return {
|
1914
1901
|
error: {
|
1915
1902
|
message: `Cannot patch. Module source at path: '${path}' does not exist`
|
@@ -1917,6 +1904,7 @@ class ValOps {
|
|
1917
1904
|
};
|
1918
1905
|
}
|
1919
1906
|
if (!schema) {
|
1907
|
+
console.error(`Cannot patch. Module schema at path: '${path}' does not exist`);
|
1920
1908
|
return {
|
1921
1909
|
error: {
|
1922
1910
|
message: `Cannot patch. Module schema at path: '${path}' does not exist`
|
@@ -1934,10 +1922,12 @@ class ValOps {
|
|
1934
1922
|
filePath
|
1935
1923
|
} = op;
|
1936
1924
|
if (files[filePath]) {
|
1925
|
+
console.error(`Cannot have multiple files with same path in same patch. Path: ${filePath}`);
|
1937
1926
|
files[filePath] = {
|
1938
1927
|
error: new PatchError("Cannot have multiple files with same path in same patch")
|
1939
1928
|
};
|
1940
1929
|
} else if (typeof value !== "string") {
|
1930
|
+
console.error(`Value is not a string. Path: ${filePath}. Value: ${value}`);
|
1941
1931
|
files[filePath] = {
|
1942
1932
|
error: new PatchError("Value is not a string")
|
1943
1933
|
};
|
@@ -1953,15 +1943,14 @@ class ValOps {
|
|
1953
1943
|
path: op.path,
|
1954
1944
|
filePath,
|
1955
1945
|
nestedFilePath: op.nestedFilePath,
|
1956
|
-
value:
|
1957
|
-
sha256
|
1958
|
-
}
|
1946
|
+
value: sha256
|
1959
1947
|
});
|
1960
1948
|
}
|
1961
1949
|
}
|
1962
1950
|
}
|
1963
1951
|
const saveRes = await this.saveSourceFilePatch(path, sourceFileOps, authorId);
|
1964
1952
|
if (saveRes.error) {
|
1953
|
+
console.error(`Could not save source file patch at path: '${path}'. Error: ${saveRes.error.message}`);
|
1965
1954
|
return {
|
1966
1955
|
error: saveRes.error
|
1967
1956
|
};
|
@@ -1985,17 +1974,20 @@ class ValOps {
|
|
1985
1974
|
? "image" : schemaAtPath instanceof FileSchema ? "file" : schemaAtPath.serialize().type;
|
1986
1975
|
} catch (e) {
|
1987
1976
|
if (e instanceof Error) {
|
1977
|
+
console.error(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`);
|
1988
1978
|
return {
|
1989
1979
|
filePath,
|
1990
1980
|
error: new PatchError(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`)
|
1991
1981
|
};
|
1992
1982
|
}
|
1983
|
+
console.error(`Could not resolve file type at: ${modulePath}. Unknown error.`);
|
1993
1984
|
return {
|
1994
1985
|
filePath,
|
1995
1986
|
error: new PatchError(`Could not resolve file type at: ${modulePath}. Unknown error.`)
|
1996
1987
|
};
|
1997
1988
|
}
|
1998
1989
|
if (type !== "image" && type !== "file") {
|
1990
|
+
console.error("Unknown file type (resolved from schema): " + type);
|
1999
1991
|
return {
|
2000
1992
|
filePath,
|
2001
1993
|
error: new PatchError("Unknown file type (resolved from schema): " + type)
|
@@ -2003,6 +1995,7 @@ class ValOps {
|
|
2003
1995
|
}
|
2004
1996
|
const mimeType = getMimeTypeFromBase64(data.value);
|
2005
1997
|
if (!mimeType) {
|
1998
|
+
console.error("Could not get mimeType from base 64 encoded value");
|
2006
1999
|
return {
|
2007
2000
|
filePath,
|
2008
2001
|
error: new PatchError("Could not get mimeType from base 64 encoded value. First chars were: " + data.value.slice(0, 20))
|
@@ -2010,6 +2003,7 @@ class ValOps {
|
|
2010
2003
|
}
|
2011
2004
|
const buffer = bufferFromDataUrl(data.value);
|
2012
2005
|
if (!buffer) {
|
2006
|
+
console.error("Could not create buffer from base 64 encoded value");
|
2013
2007
|
return {
|
2014
2008
|
filePath,
|
2015
2009
|
error: new PatchError("Could not create buffer from base 64 encoded value")
|
@@ -2017,6 +2011,7 @@ class ValOps {
|
|
2017
2011
|
}
|
2018
2012
|
const metadataOps = createMetadataFromBuffer(type, mimeType, buffer);
|
2019
2013
|
if (metadataOps.errors) {
|
2014
|
+
console.error(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`);
|
2020
2015
|
return {
|
2021
2016
|
filePath,
|
2022
2017
|
error: new PatchError(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`)
|
@@ -2038,6 +2033,14 @@ class ValOps {
|
|
2038
2033
|
};
|
2039
2034
|
}
|
2040
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
|
+
}
|
2041
2044
|
return {
|
2042
2045
|
patchId,
|
2043
2046
|
files: saveFileRes,
|
@@ -2062,14 +2065,13 @@ function isFileSource(value) {
|
|
2062
2065
|
}
|
2063
2066
|
function getFieldsForType(type) {
|
2064
2067
|
if (type === "file") {
|
2065
|
-
return ["
|
2068
|
+
return ["mimeType"];
|
2066
2069
|
} else if (type === "image") {
|
2067
|
-
return ["
|
2070
|
+
return ["mimeType", "height", "width"];
|
2068
2071
|
}
|
2069
2072
|
throw new Error("Unknown type: " + type);
|
2070
2073
|
}
|
2071
2074
|
function createMetadataFromBuffer(type, mimeType, buffer) {
|
2072
|
-
const sha256 = getSha256(mimeType, buffer);
|
2073
2075
|
const errors = [];
|
2074
2076
|
let availableMetadata;
|
2075
2077
|
if (type === "image") {
|
@@ -2078,7 +2080,7 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
|
|
2078
2080
|
height,
|
2079
2081
|
type
|
2080
2082
|
} = sizeOf(buffer);
|
2081
|
-
const normalizedType = type === "jpg" ? "jpeg" : type;
|
2083
|
+
const normalizedType = type === "jpg" ? "jpeg" : type === "svg" ? "svg+xml" : type;
|
2082
2084
|
if (type !== undefined && `image/${normalizedType}` !== mimeType) {
|
2083
2085
|
return {
|
2084
2086
|
errors: [{
|
@@ -2087,14 +2089,12 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
|
|
2087
2089
|
};
|
2088
2090
|
}
|
2089
2091
|
availableMetadata = {
|
2090
|
-
sha256: sha256,
|
2091
2092
|
mimeType,
|
2092
2093
|
height,
|
2093
2094
|
width
|
2094
2095
|
};
|
2095
2096
|
} else {
|
2096
2097
|
availableMetadata = {
|
2097
|
-
sha256: sha256,
|
2098
2098
|
mimeType
|
2099
2099
|
};
|
2100
2100
|
}
|
@@ -2214,9 +2214,7 @@ const OperationT = z$1.discriminatedUnion("op", [z$1.object({
|
|
2214
2214
|
path: z$1.array(z$1.string()),
|
2215
2215
|
filePath: z$1.string(),
|
2216
2216
|
nestedFilePath: z$1.array(z$1.string()).optional(),
|
2217
|
-
value: z$1.
|
2218
|
-
sha256: z$1.string()
|
2219
|
-
})])
|
2217
|
+
value: z$1.string()
|
2220
2218
|
}).strict()]);
|
2221
2219
|
const Patch = z$1.array(OperationT);
|
2222
2220
|
|
@@ -2230,6 +2228,161 @@ class ValOpsFS extends ValOps {
|
|
2230
2228
|
async onInit() {
|
2231
2229
|
// do nothing
|
2232
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
|
+
}
|
2233
2386
|
async readPatches(includes) {
|
2234
2387
|
const patchesCacheDir = this.getPatchesDir();
|
2235
2388
|
let patchJsonFiles = [];
|
@@ -2238,8 +2391,8 @@ class ValOpsFS extends ValOps {
|
|
2238
2391
|
}
|
2239
2392
|
const patches = {};
|
2240
2393
|
const errors = {};
|
2241
|
-
const
|
2242
|
-
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) {
|
2243
2396
|
if (Number.isNaN(patchIdNum)) {
|
2244
2397
|
throw new Error("Could not parse patch id from file name. Files found: " + patchJsonFiles.join(", "));
|
2245
2398
|
}
|
@@ -2280,6 +2433,12 @@ class ValOpsFS extends ValOps {
|
|
2280
2433
|
errors: allErrors,
|
2281
2434
|
patches: allPatches
|
2282
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
|
+
}
|
2283
2442
|
for (const [patchIdS, patch] of Object.entries(allPatches)) {
|
2284
2443
|
const patchId = patchIdS;
|
2285
2444
|
if (filters.authors && !(patch.authorId === null || filters.authors.includes(patch.authorId))) {
|
@@ -2295,10 +2454,6 @@ class ValOpsFS extends ValOps {
|
|
2295
2454
|
authorId: patch.authorId,
|
2296
2455
|
appliedAt: patch.appliedAt
|
2297
2456
|
};
|
2298
|
-
const error = allErrors && allErrors[patchId];
|
2299
|
-
if (error) {
|
2300
|
-
errors[patchId] = error;
|
2301
|
-
}
|
2302
2457
|
}
|
2303
2458
|
if (errors && Object.keys(errors).length > 0) {
|
2304
2459
|
return {
|
@@ -2642,11 +2797,11 @@ class ValOpsFS extends ValOps {
|
|
2642
2797
|
getPatchDir(patchId) {
|
2643
2798
|
return fsPath__default.join(this.getPatchesDir(), patchId);
|
2644
2799
|
}
|
2645
|
-
getBinaryFilePath(
|
2646
|
-
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));
|
2647
2802
|
}
|
2648
|
-
getBinaryFileMetadataPath(
|
2649
|
-
return fsPath__default.join(this.getPatchDir(patchId), "files",
|
2803
|
+
getBinaryFileMetadataPath(filePath, patchId) {
|
2804
|
+
return fsPath__default.join(this.getPatchDir(patchId), "files", filePath, "metadata.json");
|
2650
2805
|
}
|
2651
2806
|
getPatchFilePath(patchId) {
|
2652
2807
|
return fsPath__default.join(this.getPatchDir(patchId), "patch.json");
|
@@ -2719,12 +2874,10 @@ const BaseSha = z.string().refine(s => !!s); // TODO: validate
|
|
2719
2874
|
const AuthorId = z.string().refine(s => !!s); // TODO: validate
|
2720
2875
|
const ModuleFilePath = z.string().refine(s => !!s); // TODO: validate
|
2721
2876
|
const Metadata = z.union([z.object({
|
2722
|
-
sha256: z.string(),
|
2723
2877
|
mimeType: z.string(),
|
2724
2878
|
width: z.number(),
|
2725
2879
|
height: z.number()
|
2726
2880
|
}), z.object({
|
2727
|
-
sha256: z.string(),
|
2728
2881
|
mimeType: z.string()
|
2729
2882
|
})]);
|
2730
2883
|
const MetadataRes = z.object({
|
@@ -2796,7 +2949,9 @@ const CommitResponse = z.object({
|
|
2796
2949
|
branch: z.string()
|
2797
2950
|
});
|
2798
2951
|
class ValOpsHttp extends ValOps {
|
2799
|
-
constructor(hostUrl, project, commitSha,
|
2952
|
+
constructor(hostUrl, project, commitSha,
|
2953
|
+
// TODO: CommitSha
|
2954
|
+
branch, apiKey, valModules, options) {
|
2800
2955
|
super(valModules, options);
|
2801
2956
|
this.hostUrl = hostUrl;
|
2802
2957
|
this.project = project;
|
@@ -2810,7 +2965,150 @@ class ValOpsHttp extends ValOps {
|
|
2810
2965
|
async onInit() {
|
2811
2966
|
// TODO: unused for now. Implement or remove
|
2812
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
|
+
}
|
2813
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) {
|
2814
3112
|
const params = [];
|
2815
3113
|
params.push(["branch", this.branch]);
|
2816
3114
|
if (filters.patchIds) {
|
@@ -3059,7 +3357,7 @@ class ValOpsHttp extends ValOps {
|
|
3059
3357
|
if (!file) {
|
3060
3358
|
return null;
|
3061
3359
|
}
|
3062
|
-
return
|
3360
|
+
return bufferFromDataUrl(file.value) ?? null;
|
3063
3361
|
}
|
3064
3362
|
async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
|
3065
3363
|
const params = new URLSearchParams();
|
@@ -3167,6 +3465,7 @@ class ValOpsHttp extends ValOps {
|
|
3167
3465
|
}
|
3168
3466
|
async commit(prepared, message, committer, newBranch) {
|
3169
3467
|
try {
|
3468
|
+
var _res$headers$get;
|
3170
3469
|
const existingBranch = this.branch;
|
3171
3470
|
const res = await fetch(`${this.hostUrl}/v1/${this.project}/commit`, {
|
3172
3471
|
method: "POST",
|
@@ -3202,6 +3501,22 @@ class ValOpsHttp extends ValOps {
|
|
3202
3501
|
}
|
3203
3502
|
};
|
3204
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
|
+
}
|
3205
3520
|
return {
|
3206
3521
|
error: {
|
3207
3522
|
message: "Could not commit. HTTP error: " + res.status + " " + res.statusText
|
@@ -3222,12 +3537,14 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3222
3537
|
let serverOps;
|
3223
3538
|
if (options.mode === "fs") {
|
3224
3539
|
serverOps = new ValOpsFS(options.cwd, valModules, {
|
3225
|
-
formatter: options.formatter
|
3540
|
+
formatter: options.formatter,
|
3541
|
+
config: options.config
|
3226
3542
|
});
|
3227
3543
|
} else if (options.mode === "http") {
|
3228
3544
|
serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
|
3229
3545
|
formatter: options.formatter,
|
3230
|
-
root: options.root
|
3546
|
+
root: options.root,
|
3547
|
+
config: options.config
|
3231
3548
|
});
|
3232
3549
|
} else {
|
3233
3550
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
@@ -3345,12 +3662,116 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3345
3662
|
};
|
3346
3663
|
}
|
3347
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);
|
3670
|
+
return {
|
3671
|
+
cookies: {
|
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
|
+
}
|
3683
|
+
}
|
3684
|
+
},
|
3685
|
+
status: 302,
|
3686
|
+
redirectTo: appAuthorizeUrl
|
3687
|
+
};
|
3688
|
+
};
|
3348
3689
|
return {
|
3349
|
-
|
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
|
+
};
|
3701
|
+
}
|
3702
|
+
const query = req.query;
|
3703
|
+
const redirectToRes = getRedirectUrl(query, options.valEnableRedirectUrl);
|
3704
|
+
if (typeof redirectToRes !== "string") {
|
3705
|
+
return redirectToRes;
|
3706
|
+
}
|
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
|
+
};
|
3725
|
+
}
|
3726
|
+
const query = req.query;
|
3727
|
+
const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
|
3728
|
+
if (typeof redirectToRes !== "string") {
|
3729
|
+
return redirectToRes;
|
3730
|
+
}
|
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
|
+
};
|
3749
|
+
}
|
3750
|
+
return {
|
3751
|
+
status: 200,
|
3752
|
+
json: {
|
3753
|
+
draftMode: await callbacks.isEnabled()
|
3754
|
+
}
|
3755
|
+
};
|
3756
|
+
}
|
3757
|
+
},
|
3350
3758
|
"/enable": {
|
3351
3759
|
GET: async req => {
|
3760
|
+
const cookies = req.cookies;
|
3761
|
+
const auth = getAuth(cookies);
|
3352
3762
|
const query = req.query;
|
3353
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
|
+
};
|
3774
|
+
}
|
3354
3775
|
if (typeof redirectToRes !== "string") {
|
3355
3776
|
return redirectToRes;
|
3356
3777
|
}
|
@@ -3366,6 +3787,16 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3366
3787
|
},
|
3367
3788
|
"/disable": {
|
3368
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
|
+
};
|
3799
|
+
}
|
3369
3800
|
const query = req.query;
|
3370
3801
|
const redirectToRes = getRedirectUrl(query, options.valDisableRedirectUrl);
|
3371
3802
|
if (typeof redirectToRes !== "string") {
|
@@ -3383,6 +3814,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3383
3814
|
};
|
3384
3815
|
}
|
3385
3816
|
},
|
3817
|
+
//#region auth
|
3386
3818
|
"/authorize": {
|
3387
3819
|
GET: async req => {
|
3388
3820
|
const query = req.query;
|
@@ -3394,28 +3826,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3394
3826
|
}
|
3395
3827
|
};
|
3396
3828
|
}
|
3397
|
-
const
|
3398
|
-
|
3399
|
-
const appAuthorizeUrl = getAuthorizeUrl(`${redirectUrl.origin}/${options.route}`, token);
|
3400
|
-
await callbacks.onEnable(true);
|
3401
|
-
return {
|
3402
|
-
cookies: {
|
3403
|
-
[VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE,
|
3404
|
-
[VAL_STATE_COOKIE]: {
|
3405
|
-
value: createStateCookie({
|
3406
|
-
redirect_to: query.redirect_to,
|
3407
|
-
token
|
3408
|
-
}),
|
3409
|
-
options: {
|
3410
|
-
httpOnly: true,
|
3411
|
-
sameSite: "lax",
|
3412
|
-
expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
|
3413
|
-
}
|
3414
|
-
}
|
3415
|
-
},
|
3416
|
-
status: 302,
|
3417
|
-
redirectTo: appAuthorizeUrl
|
3418
|
-
};
|
3829
|
+
const redirectTo = query.redirect_to;
|
3830
|
+
return authorize(redirectTo);
|
3419
3831
|
}
|
3420
3832
|
},
|
3421
3833
|
"/callback": {
|
@@ -3587,6 +3999,46 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3587
3999
|
};
|
3588
4000
|
}
|
3589
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
|
+
},
|
3590
4042
|
//#region patches
|
3591
4043
|
"/patches/~": {
|
3592
4044
|
GET: async req => {
|
@@ -3681,7 +4133,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3681
4133
|
};
|
3682
4134
|
}
|
3683
4135
|
},
|
3684
|
-
//#region
|
4136
|
+
//#region schema
|
3685
4137
|
"/schema": {
|
3686
4138
|
GET: async req => {
|
3687
4139
|
const cookies = req.cookies;
|
@@ -3708,7 +4160,7 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3708
4160
|
return {
|
3709
4161
|
status: 500,
|
3710
4162
|
json: {
|
3711
|
-
message:
|
4163
|
+
message: `Got errors while fetching modules: ${moduleErrors.filter(error => error).map(error => error.message).join(", ")}`,
|
3712
4164
|
details: moduleErrors
|
3713
4165
|
}
|
3714
4166
|
};
|
@@ -3716,9 +4168,22 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3716
4168
|
const schemaSha = await serverOps.getSchemaSha();
|
3717
4169
|
const schemas = await serverOps.getSchemas();
|
3718
4170
|
const serializedSchemas = {};
|
3719
|
-
|
3720
|
-
const
|
3721
|
-
|
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
|
+
};
|
3722
4187
|
}
|
3723
4188
|
return {
|
3724
4189
|
status: 200,
|
@@ -3729,7 +4194,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3729
4194
|
};
|
3730
4195
|
}
|
3731
4196
|
},
|
3732
|
-
|
4197
|
+
// #region sources
|
4198
|
+
"/sources": {
|
3733
4199
|
PUT: async req => {
|
3734
4200
|
var _body$patchIds;
|
3735
4201
|
const query = req.query;
|
@@ -3766,8 +4232,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3766
4232
|
}
|
3767
4233
|
let tree;
|
3768
4234
|
let patchAnalysis = null;
|
3769
|
-
let
|
3770
|
-
if (body !== null && body !== void 0 && body.patchIds && (body === null || body === void 0 || (_body$patchIds = body.patchIds) === null || _body$patchIds === void 0 ? void 0 : _body$patchIds.length) > 0 || body !== null && body !== void 0 && body.
|
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) {
|
3771
4237
|
// TODO: validate patches_sha
|
3772
4238
|
const patchIds = body === null || body === void 0 ? void 0 : body.patchIds;
|
3773
4239
|
const patchOps = patchIds && patchIds.length > 0 ? await serverOps.fetchPatches({
|
@@ -3776,6 +4242,15 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3776
4242
|
}) : {
|
3777
4243
|
patches: {}
|
3778
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
|
+
}
|
3779
4254
|
let patchErrors = undefined;
|
3780
4255
|
for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
|
3781
4256
|
const patchId = patchIdS;
|
@@ -3786,45 +4261,43 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3786
4261
|
message: error.message
|
3787
4262
|
};
|
3788
4263
|
}
|
3789
|
-
|
3790
|
-
|
3791
|
-
|
3792
|
-
const
|
3793
|
-
|
3794
|
-
|
3795
|
-
|
3796
|
-
|
3797
|
-
|
3798
|
-
|
3799
|
-
|
3800
|
-
|
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
|
3801
4295
|
};
|
4296
|
+
patchAnalysis.patchesByModule[newPatchModuleFilePath] = [...(patchAnalysis.patchesByModule[newPatchModuleFilePath] || []), {
|
4297
|
+
patchId: createPatchRes.patchId
|
4298
|
+
}];
|
3802
4299
|
}
|
3803
|
-
// TODO: evaluate if we need this: seems wrong to delete patches that are not applied
|
3804
|
-
// for (const fileRes of createPatchRes.files) {
|
3805
|
-
// if (fileRes.error) {
|
3806
|
-
// // clean up broken patch:
|
3807
|
-
// await this.serverOps.deletePatches([createPatchRes.patchId]);
|
3808
|
-
// return {
|
3809
|
-
// status: 500,
|
3810
|
-
// json: {
|
3811
|
-
// message: "Failed to create patch",
|
3812
|
-
// details: fileRes.error,
|
3813
|
-
// },
|
3814
|
-
// };
|
3815
|
-
// }
|
3816
|
-
// }
|
3817
|
-
newPatchId = createPatchRes.patchId;
|
3818
|
-
patchOps.patches[createPatchRes.patchId] = {
|
3819
|
-
path: newPatchModuleFilePath,
|
3820
|
-
patch: newPatchOps,
|
3821
|
-
authorId,
|
3822
|
-
createdAt: createPatchRes.createdAt,
|
3823
|
-
appliedAt: null
|
3824
|
-
};
|
3825
4300
|
}
|
3826
|
-
// TODO: errors
|
3827
|
-
patchAnalysis = serverOps.analyzePatches(patchOps.patches);
|
3828
4301
|
tree = {
|
3829
4302
|
...(await serverOps.getTree({
|
3830
4303
|
...patchAnalysis,
|
@@ -3847,27 +4320,13 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3847
4320
|
} else {
|
3848
4321
|
tree = await serverOps.getTree();
|
3849
4322
|
}
|
3850
|
-
|
3851
|
-
|
3852
|
-
|
3853
|
-
|
3854
|
-
json: {
|
3855
|
-
type: "patch-error",
|
3856
|
-
errors: Object.fromEntries(Object.entries(tree.errors).map(([key, value]) => [key, value.map(error => ({
|
3857
|
-
patchId: error.patchId,
|
3858
|
-
skipped: error.skipped,
|
3859
|
-
error: {
|
3860
|
-
message: error.error.message
|
3861
|
-
}
|
3862
|
-
}))])),
|
3863
|
-
message: "One or more patches failed to be applied"
|
3864
|
-
}
|
3865
|
-
};
|
3866
|
-
return res;
|
3867
|
-
}
|
4323
|
+
let sourcesValidation = {
|
4324
|
+
errors: {},
|
4325
|
+
files: {}
|
4326
|
+
};
|
3868
4327
|
if (query.validate_sources || query.validate_binary_files) {
|
3869
4328
|
const schemas = await serverOps.getSchemas();
|
3870
|
-
|
4329
|
+
sourcesValidation = await serverOps.validateSources(schemas, tree.sources);
|
3871
4330
|
|
3872
4331
|
// TODO: send validation errors
|
3873
4332
|
if (query.validate_binary_files) {
|
@@ -3879,20 +4338,41 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3879
4338
|
for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
|
3880
4339
|
const moduleFilePath = moduleFilePathS;
|
3881
4340
|
if (moduleFilePath.startsWith(treePath)) {
|
4341
|
+
var _sourcesValidation$er;
|
3882
4342
|
modules[moduleFilePath] = {
|
3883
4343
|
source: module,
|
3884
4344
|
patches: patchAnalysis && patchAnalysis.patchesByModule[moduleFilePath] ? {
|
3885
4345
|
applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
|
3886
|
-
} : undefined
|
4346
|
+
} : undefined,
|
4347
|
+
validationErrors: (_sourcesValidation$er = sourcesValidation.errors[moduleFilePath]) === null || _sourcesValidation$er === void 0 ? void 0 : _sourcesValidation$er.validations
|
3887
4348
|
};
|
3888
4349
|
}
|
3889
4350
|
}
|
4351
|
+
if (tree.errors && Object.keys(tree.errors).length > 0) {
|
4352
|
+
const res = {
|
4353
|
+
status: 400,
|
4354
|
+
json: {
|
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"
|
4366
|
+
}
|
4367
|
+
};
|
4368
|
+
return res;
|
4369
|
+
}
|
3890
4370
|
const res = {
|
3891
4371
|
status: 200,
|
3892
4372
|
json: {
|
3893
4373
|
schemaSha,
|
3894
4374
|
modules,
|
3895
|
-
|
4375
|
+
newPatchIds
|
3896
4376
|
}
|
3897
4377
|
};
|
3898
4378
|
return res;
|
@@ -3938,10 +4418,10 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3938
4418
|
...patches
|
3939
4419
|
});
|
3940
4420
|
if (preparedCommit.hasErrors) {
|
3941
|
-
console.error("Failed to create commit", {
|
4421
|
+
console.error("Failed to create commit", JSON.stringify({
|
3942
4422
|
sourceFilePatchErrors: preparedCommit.sourceFilePatchErrors,
|
3943
4423
|
binaryFilePatchErrors: preparedCommit.binaryFilePatchErrors
|
3944
|
-
});
|
4424
|
+
}, null, 2));
|
3945
4425
|
return {
|
3946
4426
|
status: 400,
|
3947
4427
|
json: {
|
@@ -3957,13 +4437,34 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3957
4437
|
}
|
3958
4438
|
if (serverOps instanceof ValOpsFS) {
|
3959
4439
|
await serverOps.saveFiles(preparedCommit);
|
4440
|
+
await serverOps.deletePatches(patchIds);
|
3960
4441
|
return {
|
3961
4442
|
status: 200,
|
3962
4443
|
json: {} // TODO:
|
3963
4444
|
};
|
3964
4445
|
} else if (serverOps instanceof ValOpsHttp) {
|
3965
4446
|
if (auth.error === undefined && auth.id) {
|
3966
|
-
await serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", 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);
|
3967
4468
|
return {
|
3968
4469
|
status: 200,
|
3969
4470
|
json: {} // TODO:
|
@@ -3994,15 +4495,24 @@ const ValServer = (valModules, options, callbacks) => {
|
|
3994
4495
|
// 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
|
3995
4496
|
// Thus: attack surface + ease of attack + benefit = low probability of attack
|
3996
4497
|
// If we couldn't argue that patch ids are secret enough, then this would be a problem.
|
4498
|
+
let cacheControl;
|
3997
4499
|
let fileBuffer;
|
4500
|
+
let mimeType;
|
3998
4501
|
if (query.patch_id) {
|
3999
4502
|
fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
|
4503
|
+
mimeType = Internal.filenameToMimeType(filePath);
|
4504
|
+
cacheControl = "public, max-age=20000, immutable";
|
4000
4505
|
} else {
|
4001
4506
|
fileBuffer = await serverOps.getBinaryFile(filePath);
|
4002
4507
|
}
|
4003
4508
|
if (fileBuffer) {
|
4004
4509
|
return {
|
4005
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
|
+
},
|
4006
4516
|
body: bufferToReadableStream(fileBuffer)
|
4007
4517
|
};
|
4008
4518
|
} else {
|
@@ -4322,14 +4832,14 @@ function guessMimeTypeFromPath(filePath) {
|
|
4322
4832
|
return null;
|
4323
4833
|
}
|
4324
4834
|
|
4325
|
-
async function createValServer(valModules, route, opts, callbacks, formatter) {
|
4326
|
-
const valServerConfig = await initHandlerOptions(route, opts);
|
4835
|
+
async function createValServer(valModules, route, opts, config, callbacks, formatter) {
|
4836
|
+
const valServerConfig = await initHandlerOptions(route, opts, config);
|
4327
4837
|
return ValServer(valModules, {
|
4328
4838
|
formatter,
|
4329
4839
|
...valServerConfig
|
4330
4840
|
}, callbacks);
|
4331
4841
|
}
|
4332
|
-
async function initHandlerOptions(route, opts) {
|
4842
|
+
async function initHandlerOptions(route, opts, config) {
|
4333
4843
|
const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
|
4334
4844
|
const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
|
4335
4845
|
const isProxyMode = opts.mode === "proxy" || opts.mode === undefined && (maybeApiKey || maybeValSecret);
|
@@ -4374,7 +4884,8 @@ async function initHandlerOptions(route, opts) {
|
|
4374
4884
|
valEnableRedirectUrl,
|
4375
4885
|
valDisableRedirectUrl,
|
4376
4886
|
valContentUrl,
|
4377
|
-
valBuildUrl
|
4887
|
+
valBuildUrl,
|
4888
|
+
config
|
4378
4889
|
};
|
4379
4890
|
} else {
|
4380
4891
|
const cwd = process.cwd();
|
@@ -4388,7 +4899,8 @@ async function initHandlerOptions(route, opts) {
|
|
4388
4899
|
valBuildUrl,
|
4389
4900
|
apiKey: maybeApiKey,
|
4390
4901
|
valSecret: maybeValSecret,
|
4391
|
-
project: maybeValProject
|
4902
|
+
project: maybeValProject,
|
4903
|
+
config
|
4392
4904
|
};
|
4393
4905
|
}
|
4394
4906
|
}
|
@@ -4557,12 +5069,25 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
4557
5069
|
}
|
4558
5070
|
};
|
4559
5071
|
}
|
4560
|
-
|
4561
|
-
|
4562
|
-
|
4563
|
-
|
4564
|
-
|
4565
|
-
|
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");
|
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
|
+
};
|
4566
5091
|
}
|
4567
5092
|
const cookiesRes = reqDefinition.cookies ? getCookies(req, reqDefinition.cookies) : {
|
4568
5093
|
success: true,
|
@@ -4593,7 +5118,7 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
4593
5118
|
}
|
4594
5119
|
// convert boolean to union of literals true and false so we can parse it as a string
|
4595
5120
|
if (innerType instanceof z.ZodBoolean) {
|
4596
|
-
innerType = z.union([z.literal("true"), z.literal("false")]).transform(arg =>
|
5121
|
+
innerType = z.union([z.literal("true"), z.literal("false")]).transform(arg => arg === "true");
|
4597
5122
|
}
|
4598
5123
|
// re-build rules:
|
4599
5124
|
let arrayCompatibleRule = innerType;
|
@@ -4618,6 +5143,15 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
4618
5143
|
query,
|
4619
5144
|
path
|
4620
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
|
+
}
|
4621
5155
|
const resDef = apiEndpoint.res;
|
4622
5156
|
if (resDef) {
|
4623
5157
|
const responseResult = resDef.safeParse(res);
|
@@ -4714,6 +5248,49 @@ class ValFSHost {
|
|
4714
5248
|
}
|
4715
5249
|
}
|
4716
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
|
+
|
4717
5294
|
function getValidationErrorFileRef(validationError) {
|
4718
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;
|
4719
5296
|
if (!maybeRef) {
|
@@ -4741,15 +5318,15 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4741
5318
|
throw Error("Cannot fix file without a file reference");
|
4742
5319
|
}
|
4743
5320
|
const filename = fsPath__default.join(config.projectRoot, fileRef);
|
4744
|
-
|
4745
|
-
return extractFileMetadata(fileRef
|
5321
|
+
fs.readFileSync(filename);
|
5322
|
+
return extractFileMetadata(fileRef);
|
4746
5323
|
}
|
4747
5324
|
const remainingErrors = [];
|
4748
5325
|
const patch = [];
|
4749
5326
|
for (const fix of validationError.fixes || []) {
|
4750
5327
|
if (fix === "image:replace-metadata" || fix === "image:add-metadata") {
|
4751
5328
|
const imageMetadata = await getImageMetadata();
|
4752
|
-
if (imageMetadata.width === undefined || imageMetadata.height === undefined
|
5329
|
+
if (imageMetadata.width === undefined || imageMetadata.height === undefined) {
|
4753
5330
|
remainingErrors.push({
|
4754
5331
|
...validationError,
|
4755
5332
|
message: "Failed to get image metadata",
|
@@ -4760,8 +5337,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4760
5337
|
const metadataIsCorrect =
|
4761
5338
|
// metadata is a prop that is an object
|
4762
5339
|
typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
|
4763
|
-
// sha256 is correct
|
4764
|
-
"sha256" in currentValue.metadata && currentValue.metadata.sha256 === imageMetadata.sha256 &&
|
4765
5340
|
// width is correct
|
4766
5341
|
"width" in currentValue.metadata && currentValue.metadata.width === imageMetadata.width &&
|
4767
5342
|
// height is correct
|
@@ -4778,18 +5353,11 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4778
5353
|
value: {
|
4779
5354
|
width: imageMetadata.width,
|
4780
5355
|
height: imageMetadata.height,
|
4781
|
-
sha256: imageMetadata.sha256,
|
4782
5356
|
mimeType: imageMetadata.mimeType
|
4783
5357
|
}
|
4784
5358
|
});
|
4785
5359
|
} else {
|
4786
5360
|
if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
|
4787
|
-
if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== imageMetadata.sha256) {
|
4788
|
-
remainingErrors.push({
|
4789
|
-
message: "Image metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + imageMetadata.sha256 + ".",
|
4790
|
-
fixes: undefined
|
4791
|
-
});
|
4792
|
-
}
|
4793
5361
|
if (!("width" in currentValue.metadata) || currentValue.metadata.width !== imageMetadata.width) {
|
4794
5362
|
remainingErrors.push({
|
4795
5363
|
message: "Image metadata width is incorrect! Found: " + ("width" in currentValue.metadata ? currentValue.metadata.width : "<empty>") + ". Expected: " + imageMetadata.width,
|
@@ -4824,14 +5392,13 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4824
5392
|
value: {
|
4825
5393
|
width: imageMetadata.width,
|
4826
5394
|
height: imageMetadata.height,
|
4827
|
-
sha256: imageMetadata.sha256,
|
4828
5395
|
mimeType: imageMetadata.mimeType
|
4829
5396
|
}
|
4830
5397
|
});
|
4831
5398
|
}
|
4832
5399
|
} else if (fix === "file:add-metadata" || fix === "file:check-metadata") {
|
4833
5400
|
const fileMetadata = await getFileMetadata();
|
4834
|
-
if (fileMetadata
|
5401
|
+
if (fileMetadata === undefined) {
|
4835
5402
|
remainingErrors.push({
|
4836
5403
|
...validationError,
|
4837
5404
|
message: "Failed to get image metadata",
|
@@ -4842,8 +5409,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4842
5409
|
const metadataIsCorrect =
|
4843
5410
|
// metadata is a prop that is an object
|
4844
5411
|
typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object" &&
|
4845
|
-
// sha256 is correct
|
4846
|
-
"sha256" in currentValue.metadata && currentValue.metadata.sha256 === fileMetadata.sha256 &&
|
4847
5412
|
// mimeType is correct
|
4848
5413
|
"mimeType" in currentValue.metadata && currentValue.metadata.mimeType === fileMetadata.mimeType;
|
4849
5414
|
|
@@ -4854,7 +5419,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4854
5419
|
op: "replace",
|
4855
5420
|
path: sourceToPatchPath(sourcePath).concat("metadata"),
|
4856
5421
|
value: {
|
4857
|
-
sha256: fileMetadata.sha256,
|
4858
5422
|
...(fileMetadata.mimeType ? {
|
4859
5423
|
mimeType: fileMetadata.mimeType
|
4860
5424
|
} : {})
|
@@ -4862,12 +5426,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4862
5426
|
});
|
4863
5427
|
} else {
|
4864
5428
|
if (typeof currentValue === "object" && currentValue && "metadata" in currentValue && currentValue.metadata && typeof currentValue.metadata === "object") {
|
4865
|
-
if (!("sha256" in currentValue.metadata) || currentValue.metadata.sha256 !== fileMetadata.sha256) {
|
4866
|
-
remainingErrors.push({
|
4867
|
-
message: "File metadata sha256 is incorrect! Found: " + ("sha256" in currentValue.metadata ? currentValue.metadata.sha256 : "<empty>") + ". Expected: " + fileMetadata.sha256 + ".",
|
4868
|
-
fixes: undefined
|
4869
|
-
});
|
4870
|
-
}
|
4871
5429
|
if (!("mimeType" in currentValue.metadata) || currentValue.metadata.mimeType !== fileMetadata.mimeType) {
|
4872
5430
|
remainingErrors.push({
|
4873
5431
|
message: "File metadata mimeType is incorrect! Found: " + ("mimeType" in currentValue.metadata ? currentValue.metadata.mimeType : "<empty>") + ". Expected: " + fileMetadata.mimeType,
|
@@ -4888,7 +5446,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
4888
5446
|
op: "add",
|
4889
5447
|
path: sourceToPatchPath(sourcePath).concat("metadata"),
|
4890
5448
|
value: {
|
4891
|
-
sha256: fileMetadata.sha256,
|
4892
5449
|
...(fileMetadata.mimeType ? {
|
4893
5450
|
mimeType: fileMetadata.mimeType
|
4894
5451
|
} : {})
|