@valbuild/server 0.60.27 → 0.62.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/SerializedModuleContent.d.ts +2 -2
- package/dist/declarations/src/Service.d.ts +3 -3
- package/dist/declarations/src/ValServer.d.ts +93 -133
- package/dist/declarations/src/createValApiRouter.d.ts +7 -8
- package/dist/declarations/src/index.d.ts +0 -1
- package/dist/valbuild-server.cjs.dev.js +2621 -1426
- package/dist/valbuild-server.cjs.prod.js +2621 -1426
- package/dist/valbuild-server.esm.js +2622 -1426
- package/package.json +7 -10
- package/dist/declarations/src/LocalValServer.d.ts +0 -63
@@ -1,18 +1,19 @@
|
|
1
1
|
import { newQuickJSWASMModule } from 'quickjs-emscripten';
|
2
2
|
import ts from 'typescript';
|
3
3
|
import { result, pipe } from '@valbuild/core/fp';
|
4
|
-
import { FILE_REF_PROP, FILE_REF_SUBTYPE_TAG, RT_IMAGE_TAG, VAL_EXTENSION, derefPatch, Internal, Schema,
|
4
|
+
import { FILE_REF_PROP, FILE_REF_SUBTYPE_TAG, RT_IMAGE_TAG, VAL_EXTENSION, derefPatch, Internal, Schema, ImageSchema, FileSchema } from '@valbuild/core';
|
5
5
|
import { deepEqual, isNotRoot, PatchError, parseAndValidateArrayIndex, applyPatch, JSONOps, sourceToPatchPath } from '@valbuild/core/patch';
|
6
|
-
import * as
|
7
|
-
import
|
6
|
+
import * as fsPath from 'path';
|
7
|
+
import fsPath__default from 'path';
|
8
8
|
import fs, { promises } from 'fs';
|
9
9
|
import { transform } from 'sucrase';
|
10
10
|
import { VAL_CSS_PATH, VAL_APP_ID, VAL_OVERLAY_ID } from '@valbuild/ui';
|
11
|
-
import
|
11
|
+
import { createUIRequestHandler } from '@valbuild/ui/server';
|
12
12
|
import { MIME_TYPES_TO_EXT, filenameToMimeType, VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE as VAL_STATE_COOKIE$1, VAL_SESSION_COOKIE as VAL_SESSION_COOKIE$1 } from '@valbuild/shared/internal';
|
13
|
+
import crypto$1 from 'crypto';
|
14
|
+
import z$1, { z } from 'zod';
|
13
15
|
import sizeOf from 'image-size';
|
14
|
-
import
|
15
|
-
import { createUIRequestHandler } from '@valbuild/ui/server';
|
16
|
+
import { fromError } from 'zod-validation-error';
|
16
17
|
|
17
18
|
class ValSyntaxError {
|
18
19
|
constructor(message, node) {
|
@@ -604,10 +605,10 @@ class TSOps {
|
|
604
605
|
}
|
605
606
|
|
606
607
|
function getSyntheticContainingPath(rootDir) {
|
607
|
-
return
|
608
|
+
return fsPath__default.join(rootDir, "<val>"); // TODO: this is the synthetic path used when evaluating / patching modules. I am not sure <val> is the best choice: val.ts / js better? But that is weird too. At least now it is clear(er) that it is indeed a synthetic file (i.e. not an actual file)
|
608
609
|
}
|
609
610
|
|
610
|
-
const ops
|
611
|
+
const ops = new TSOps(document => {
|
611
612
|
return pipe(analyzeValModule(document), result.map(({
|
612
613
|
source
|
613
614
|
}) => source));
|
@@ -617,12 +618,12 @@ const ops$1 = new TSOps(document => {
|
|
617
618
|
const patchValFile = async (id, rootDir, patch, sourceFileHandler, runtime) => {
|
618
619
|
// const timeId = randomUUID();
|
619
620
|
// console.time("patchValFile" + timeId);
|
620
|
-
const filePath = sourceFileHandler.resolveSourceModulePath(getSyntheticContainingPath(rootDir), `.${id
|
621
|
+
const filePath = sourceFileHandler.resolveSourceModulePath(getSyntheticContainingPath(rootDir), `.${id.replace(".val.ts", ".val").replace(".val.js", ".val").replace(".val.jsx", ".val").replace(".val.tsx", ".val")}`);
|
621
622
|
const sourceFile = sourceFileHandler.getSourceFile(filePath);
|
622
623
|
if (!sourceFile) {
|
623
624
|
throw Error(`Source file ${filePath} not found`);
|
624
625
|
}
|
625
|
-
const derefRes = derefPatch(patch, sourceFile, ops
|
626
|
+
const derefRes = derefPatch(patch, sourceFile, ops);
|
626
627
|
if (result.isErr(derefRes)) {
|
627
628
|
throw derefRes.error;
|
628
629
|
}
|
@@ -660,12 +661,12 @@ function convertDataUrlToBase64(dataUrl) {
|
|
660
661
|
}
|
661
662
|
const patchSourceFile = (sourceFile, patch) => {
|
662
663
|
if (typeof sourceFile === "string") {
|
663
|
-
return applyPatch(ts.createSourceFile("<val>", sourceFile, ts.ScriptTarget.ES2015), ops
|
664
|
+
return applyPatch(ts.createSourceFile("<val>", sourceFile, ts.ScriptTarget.ES2015), ops, patch);
|
664
665
|
}
|
665
|
-
return applyPatch(sourceFile, ops
|
666
|
+
return applyPatch(sourceFile, ops, patch);
|
666
667
|
};
|
667
668
|
|
668
|
-
const readValFile = async (
|
669
|
+
const readValFile = async (moduleFilePath, rootDirPath, runtime, options) => {
|
669
670
|
const context = runtime.newContext();
|
670
671
|
|
671
672
|
// avoid failures when console.log is called
|
@@ -700,12 +701,12 @@ const readValFile = async (id, rootDirPath, runtime, options) => {
|
|
700
701
|
processHandle.dispose();
|
701
702
|
optionsHandle.dispose();
|
702
703
|
try {
|
703
|
-
const modulePath = `.${
|
704
|
+
const modulePath = `.${moduleFilePath.replace(".val.js", ".val").replace(".val.ts", ".val").replace(".val.tsx", ".val").replace(".val.jsx", ".val")}`;
|
704
705
|
const code = `import * as valModule from ${JSON.stringify(modulePath)};
|
705
706
|
import { Internal } from "@valbuild/core";
|
706
707
|
|
707
708
|
globalThis.valModule = {
|
708
|
-
|
709
|
+
path: valModule?.default && Internal.getValPath(valModule?.default),
|
709
710
|
schema: !!globalThis['__VAL_OPTIONS__'].schema ? valModule?.default && Internal.getSchema(valModule?.default)?.serialize() : undefined,
|
710
711
|
source: !!globalThis['__VAL_OPTIONS__'].source ? valModule?.default && Internal.getSource(valModule?.default) : undefined,
|
711
712
|
validation: !!globalThis['__VAL_OPTIONS__'].validate ? valModule?.default && Internal.getSchema(valModule?.default)?.validate(
|
@@ -719,11 +720,11 @@ globalThis.valModule = {
|
|
719
720
|
const fatalErrors = [];
|
720
721
|
if (result.error) {
|
721
722
|
const error = result.error.consume(context.dump);
|
722
|
-
console.error(`Fatal error reading val file: ${
|
723
|
+
console.error(`Fatal error reading val file: ${moduleFilePath}. Error: ${error.message}\n`, error.stack);
|
723
724
|
return {
|
724
|
-
path:
|
725
|
+
path: moduleFilePath,
|
725
726
|
errors: {
|
726
|
-
|
727
|
+
invalidModulePath: moduleFilePath,
|
727
728
|
fatal: [{
|
728
729
|
message: `${error.name || "Unknown error"}: ${error.message || "<no message>"}`,
|
729
730
|
stack: error.stack
|
@@ -735,21 +736,21 @@ globalThis.valModule = {
|
|
735
736
|
const valModule = context.getProp(context.global, "valModule").consume(context.dump);
|
736
737
|
if (
|
737
738
|
// if one of these are set it is a Val module, so must validate
|
738
|
-
(valModule === null || valModule === void 0 ? void 0 : valModule.
|
739
|
-
if (valModule.
|
740
|
-
fatalErrors.push(`Wrong c.define id! Expected: '${
|
739
|
+
(valModule === null || valModule === void 0 ? void 0 : valModule.path) !== undefined || (valModule === null || valModule === void 0 ? void 0 : valModule.schema) !== undefined || (valModule === null || valModule === void 0 ? void 0 : valModule.source) !== undefined) {
|
740
|
+
if (valModule.path !== moduleFilePath) {
|
741
|
+
fatalErrors.push(`Wrong c.define id! Expected: '${moduleFilePath}', found: '${valModule.path}'`);
|
741
742
|
} else if (encodeURIComponent(valModule.id).replace(/%2F/g, "/") !== valModule.id) {
|
742
743
|
fatalErrors.push(`Invalid c.define id! Must be a web-safe path without escape characters, found: '${valModule.id}', which was encoded as: '${encodeURIComponent(valModule.id).replace("%2F", "/")}'`);
|
743
744
|
} else if ((valModule === null || valModule === void 0 ? void 0 : valModule.schema) === undefined && options.schema) {
|
744
|
-
fatalErrors.push(`Expected val id: '${
|
745
|
+
fatalErrors.push(`Expected val id: '${moduleFilePath}' to have a schema`);
|
745
746
|
} else if ((valModule === null || valModule === void 0 ? void 0 : valModule.source) === undefined && options.source) {
|
746
|
-
fatalErrors.push(`Expected val id: '${
|
747
|
+
fatalErrors.push(`Expected val id: '${moduleFilePath}' to have a source`);
|
747
748
|
}
|
748
749
|
}
|
749
750
|
let errors = false;
|
750
751
|
if (fatalErrors.length > 0) {
|
751
752
|
errors = {
|
752
|
-
|
753
|
+
invalidModulePath: valModule.path !== moduleFilePath ? moduleFilePath : undefined,
|
753
754
|
fatal: fatalErrors.map(message => ({
|
754
755
|
message
|
755
756
|
}))
|
@@ -762,7 +763,7 @@ globalThis.valModule = {
|
|
762
763
|
};
|
763
764
|
}
|
764
765
|
return {
|
765
|
-
path: valModule.id ||
|
766
|
+
path: valModule.id || moduleFilePath,
|
766
767
|
// NOTE: we use path here, since SerializedModuleContent (maybe bad name?) can be used for whole modules as well as subparts of modules
|
767
768
|
source: valModule.source,
|
768
769
|
schema: valModule.schema,
|
@@ -775,8 +776,8 @@ globalThis.valModule = {
|
|
775
776
|
};
|
776
777
|
|
777
778
|
const getCompilerOptions = (rootDir, parseConfigHost) => {
|
778
|
-
const tsConfigPath =
|
779
|
-
const jsConfigPath =
|
779
|
+
const tsConfigPath = fsPath__default.resolve(rootDir, "tsconfig.json");
|
780
|
+
const jsConfigPath = fsPath__default.resolve(rootDir, "jsconfig.json");
|
780
781
|
let configFilePath;
|
781
782
|
if (parseConfigHost.fileExists(jsConfigPath)) {
|
782
783
|
configFilePath = jsConfigPath;
|
@@ -807,7 +808,7 @@ class ValSourceFileHandler {
|
|
807
808
|
constructor(projectRoot, compilerOptions, host = {
|
808
809
|
...ts.sys,
|
809
810
|
writeFile: (fileName, data, encoding) => {
|
810
|
-
fs.mkdirSync(
|
811
|
+
fs.mkdirSync(fsPath__default.dirname(fileName), {
|
811
812
|
recursive: true
|
812
813
|
});
|
813
814
|
fs.writeFileSync(fileName, data, encoding);
|
@@ -834,7 +835,7 @@ class ValSourceFileHandler {
|
|
834
835
|
this.host.writeFile(filePath, content, encoding);
|
835
836
|
}
|
836
837
|
resolveSourceModulePath(containingFilePath, requestedModuleName) {
|
837
|
-
const resolutionRes = ts.resolveModuleName(requestedModuleName,
|
838
|
+
const resolutionRes = ts.resolveModuleName(requestedModuleName, fsPath__default.isAbsolute(containingFilePath) ? containingFilePath : fsPath__default.resolve(this.projectRoot, containingFilePath), this.compilerOptions, this.host, undefined, undefined, ts.ModuleKind.ESNext);
|
838
839
|
const resolvedModule = resolutionRes.resolvedModule;
|
839
840
|
if (!resolvedModule) {
|
840
841
|
throw Error(`Could not resolve module "${requestedModuleName}", base: "${containingFilePath}": No resolved modules returned: ${JSON.stringify(resolutionRes)}`);
|
@@ -857,7 +858,7 @@ class ValModuleLoader {
|
|
857
858
|
sourceFileHandler, host = {
|
858
859
|
...ts.sys,
|
859
860
|
writeFile: (fileName, data, encoding) => {
|
860
|
-
fs.mkdirSync(
|
861
|
+
fs.mkdirSync(fsPath__default.dirname(fileName), {
|
861
862
|
recursive: true
|
862
863
|
});
|
863
864
|
fs.writeFileSync(fileName, data, encoding);
|
@@ -1149,7 +1150,7 @@ export default new Proxy({}, {
|
|
1149
1150
|
async function createService(projectRoot, opts, host = {
|
1150
1151
|
...ts.sys,
|
1151
1152
|
writeFile: (fileName, data, encoding) => {
|
1152
|
-
fs.mkdirSync(
|
1153
|
+
fs.mkdirSync(fsPath__default.dirname(fileName), {
|
1153
1154
|
recursive: true
|
1154
1155
|
});
|
1155
1156
|
fs.writeFileSync(fileName, data, encoding);
|
@@ -1168,15 +1169,15 @@ class Service {
|
|
1168
1169
|
this.runtime = runtime;
|
1169
1170
|
this.projectRoot = projectRoot;
|
1170
1171
|
}
|
1171
|
-
async get(
|
1172
|
-
const valModule = await readValFile(
|
1172
|
+
async get(moduleFilePath, modulePath, options) {
|
1173
|
+
const valModule = await readValFile(moduleFilePath, this.projectRoot, this.runtime, options ?? {
|
1173
1174
|
validate: true,
|
1174
1175
|
source: true,
|
1175
1176
|
schema: true
|
1176
1177
|
});
|
1177
1178
|
if (valModule.source && valModule.schema) {
|
1178
1179
|
const resolved = Internal.resolvePath(modulePath, valModule.source, valModule.schema);
|
1179
|
-
const sourcePath = resolved.path ? [
|
1180
|
+
const sourcePath = resolved.path ? [moduleFilePath, resolved.path].join(".") : moduleFilePath;
|
1180
1181
|
return {
|
1181
1182
|
path: sourcePath,
|
1182
1183
|
schema: resolved.schema instanceof Schema ? resolved.schema.serialize() : resolved.schema,
|
@@ -1192,101 +1193,73 @@ class Service {
|
|
1192
1193
|
return valModule;
|
1193
1194
|
}
|
1194
1195
|
}
|
1195
|
-
async patch(
|
1196
|
-
await patchValFile(
|
1196
|
+
async patch(moduleFilePath, patch) {
|
1197
|
+
await patchValFile(moduleFilePath, this.projectRoot, patch, this.sourceFileHandler, this.runtime);
|
1197
1198
|
}
|
1198
1199
|
dispose() {
|
1199
1200
|
this.runtime.dispose();
|
1200
1201
|
}
|
1201
1202
|
}
|
1202
1203
|
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
*/
|
1208
|
-
const OperationJSONT = z.discriminatedUnion("op", [z.object({
|
1209
|
-
op: z.literal("add"),
|
1210
|
-
path: z.string(),
|
1211
|
-
value: JSONValueT
|
1212
|
-
}).strict(), z.object({
|
1213
|
-
op: z.literal("remove"),
|
1214
|
-
/**
|
1215
|
-
* Must be non-root
|
1216
|
-
*/
|
1217
|
-
path: z.string()
|
1218
|
-
}).strict(), z.object({
|
1219
|
-
op: z.literal("replace"),
|
1220
|
-
path: z.string(),
|
1221
|
-
value: JSONValueT
|
1222
|
-
}).strict(), z.object({
|
1223
|
-
op: z.literal("move"),
|
1224
|
-
/**
|
1225
|
-
* Must be non-root and not a proper prefix of "path".
|
1226
|
-
*/
|
1227
|
-
from: z.string(),
|
1228
|
-
path: z.string()
|
1229
|
-
}).strict(), z.object({
|
1230
|
-
op: z.literal("copy"),
|
1231
|
-
from: z.string(),
|
1232
|
-
path: z.string()
|
1233
|
-
}).strict(), z.object({
|
1234
|
-
op: z.literal("test"),
|
1235
|
-
path: z.string(),
|
1236
|
-
value: JSONValueT
|
1237
|
-
}).strict(), z.object({
|
1238
|
-
op: z.literal("file"),
|
1239
|
-
path: z.string(),
|
1240
|
-
filePath: z.string(),
|
1241
|
-
value: z.string()
|
1242
|
-
}).strict()]);
|
1243
|
-
const PatchJSON = z.array(OperationJSONT);
|
1244
|
-
/**
|
1245
|
-
* Raw JSON patch operation.
|
1246
|
-
*/
|
1247
|
-
const OperationT = z.discriminatedUnion("op", [z.object({
|
1248
|
-
op: z.literal("add"),
|
1249
|
-
path: z.array(z.string()),
|
1250
|
-
value: JSONValueT
|
1251
|
-
}).strict(), z.object({
|
1252
|
-
op: z.literal("remove"),
|
1253
|
-
path: z.array(z.string()).nonempty()
|
1254
|
-
}).strict(), z.object({
|
1255
|
-
op: z.literal("replace"),
|
1256
|
-
path: z.array(z.string()),
|
1257
|
-
value: JSONValueT
|
1258
|
-
}).strict(), z.object({
|
1259
|
-
op: z.literal("move"),
|
1260
|
-
from: z.array(z.string()).nonempty(),
|
1261
|
-
path: z.array(z.string())
|
1262
|
-
}).strict(), z.object({
|
1263
|
-
op: z.literal("copy"),
|
1264
|
-
from: z.array(z.string()),
|
1265
|
-
path: z.array(z.string())
|
1266
|
-
}).strict(), z.object({
|
1267
|
-
op: z.literal("test"),
|
1268
|
-
path: z.array(z.string()),
|
1269
|
-
value: JSONValueT
|
1270
|
-
}).strict(), z.object({
|
1271
|
-
op: z.literal("file"),
|
1272
|
-
path: z.array(z.string()),
|
1273
|
-
filePath: z.string(),
|
1274
|
-
value: z.union([z.string(), z.object({
|
1275
|
-
sha256: z.string(),
|
1276
|
-
mimeType: z.string()
|
1277
|
-
})])
|
1278
|
-
}).strict()]);
|
1279
|
-
const Patch = z.array(OperationT);
|
1280
|
-
|
1281
|
-
function getValidationErrorFileRef(validationError) {
|
1282
|
-
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;
|
1283
|
-
if (!maybeRef) {
|
1204
|
+
function decodeJwt(token, secretKey) {
|
1205
|
+
const [headerBase64, payloadBase64, signatureBase64, ...rest] = token.split(".");
|
1206
|
+
if (!headerBase64 || !payloadBase64 || !signatureBase64 || rest.length > 0) {
|
1207
|
+
console.debug("Invalid JWT: format is not exactly {header}.{payload}.{signature}", token);
|
1284
1208
|
return null;
|
1285
1209
|
}
|
1286
|
-
|
1210
|
+
try {
|
1211
|
+
const parsedHeader = JSON.parse(Buffer.from(headerBase64, "base64").toString("utf8"));
|
1212
|
+
const headerVerification = JwtHeaderSchema.safeParse(parsedHeader);
|
1213
|
+
if (!headerVerification.success) {
|
1214
|
+
console.debug("Invalid JWT: invalid header", parsedHeader);
|
1215
|
+
return null;
|
1216
|
+
}
|
1217
|
+
if (headerVerification.data.typ !== jwtHeader.typ) {
|
1218
|
+
console.debug("Invalid JWT: invalid header typ", parsedHeader);
|
1219
|
+
return null;
|
1220
|
+
}
|
1221
|
+
if (headerVerification.data.alg !== jwtHeader.alg) {
|
1222
|
+
console.debug("Invalid JWT: invalid header alg", parsedHeader);
|
1223
|
+
return null;
|
1224
|
+
}
|
1225
|
+
} catch (err) {
|
1226
|
+
console.debug("Invalid JWT: could not parse header", err);
|
1227
|
+
return null;
|
1228
|
+
}
|
1229
|
+
if (secretKey) {
|
1230
|
+
const signature = crypto$1.createHmac("sha256", secretKey).update(`${headerBase64}.${payloadBase64}`).digest("base64");
|
1231
|
+
if (signature !== signatureBase64) {
|
1232
|
+
console.debug("Invalid JWT: invalid signature");
|
1233
|
+
return null;
|
1234
|
+
}
|
1235
|
+
}
|
1236
|
+
try {
|
1237
|
+
const parsedPayload = JSON.parse(Buffer.from(payloadBase64, "base64").toString("utf8"));
|
1238
|
+
return parsedPayload;
|
1239
|
+
} catch (err) {
|
1240
|
+
console.debug("Invalid JWT: could not parse payload", err);
|
1241
|
+
return null;
|
1242
|
+
}
|
1243
|
+
}
|
1244
|
+
function getExpire() {
|
1245
|
+
return Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 4; // 4 days
|
1246
|
+
}
|
1247
|
+
const JwtHeaderSchema = z.object({
|
1248
|
+
alg: z.literal("HS256"),
|
1249
|
+
typ: z.literal("JWT")
|
1250
|
+
});
|
1251
|
+
const jwtHeader = {
|
1252
|
+
alg: "HS256",
|
1253
|
+
typ: "JWT"
|
1254
|
+
};
|
1255
|
+
const jwtHeaderBase64 = Buffer.from(JSON.stringify(jwtHeader)).toString("base64");
|
1256
|
+
function encodeJwt(payload, sessionKey) {
|
1257
|
+
// NOTE: this is only used for authentication, not for authorization (i.e. what a user can do) - this is handled when actually doing operations
|
1258
|
+
const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString("base64");
|
1259
|
+
return `${jwtHeaderBase64}.${payloadBase64}.${crypto$1.createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
|
1287
1260
|
}
|
1288
1261
|
|
1289
|
-
const textEncoder$
|
1262
|
+
const textEncoder$2 = new TextEncoder();
|
1290
1263
|
async function extractImageMetadata(filename, input) {
|
1291
1264
|
const imageSize = sizeOf(input);
|
1292
1265
|
let mimeType = null;
|
@@ -1320,7 +1293,7 @@ async function extractImageMetadata(filename, input) {
|
|
1320
1293
|
};
|
1321
1294
|
}
|
1322
1295
|
function getSha256(mimeType, input) {
|
1323
|
-
return Internal.getSHA256Hash(textEncoder$
|
1296
|
+
return Internal.getSHA256Hash(textEncoder$2.encode(
|
1324
1297
|
// TODO: we should probably store the mimetype in the metadata and reuse it here
|
1325
1298
|
`data:${mimeType};base64,${input.toString("base64")}`));
|
1326
1299
|
}
|
@@ -1336,559 +1309,819 @@ async function extractFileMetadata(filename, input) {
|
|
1336
1309
|
};
|
1337
1310
|
}
|
1338
1311
|
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
};
|
1351
|
-
}
|
1352
|
-
const recordMetadata = actualMetadata;
|
1353
|
-
const globalErrors = [];
|
1354
|
-
for (const anyKey in expectedMetadata) {
|
1355
|
-
if (typeof anyKey !== "string") {
|
1356
|
-
globalErrors.push(`Expected metadata has key '${anyKey}' that is not typeof 'string', but: '${typeof anyKey}'. This is most likely a Val bug.`);
|
1357
|
-
} else {
|
1358
|
-
if (anyKey in actualMetadata) {
|
1359
|
-
const key = anyKey;
|
1360
|
-
if (expectedMetadata[key] !== recordMetadata[key]) {
|
1361
|
-
erroneousMetadata[key] = `Expected metadata '${key}' to be ${JSON.stringify(expectedMetadata[key])}, but got ${JSON.stringify(recordMetadata[key])}.`;
|
1362
|
-
}
|
1363
|
-
} else {
|
1364
|
-
missingMetadata.push(anyKey);
|
1365
|
-
}
|
1366
|
-
}
|
1367
|
-
}
|
1368
|
-
if (globalErrors.length === 0 && missingMetadata.length === 0 && Object.keys(erroneousMetadata).length === 0) {
|
1369
|
-
return false;
|
1370
|
-
}
|
1371
|
-
return {
|
1372
|
-
missingMetadata,
|
1373
|
-
erroneousMetadata
|
1374
|
-
};
|
1375
|
-
}
|
1312
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
1313
|
+
const textEncoder$1 = new TextEncoder();
|
1314
|
+
const jsonOps = new JSONOps();
|
1315
|
+
const tsOps = new TSOps(document => {
|
1316
|
+
return pipe(analyzeValModule(document), result.map(({
|
1317
|
+
source
|
1318
|
+
}) => source));
|
1319
|
+
});
|
1320
|
+
// #region ValOps
|
1321
|
+
class ValOps {
|
1322
|
+
/** Sources from val modules, immutable (without patches or anything) */
|
1376
1323
|
|
1377
|
-
|
1378
|
-
const maybeMetadata = validationError.value && typeof validationError.value === "object" && "metadata" in validationError.value && validationError.value.metadata && validationError.value.metadata;
|
1379
|
-
if (!maybeMetadata) {
|
1380
|
-
return null;
|
1381
|
-
}
|
1382
|
-
return maybeMetadata;
|
1383
|
-
}
|
1324
|
+
/** The sha265 / hash of sources + schema + config */
|
1384
1325
|
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
1326
|
+
/** Schema from val modules, immutable */
|
1327
|
+
|
1328
|
+
/** The sha265 / hash of schema + config - if this changes users needs to reload */
|
1329
|
+
|
1330
|
+
constructor(valModules, options) {
|
1331
|
+
this.valModules = valModules;
|
1389
1332
|
this.options = options;
|
1390
|
-
this.
|
1333
|
+
this.sources = null;
|
1334
|
+
this.baseSha = null;
|
1335
|
+
this.schemas = null;
|
1336
|
+
this.schemaSha = null;
|
1337
|
+
this.modulesErrors = null;
|
1338
|
+
}
|
1339
|
+
hash(input) {
|
1340
|
+
let str;
|
1341
|
+
if (typeof input === "string") {
|
1342
|
+
str = input;
|
1343
|
+
} else {
|
1344
|
+
str = JSON.stringify(input);
|
1345
|
+
}
|
1346
|
+
return Internal.getSHA256Hash(textEncoder$1.encode(str));
|
1391
1347
|
}
|
1392
1348
|
|
1393
|
-
|
1394
|
-
async
|
1395
|
-
|
1396
|
-
|
1397
|
-
|
1349
|
+
// #region initTree
|
1350
|
+
async initTree() {
|
1351
|
+
if (this.baseSha === null || this.schemaSha === null || this.sources === null || this.schemas === null || this.modulesErrors === null) {
|
1352
|
+
const currentModulesErrors = [];
|
1353
|
+
const addModuleError = (message, index, path) => {
|
1354
|
+
currentModulesErrors[index] = {
|
1355
|
+
message,
|
1356
|
+
path: path
|
1357
|
+
};
|
1358
|
+
};
|
1359
|
+
const currentSources = {};
|
1360
|
+
const currentSchemas = {};
|
1361
|
+
let baseSha = this.hash(JSON.stringify(this.valModules.config));
|
1362
|
+
let schemaSha = baseSha;
|
1363
|
+
for (let moduleIdx = 0; moduleIdx < this.valModules.modules.length; moduleIdx++) {
|
1364
|
+
const module = this.valModules.modules[moduleIdx];
|
1365
|
+
if (!module.def) {
|
1366
|
+
addModuleError("val.modules is missing 'def' property", moduleIdx);
|
1367
|
+
continue;
|
1368
|
+
}
|
1369
|
+
if (typeof module.def !== "function") {
|
1370
|
+
addModuleError("val.modules 'def' property is not a function", moduleIdx);
|
1371
|
+
continue;
|
1372
|
+
}
|
1373
|
+
await module.def().then(value => {
|
1374
|
+
if (!value) {
|
1375
|
+
addModuleError(`val.modules 'def' did not return a value`, moduleIdx);
|
1376
|
+
return;
|
1377
|
+
}
|
1378
|
+
if (!value.default) {
|
1379
|
+
addModuleError(`val.modules 'def' did not return a default export`, moduleIdx);
|
1380
|
+
return;
|
1381
|
+
}
|
1382
|
+
const path = Internal.getValPath(value.default);
|
1383
|
+
if (path === undefined) {
|
1384
|
+
addModuleError(`path is undefined`, moduleIdx);
|
1385
|
+
return;
|
1386
|
+
}
|
1387
|
+
const schema = Internal.getSchema(value.default);
|
1388
|
+
if (schema === undefined) {
|
1389
|
+
addModuleError(`schema in path '${path}' is undefined`, moduleIdx, path);
|
1390
|
+
return;
|
1391
|
+
}
|
1392
|
+
if (!(schema instanceof Schema)) {
|
1393
|
+
addModuleError(`schema in path '${path}' is not an instance of Schema`, moduleIdx, path);
|
1394
|
+
return;
|
1395
|
+
}
|
1396
|
+
if (typeof schema.serialize !== "function") {
|
1397
|
+
addModuleError(`schema.serialize in path '${path}' is not a function`, moduleIdx, path);
|
1398
|
+
return;
|
1399
|
+
}
|
1400
|
+
const source = Internal.getSource(value.default);
|
1401
|
+
if (source === undefined) {
|
1402
|
+
addModuleError(`source in ${path} is undefined`, moduleIdx, path);
|
1403
|
+
return;
|
1404
|
+
}
|
1405
|
+
const pathM = path;
|
1406
|
+
currentSources[pathM] = source;
|
1407
|
+
currentSchemas[pathM] = schema;
|
1408
|
+
// make sure the checks above is enough that this does not fail - even if val modules are not set up correctly
|
1409
|
+
baseSha += this.hash({
|
1410
|
+
path,
|
1411
|
+
schema: schema.serialize(),
|
1412
|
+
source,
|
1413
|
+
modulesErrors: currentModulesErrors
|
1414
|
+
});
|
1415
|
+
schemaSha += this.hash(schema.serialize());
|
1416
|
+
});
|
1417
|
+
}
|
1418
|
+
this.sources = currentSources;
|
1419
|
+
this.schemas = currentSchemas;
|
1420
|
+
this.baseSha = baseSha;
|
1421
|
+
this.schemaSha = schemaSha;
|
1422
|
+
this.modulesErrors = currentModulesErrors;
|
1398
1423
|
}
|
1399
|
-
await this.callbacks.onEnable(true);
|
1400
1424
|
return {
|
1401
|
-
|
1402
|
-
|
1403
|
-
|
1404
|
-
|
1405
|
-
|
1425
|
+
baseSha: this.baseSha,
|
1426
|
+
schemaSha: this.schemaSha,
|
1427
|
+
sources: this.sources,
|
1428
|
+
schemas: this.schemas,
|
1429
|
+
moduleErrors: this.modulesErrors
|
1406
1430
|
};
|
1407
1431
|
}
|
1408
|
-
async
|
1409
|
-
const
|
1410
|
-
|
1411
|
-
|
1412
|
-
}
|
1413
|
-
await this.
|
1414
|
-
return {
|
1415
|
-
cookies: {
|
1416
|
-
[VAL_ENABLE_COOKIE_NAME]: {
|
1417
|
-
value: "false"
|
1418
|
-
}
|
1419
|
-
},
|
1420
|
-
status: 302,
|
1421
|
-
redirectTo: redirectToRes
|
1422
|
-
};
|
1432
|
+
async init() {
|
1433
|
+
const {
|
1434
|
+
baseSha,
|
1435
|
+
schemaSha
|
1436
|
+
} = await this.initTree();
|
1437
|
+
await this.onInit(baseSha, schemaSha);
|
1423
1438
|
}
|
1424
|
-
async
|
1425
|
-
|
1426
|
-
query, cookies, requestHeaders) {
|
1427
|
-
const ensureRes = await debugTiming("ensureInitialized", () => this.ensureInitialized("getTree", cookies));
|
1428
|
-
if (result.isErr(ensureRes)) {
|
1429
|
-
return ensureRes.error;
|
1430
|
-
}
|
1431
|
-
const applyPatches = query.patch === "true";
|
1432
|
-
const includeSource = query.source === "true";
|
1433
|
-
const includeSchema = query.schema === "true";
|
1434
|
-
const moduleIds = await debugTiming("getAllModules", () => this.getAllModules(treePath));
|
1435
|
-
let {
|
1436
|
-
patchIdsByModuleId,
|
1437
|
-
patchesById,
|
1438
|
-
fileUpdates
|
1439
|
-
} = {
|
1440
|
-
patchIdsByModuleId: {},
|
1441
|
-
patchesById: {},
|
1442
|
-
fileUpdates: {}
|
1443
|
-
};
|
1444
|
-
if (applyPatches) {
|
1445
|
-
const res = await debugTiming("readPatches", () => this.readPatches(cookies));
|
1446
|
-
if (result.isErr(res)) {
|
1447
|
-
return res.error;
|
1448
|
-
}
|
1449
|
-
patchIdsByModuleId = res.value.patchIdsByModuleId;
|
1450
|
-
patchesById = res.value.patchesById;
|
1451
|
-
fileUpdates = res.value.fileUpdates;
|
1452
|
-
}
|
1453
|
-
const validate = false;
|
1454
|
-
const possiblyPatchedContent = await debugTiming("applyAllPatchesThenValidate", () => Promise.all(moduleIds.map(async moduleId => {
|
1455
|
-
return this.applyAllPatchesThenValidate(moduleId, patchIdsByModuleId, patchesById, fileUpdates, cookies, requestHeaders, applyPatches, validate, includeSource, includeSchema);
|
1456
|
-
})));
|
1457
|
-
const modules = Object.fromEntries(possiblyPatchedContent.map(serializedModuleContent => {
|
1458
|
-
const module = {
|
1459
|
-
schema: serializedModuleContent.schema,
|
1460
|
-
source: serializedModuleContent.source,
|
1461
|
-
errors: serializedModuleContent.errors
|
1462
|
-
};
|
1463
|
-
return [serializedModuleContent.path, module];
|
1464
|
-
}));
|
1465
|
-
const apiTreeResponse = {
|
1466
|
-
modules,
|
1467
|
-
git: this.options.git
|
1468
|
-
};
|
1469
|
-
return {
|
1470
|
-
status: 200,
|
1471
|
-
json: apiTreeResponse
|
1472
|
-
};
|
1439
|
+
async getBaseSources() {
|
1440
|
+
return this.initTree().then(result => result.sources);
|
1473
1441
|
}
|
1474
|
-
async
|
1475
|
-
|
1476
|
-
if (result.isErr(ensureRes)) {
|
1477
|
-
return ensureRes.error;
|
1478
|
-
}
|
1479
|
-
return this.validateThenMaybeCommit(rawBody, false, cookies, requestHeaders);
|
1442
|
+
async getSchemas() {
|
1443
|
+
return this.initTree().then(result => result.schemas);
|
1480
1444
|
}
|
1481
|
-
async
|
1482
|
-
|
1483
|
-
|
1484
|
-
|
1485
|
-
|
1486
|
-
|
1487
|
-
|
1488
|
-
|
1489
|
-
return {
|
1490
|
-
status: 400,
|
1491
|
-
json: {
|
1492
|
-
...res.json
|
1493
|
-
}
|
1494
|
-
};
|
1495
|
-
}
|
1496
|
-
return {
|
1497
|
-
status: 200,
|
1498
|
-
json: {
|
1499
|
-
...res.json,
|
1500
|
-
git: this.options.git
|
1501
|
-
}
|
1502
|
-
};
|
1503
|
-
}
|
1504
|
-
return res;
|
1445
|
+
async getModuleErrors() {
|
1446
|
+
return this.initTree().then(result => result.moduleErrors);
|
1447
|
+
}
|
1448
|
+
async getBaseSha() {
|
1449
|
+
return this.initTree().then(result => result.baseSha);
|
1450
|
+
}
|
1451
|
+
async getSchemaSha() {
|
1452
|
+
return this.initTree().then(result => result.schemaSha);
|
1505
1453
|
}
|
1506
1454
|
|
1507
|
-
|
1508
|
-
|
1509
|
-
const
|
1510
|
-
|
1511
|
-
|
1512
|
-
|
1513
|
-
|
1514
|
-
|
1455
|
+
// #region analyzePatches
|
1456
|
+
analyzePatches(patchesById) {
|
1457
|
+
const patchesByModule = {};
|
1458
|
+
const fileLastUpdatedByPatchId = {};
|
1459
|
+
for (const [patchIdS, {
|
1460
|
+
path,
|
1461
|
+
patch,
|
1462
|
+
createdAt: created_at
|
1463
|
+
}] of Object.entries(patchesById)) {
|
1464
|
+
const patchId = patchIdS;
|
1465
|
+
for (const op of patch) {
|
1466
|
+
if (op.op === "file") {
|
1467
|
+
fileLastUpdatedByPatchId[op.filePath] = patchId;
|
1468
|
+
}
|
1469
|
+
}
|
1470
|
+
if (!patchesByModule[path]) {
|
1471
|
+
patchesByModule[path] = [];
|
1472
|
+
}
|
1473
|
+
patchesByModule[path].push({
|
1474
|
+
patchId,
|
1475
|
+
createdAt: created_at
|
1476
|
+
});
|
1515
1477
|
}
|
1516
|
-
const
|
1517
|
-
|
1518
|
-
if (serializedModuleContent.errors && (serializedModuleContent.errors.fatal || serializedModuleContent.errors.invalidModuleId)) {
|
1519
|
-
return serializedModuleContent;
|
1478
|
+
for (const path in patchesByModule) {
|
1479
|
+
patchesByModule[path].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
1520
1480
|
}
|
1521
|
-
|
1522
|
-
|
1481
|
+
return {
|
1482
|
+
patchesByModule,
|
1483
|
+
fileLastUpdatedByPatchId
|
1484
|
+
};
|
1485
|
+
}
|
1486
|
+
|
1487
|
+
// #region getTree
|
1488
|
+
async getTree(analysis) {
|
1489
|
+
if (!analysis) {
|
1490
|
+
const {
|
1491
|
+
sources
|
1492
|
+
} = await this.initTree();
|
1493
|
+
return {
|
1494
|
+
sources,
|
1495
|
+
errors: {}
|
1496
|
+
};
|
1523
1497
|
}
|
1524
|
-
|
1525
|
-
|
1526
|
-
|
1527
|
-
|
1528
|
-
|
1529
|
-
|
1498
|
+
const {
|
1499
|
+
sources
|
1500
|
+
} = await this.initTree();
|
1501
|
+
const patchedSources = {};
|
1502
|
+
const errors = {};
|
1503
|
+
for (const [pathS, patches] of Object.entries(analysis.patchesByModule)) {
|
1504
|
+
const path = pathS;
|
1505
|
+
if (!sources[path]) {
|
1506
|
+
if (!errors[path]) {
|
1507
|
+
errors[path] = [];
|
1530
1508
|
}
|
1531
|
-
|
1532
|
-
|
1533
|
-
|
1534
|
-
}
|
1535
|
-
|
1536
|
-
|
1537
|
-
|
1538
|
-
|
1539
|
-
|
1509
|
+
errors[path].push({
|
1510
|
+
invalidPath: true,
|
1511
|
+
error: new PatchError(`Module at path: '${path}' not found`)
|
1512
|
+
});
|
1513
|
+
}
|
1514
|
+
const source = sources[path];
|
1515
|
+
for (const {
|
1516
|
+
patchId
|
1517
|
+
} of patches) {
|
1518
|
+
if (errors[path]) {
|
1519
|
+
errors[path].push({
|
1520
|
+
patchId: patchId,
|
1521
|
+
error: new PatchError(`Cannot apply patch: previous errors exists`)
|
1540
1522
|
});
|
1541
|
-
|
1542
|
-
|
1543
|
-
|
1544
|
-
|
1545
|
-
|
1546
|
-
|
1547
|
-
|
1548
|
-
|
1549
|
-
|
1523
|
+
} else {
|
1524
|
+
const patchData = analysis.patches[patchId];
|
1525
|
+
if (!patchData) {
|
1526
|
+
errors[path].push({
|
1527
|
+
patchId: patchId,
|
1528
|
+
error: new PatchError(`Patch not found`)
|
1529
|
+
});
|
1530
|
+
continue;
|
1531
|
+
}
|
1532
|
+
const applicableOps = [];
|
1533
|
+
const fileFixOps = {};
|
1534
|
+
for (const op of patchData.patch) {
|
1535
|
+
if (op.op === "file") {
|
1536
|
+
// NOTE: We insert the last patch_id that modify a file
|
1537
|
+
// when constructing the url we use the patch id (and the file path)
|
1538
|
+
// to fetch the right file
|
1539
|
+
// NOTE: overwrite and use last patch_id if multiple patches modify the same file
|
1540
|
+
fileFixOps[op.path.join("/")] = [{
|
1541
|
+
op: "add",
|
1542
|
+
path: op.path.concat("patch_id"),
|
1543
|
+
value: patchId
|
1544
|
+
}];
|
1545
|
+
} else {
|
1546
|
+
applicableOps.push(op);
|
1550
1547
|
}
|
1551
|
-
};
|
1552
|
-
}
|
1553
|
-
}
|
1554
|
-
});
|
1555
|
-
if (validate) {
|
1556
|
-
const validationErrors = await debugTiming("validate:" + moduleId, async () => deserializeSchema(schema).validate(moduleId, source));
|
1557
|
-
if (validationErrors) {
|
1558
|
-
const revalidated = await debugTiming("revalidate image/file:" + moduleId, async () => this.revalidateImageAndFileValidation(validationErrors, fileUpdates, cookies, requestHeaders));
|
1559
|
-
return {
|
1560
|
-
path: moduleId,
|
1561
|
-
schema,
|
1562
|
-
source,
|
1563
|
-
errors: revalidated && {
|
1564
|
-
validation: revalidated
|
1565
1548
|
}
|
1566
|
-
|
1549
|
+
const patchRes = applyPatch(source, jsonOps, applicableOps.concat(...Object.values(fileFixOps)));
|
1550
|
+
if (result.isErr(patchRes)) {
|
1551
|
+
if (!errors[path]) {
|
1552
|
+
errors[path] = [];
|
1553
|
+
}
|
1554
|
+
errors[path].push({
|
1555
|
+
patchId: patchId,
|
1556
|
+
error: patchRes.error
|
1557
|
+
});
|
1558
|
+
} else {
|
1559
|
+
patchedSources[path] = patchRes.value;
|
1560
|
+
}
|
1561
|
+
}
|
1567
1562
|
}
|
1568
1563
|
}
|
1569
1564
|
return {
|
1570
|
-
|
1571
|
-
|
1572
|
-
source,
|
1573
|
-
errors: false
|
1565
|
+
sources: patchedSources,
|
1566
|
+
errors
|
1574
1567
|
};
|
1575
1568
|
}
|
1576
|
-
|
1577
|
-
//
|
1578
|
-
|
1579
|
-
|
1580
|
-
|
1581
|
-
|
1582
|
-
const
|
1583
|
-
for (const
|
1584
|
-
|
1585
|
-
|
1586
|
-
|
1587
|
-
|
1588
|
-
|
1589
|
-
|
1590
|
-
)
|
1591
|
-
|
1592
|
-
|
1593
|
-
|
1594
|
-
|
1595
|
-
|
1596
|
-
|
1597
|
-
|
1598
|
-
|
1599
|
-
|
1600
|
-
|
1601
|
-
|
1602
|
-
|
1603
|
-
|
1604
|
-
|
1605
|
-
|
1606
|
-
|
1607
|
-
|
1608
|
-
|
1609
|
-
|
1610
|
-
|
1611
|
-
|
1612
|
-
|
1613
|
-
}
|
1614
|
-
// try fetch file directly via http
|
1615
|
-
if (fileRef.startsWith("/public")) {
|
1616
|
-
const host = `${reqHeaders["x-forwarded-proto"]}://${reqHeaders["host"]}`;
|
1617
|
-
const fileUrl = fileRef.slice("/public".length);
|
1618
|
-
const fetchRes = await fetch(new URL(fileUrl, host));
|
1619
|
-
if (fetchRes.status === 200) {
|
1620
|
-
fileBuffer = Buffer.from(await fetchRes.arrayBuffer());
|
1621
|
-
} else {
|
1622
|
-
console.error("Val: unexpected error while fetching image / file:", fileRef, {
|
1623
|
-
error: {
|
1624
|
-
status: fetchRes.status,
|
1625
|
-
url: fetchRes.url
|
1626
|
-
}
|
1627
|
-
});
|
1628
|
-
}
|
1629
|
-
} else {
|
1630
|
-
console.error("Val: unexpected while getting public image / file (file reference did not start with /public)", fileRef);
|
1631
|
-
}
|
1632
|
-
if (!fileBuffer) {
|
1633
|
-
revalidatedValidationErrors[errorSourcePath].push({
|
1634
|
-
message: `Could not read file: ${filePath}`
|
1635
|
-
});
|
1636
|
-
continue;
|
1637
|
-
}
|
1638
|
-
if (error.fixes.some(fix => fix === "image:replace-metadata")) {
|
1639
|
-
expectedMetadata = await extractImageMetadata(filePath, fileBuffer);
|
1640
|
-
} else {
|
1641
|
-
expectedMetadata = await extractFileMetadata(filePath, fileBuffer);
|
1642
|
-
}
|
1569
|
+
|
1570
|
+
// #region validateSources
|
1571
|
+
async validateSources(schemas, sources, patchesByModule) {
|
1572
|
+
const errors = {};
|
1573
|
+
const files = {};
|
1574
|
+
const entries = Object.entries(schemas);
|
1575
|
+
const modulePathsToValidate = patchesByModule && Object.keys(patchesByModule);
|
1576
|
+
for (const [pathS, schema] of entries) {
|
1577
|
+
if (modulePathsToValidate && !modulePathsToValidate.includes(pathS)) {
|
1578
|
+
continue;
|
1579
|
+
}
|
1580
|
+
const path = pathS;
|
1581
|
+
const source = sources[path];
|
1582
|
+
if (source === undefined) {
|
1583
|
+
if (!errors[path]) {
|
1584
|
+
errors[path] = {
|
1585
|
+
validations: {}
|
1586
|
+
};
|
1587
|
+
}
|
1588
|
+
errors[path] = {
|
1589
|
+
...errors[path],
|
1590
|
+
invalidSource: {
|
1591
|
+
message: `Module at path: '${path}' does not exist`
|
1592
|
+
}
|
1593
|
+
};
|
1594
|
+
continue;
|
1595
|
+
}
|
1596
|
+
const res = schema.validate(path, source);
|
1597
|
+
if (res === false) {
|
1598
|
+
continue;
|
1599
|
+
}
|
1600
|
+
for (const [sourcePathS, validationErrors] of Object.entries(res)) {
|
1601
|
+
const sourcePath = sourcePathS;
|
1602
|
+
for (const validationError of validationErrors) {
|
1603
|
+
if (isOnlyFileCheckValidationError(validationError)) {
|
1604
|
+
if (files[sourcePath]) {
|
1605
|
+
throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
|
1643
1606
|
}
|
1644
|
-
|
1645
|
-
|
1646
|
-
|
1647
|
-
});
|
1648
|
-
} else {
|
1649
|
-
const actualMetadata = getValidationErrorMetadata(error);
|
1650
|
-
const revalidatedError = validateMetadata(actualMetadata, expectedMetadata);
|
1651
|
-
if (!revalidatedError) {
|
1652
|
-
// no errors anymore:
|
1653
|
-
continue;
|
1654
|
-
}
|
1655
|
-
const errorMsgs = (revalidatedError.globalErrors || []).concat(Object.values(revalidatedError.erroneousMetadata || {})).concat(Object.values(revalidatedError.missingMetadata || []).map(missingKey => {
|
1656
|
-
var _expectedMetadata;
|
1657
|
-
return `Required key: '${missingKey}' is not defined. Should be: '${JSON.stringify((_expectedMetadata = expectedMetadata) === null || _expectedMetadata === void 0 ? void 0 : _expectedMetadata[missingKey])}'`;
|
1658
|
-
}));
|
1659
|
-
revalidatedValidationErrors[errorSourcePath].push(...errorMsgs.map(message => ({
|
1660
|
-
message
|
1661
|
-
})));
|
1607
|
+
const value = validationError.value;
|
1608
|
+
if (isFileSource(value)) {
|
1609
|
+
files[sourcePath] = value;
|
1662
1610
|
}
|
1663
1611
|
} else {
|
1664
|
-
|
1665
|
-
|
1666
|
-
|
1667
|
-
|
1668
|
-
}
|
1669
|
-
}
|
1670
|
-
}
|
1671
|
-
const hasErrors = Object.values(revalidatedValidationErrors).some(errors => errors.length > 0);
|
1672
|
-
if (hasErrors) {
|
1673
|
-
return revalidatedValidationErrors;
|
1674
|
-
}
|
1675
|
-
return hasErrors;
|
1676
|
-
}
|
1677
|
-
sortPatchIds(patchesByModule) {
|
1678
|
-
return Object.values(patchesByModule).flatMap(modulePatches => modulePatches).sort((a, b) => {
|
1679
|
-
return a.created_at.localeCompare(b.created_at);
|
1680
|
-
}).map(patchData => patchData.patch_id);
|
1681
|
-
}
|
1682
|
-
|
1683
|
-
// can be overridden if FS cannot read from static assets / public folder (because of bundlers or what not)
|
1684
|
-
async readStaticBinaryFile(filePath) {
|
1685
|
-
return fs.promises.readFile(filePath);
|
1686
|
-
}
|
1687
|
-
async readPatches(cookies) {
|
1688
|
-
const res = await this.getPatches({},
|
1689
|
-
// {} means no ids, so get all patches
|
1690
|
-
cookies);
|
1691
|
-
if (res.status === 400 || res.status === 401 || res.status === 403 || res.status === 404 || res.status === 500 || res.status === 501) {
|
1692
|
-
return result.err(res);
|
1693
|
-
} else if (res.status === 200 || res.status === 201) {
|
1694
|
-
const patchesByModule = res.json;
|
1695
|
-
const patches = [];
|
1696
|
-
const patchIdsByModuleId = {};
|
1697
|
-
const patchesById = {};
|
1698
|
-
for (const [moduleIdS, modulePatchData] of Object.entries(patchesByModule)) {
|
1699
|
-
const moduleId = moduleIdS;
|
1700
|
-
patchIdsByModuleId[moduleId] = modulePatchData.map(patch => patch.patch_id);
|
1701
|
-
for (const patchData of modulePatchData) {
|
1702
|
-
patches.push([patchData.patch_id, moduleId, patchData.patch]);
|
1703
|
-
patchesById[patchData.patch_id] = patchData.patch;
|
1704
|
-
}
|
1705
|
-
}
|
1706
|
-
const fileUpdates = {};
|
1707
|
-
const sortedPatchIds = this.sortPatchIds(patchesByModule);
|
1708
|
-
for (const sortedPatchId of sortedPatchIds) {
|
1709
|
-
const patchId = sortedPatchId;
|
1710
|
-
for (const op of patchesById[patchId] || []) {
|
1711
|
-
if (op.op === "file") {
|
1712
|
-
const parsedFileOp = z$1.object({
|
1713
|
-
sha256: z$1.string(),
|
1714
|
-
mimeType: z$1.string()
|
1715
|
-
}).safeParse(op.value);
|
1716
|
-
if (!parsedFileOp.success) {
|
1717
|
-
return result.err({
|
1718
|
-
status: 500,
|
1719
|
-
json: {
|
1720
|
-
message: "Unexpected error: file op value must be transformed into object",
|
1721
|
-
details: {
|
1722
|
-
value: "First 200 chars: " + JSON.stringify(op.value).slice(0, 200),
|
1723
|
-
patchId
|
1724
|
-
}
|
1725
|
-
}
|
1726
|
-
});
|
1612
|
+
if (!errors[path]) {
|
1613
|
+
errors[path] = {
|
1614
|
+
validations: {}
|
1615
|
+
};
|
1727
1616
|
}
|
1728
|
-
|
1729
|
-
|
1730
|
-
}
|
1617
|
+
if (!errors[path].validations[sourcePath]) {
|
1618
|
+
errors[path].validations[sourcePath] = [];
|
1619
|
+
}
|
1620
|
+
errors[path].validations[sourcePath].push(validationError);
|
1731
1621
|
}
|
1732
1622
|
}
|
1733
1623
|
}
|
1734
|
-
return result.ok({
|
1735
|
-
patches,
|
1736
|
-
patchIdsByModuleId,
|
1737
|
-
patchesById,
|
1738
|
-
fileUpdates
|
1739
|
-
});
|
1740
|
-
} else {
|
1741
|
-
return result.err({
|
1742
|
-
status: 500,
|
1743
|
-
json: {
|
1744
|
-
message: "Unknown error"
|
1745
|
-
}
|
1746
|
-
});
|
1747
1624
|
}
|
1625
|
+
return {
|
1626
|
+
errors,
|
1627
|
+
files
|
1628
|
+
};
|
1748
1629
|
}
|
1749
|
-
|
1750
|
-
|
1751
|
-
|
1752
|
-
|
1753
|
-
|
1754
|
-
|
1755
|
-
|
1756
|
-
|
1757
|
-
|
1758
|
-
|
1759
|
-
|
1760
|
-
|
1761
|
-
|
1762
|
-
|
1763
|
-
|
1764
|
-
|
1765
|
-
|
1766
|
-
|
1767
|
-
|
1768
|
-
|
1769
|
-
|
1770
|
-
|
1771
|
-
|
1772
|
-
|
1773
|
-
|
1774
|
-
|
1775
|
-
|
1776
|
-
|
1777
|
-
|
1778
|
-
|
1779
|
-
|
1780
|
-
|
1781
|
-
|
1782
|
-
|
1783
|
-
|
1784
|
-
|
1785
|
-
if (!modules[moduleId]) {
|
1786
|
-
modules[moduleId] = {
|
1787
|
-
patches: {
|
1788
|
-
applied: []
|
1789
|
-
}
|
1630
|
+
|
1631
|
+
// #region validateFiles
|
1632
|
+
async validateFiles(schemas, sources, files, fileLastUpdatedByPatchId) {
|
1633
|
+
const validateFileAtSourcePath = async (sourcePath, value) => {
|
1634
|
+
const [fullModulePath, modulePath] = Internal.splitModuleFilePathAndModulePath(sourcePath);
|
1635
|
+
const schema = schemas[fullModulePath];
|
1636
|
+
if (!schema) {
|
1637
|
+
return {
|
1638
|
+
[sourcePath]: [{
|
1639
|
+
message: `Schema not found for path: '${fullModulePath}'`,
|
1640
|
+
value
|
1641
|
+
}]
|
1642
|
+
};
|
1643
|
+
}
|
1644
|
+
const source = sources[fullModulePath];
|
1645
|
+
if (!source) {
|
1646
|
+
return {
|
1647
|
+
[sourcePath]: [{
|
1648
|
+
message: `Source not found for path: '${fullModulePath}'`,
|
1649
|
+
value
|
1650
|
+
}]
|
1651
|
+
};
|
1652
|
+
}
|
1653
|
+
let schemaAtPath;
|
1654
|
+
try {
|
1655
|
+
const {
|
1656
|
+
schema: resolvedSchema
|
1657
|
+
} = Internal.resolvePath(modulePath, sources[fullModulePath], schemas[fullModulePath]);
|
1658
|
+
schemaAtPath = resolvedSchema;
|
1659
|
+
} catch (e) {
|
1660
|
+
if (e instanceof Error) {
|
1661
|
+
return {
|
1662
|
+
[sourcePath]: [{
|
1663
|
+
message: `Could not resolve schema at path: ${modulePath}. Error: ${e.message}`,
|
1664
|
+
value
|
1665
|
+
}]
|
1790
1666
|
};
|
1791
1667
|
}
|
1792
|
-
|
1793
|
-
|
1794
|
-
|
1795
|
-
|
1796
|
-
}
|
1797
|
-
|
1668
|
+
return {
|
1669
|
+
[sourcePath]: [{
|
1670
|
+
message: `Could not resolve schema at path: ${modulePath}. Unknown error.`,
|
1671
|
+
value
|
1672
|
+
}]
|
1673
|
+
};
|
1674
|
+
}
|
1675
|
+
const type = schemaAtPath instanceof ImageSchema ? "image" : "file";
|
1676
|
+
const filePath = value[FILE_REF_PROP];
|
1677
|
+
const patchId = (fileLastUpdatedByPatchId === null || fileLastUpdatedByPatchId === void 0 ? void 0 : fileLastUpdatedByPatchId[filePath]) || null;
|
1678
|
+
let metadata;
|
1679
|
+
let metadataErrors;
|
1680
|
+
|
1681
|
+
// TODO: refactor so we call get metadata once instead of iterating like this. Reason: should be a lot faster
|
1682
|
+
if (patchId) {
|
1683
|
+
const patchFileMetadata = await this.getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId);
|
1684
|
+
if (patchFileMetadata.errors) {
|
1685
|
+
metadataErrors = patchFileMetadata.errors;
|
1798
1686
|
} else {
|
1799
|
-
|
1687
|
+
metadata = patchFileMetadata.metadata;
|
1800
1688
|
}
|
1801
|
-
}
|
1802
|
-
|
1803
|
-
|
1804
|
-
|
1805
|
-
|
1806
|
-
|
1689
|
+
} else {
|
1690
|
+
const patchFileMetadata = await this.getBinaryFileMetadata(filePath, type);
|
1691
|
+
if (patchFileMetadata.errors) {
|
1692
|
+
metadataErrors = patchFileMetadata.errors;
|
1693
|
+
} else {
|
1694
|
+
metadata = patchFileMetadata.metadata;
|
1807
1695
|
}
|
1808
|
-
};
|
1809
|
-
}
|
1810
|
-
let modules;
|
1811
|
-
if (commit) {
|
1812
|
-
const commitRes = await this.execCommit(patches, cookies);
|
1813
|
-
if (commitRes.status !== 200) {
|
1814
|
-
return commitRes;
|
1815
1696
|
}
|
1816
|
-
|
1817
|
-
|
1818
|
-
|
1819
|
-
|
1820
|
-
|
1821
|
-
|
1822
|
-
|
1823
|
-
|
1824
|
-
|
1697
|
+
if (metadataErrors && metadataErrors.length > 0) {
|
1698
|
+
return {
|
1699
|
+
[sourcePath]: metadataErrors.map(e => ({
|
1700
|
+
message: e.message,
|
1701
|
+
value: {
|
1702
|
+
filePath,
|
1703
|
+
patchId
|
1704
|
+
}
|
1705
|
+
}))
|
1706
|
+
};
|
1707
|
+
}
|
1708
|
+
if (!metadata) {
|
1709
|
+
return {
|
1710
|
+
[sourcePath]: [{
|
1711
|
+
message: "Unexpectedly got no metadata",
|
1712
|
+
value: {
|
1713
|
+
filePath
|
1714
|
+
}
|
1715
|
+
}]
|
1716
|
+
};
|
1717
|
+
}
|
1718
|
+
const metadataSourcePath = Internal.createValPathOfItem(sourcePath, "metadata");
|
1719
|
+
if (!metadataSourcePath) {
|
1720
|
+
throw new Error("Could not create metadata path");
|
1721
|
+
}
|
1722
|
+
const currentValueMetadata = value.metadata;
|
1723
|
+
if (!currentValueMetadata) {
|
1724
|
+
return {
|
1725
|
+
[metadataSourcePath]: [{
|
1726
|
+
message: "Missing metadata field: 'metadata'",
|
1727
|
+
value
|
1728
|
+
}]
|
1729
|
+
};
|
1730
|
+
}
|
1731
|
+
const fieldErrors = {};
|
1732
|
+
for (const field of getFieldsForType(type)) {
|
1733
|
+
const fieldMetadata = metadata[field];
|
1734
|
+
const fieldSourcePath = Internal.createValPathOfItem(metadataSourcePath, field);
|
1735
|
+
if (!fieldSourcePath) {
|
1736
|
+
throw new Error("Could not create field path");
|
1737
|
+
}
|
1738
|
+
if (!(field in currentValueMetadata)) {
|
1739
|
+
return {
|
1740
|
+
[fieldSourcePath]: [{
|
1741
|
+
message: `Missing metadata field: '${field}'`,
|
1742
|
+
value
|
1743
|
+
}]
|
1744
|
+
};
|
1745
|
+
}
|
1746
|
+
if (fieldMetadata !== currentValueMetadata[field]) {
|
1747
|
+
fieldErrors[fieldSourcePath] = [{
|
1748
|
+
message: `Metadata field '${field}' of value: ${JSON.stringify(currentValueMetadata[field])} does not match expected value: ${JSON.stringify(fieldMetadata)}`,
|
1749
|
+
value: {
|
1750
|
+
actual: currentValueMetadata[field],
|
1751
|
+
expected: fieldMetadata
|
1752
|
+
},
|
1753
|
+
fixes: ["image:replace-metadata"]
|
1754
|
+
}];
|
1755
|
+
}
|
1825
1756
|
}
|
1757
|
+
return fieldErrors;
|
1826
1758
|
};
|
1759
|
+
const allErrors = (await Promise.all(Object.entries(files).map(([sourcePathS, value]) => validateFileAtSourcePath(sourcePathS, value).then(res => {
|
1760
|
+
if (res) {
|
1761
|
+
return Object.entries(res);
|
1762
|
+
} else {
|
1763
|
+
return [];
|
1764
|
+
}
|
1765
|
+
})))).flat();
|
1766
|
+
return Object.fromEntries(allErrors);
|
1827
1767
|
}
|
1828
|
-
|
1829
|
-
|
1830
|
-
|
1831
|
-
|
1832
|
-
|
1833
|
-
|
1834
|
-
|
1768
|
+
|
1769
|
+
// #region prepareCommit
|
1770
|
+
async prepare(patchAnalysis) {
|
1771
|
+
const {
|
1772
|
+
patchesByModule,
|
1773
|
+
fileLastUpdatedByPatchId
|
1774
|
+
} = patchAnalysis;
|
1775
|
+
const applySourceFilePatches = async (path, patches) => {
|
1776
|
+
const sourceFileRes = await this.getSourceFile(path);
|
1777
|
+
const errors = [];
|
1778
|
+
if (sourceFileRes.error) {
|
1779
|
+
errors.push({
|
1780
|
+
message: sourceFileRes.error.message,
|
1781
|
+
filePath: path
|
1782
|
+
});
|
1783
|
+
return {
|
1784
|
+
path,
|
1785
|
+
errors,
|
1786
|
+
skippedPatches: patches.map(p => p.patchId)
|
1787
|
+
};
|
1788
|
+
}
|
1789
|
+
const sourceFile = sourceFileRes.data;
|
1790
|
+
let tsSourceFile = ts.createSourceFile("<val>", sourceFile, ts.ScriptTarget.ES2015);
|
1791
|
+
const appliedPatches = [];
|
1792
|
+
const triedPatches = [];
|
1793
|
+
for (const {
|
1794
|
+
patchId
|
1795
|
+
} of patches) {
|
1796
|
+
var _patchAnalysis$patche;
|
1797
|
+
const patch = (_patchAnalysis$patche = patchAnalysis.patches) === null || _patchAnalysis$patche === void 0 || (_patchAnalysis$patche = _patchAnalysis$patche[patchId]) === null || _patchAnalysis$patche === void 0 ? void 0 : _patchAnalysis$patche.patch;
|
1798
|
+
if (!patch) {
|
1799
|
+
errors.push({
|
1800
|
+
message: `Analysis required non-existing patch: ${patchId}`
|
1801
|
+
});
|
1802
|
+
break;
|
1803
|
+
}
|
1804
|
+
const sourceFileOps = patch.filter(op => op.op !== "file"); // file is not a valid source file op
|
1805
|
+
const patchRes = applyPatch(tsSourceFile, tsOps, sourceFileOps);
|
1806
|
+
if (result.isErr(patchRes)) {
|
1807
|
+
if (Array.isArray(patchRes.error)) {
|
1808
|
+
errors.push(...patchRes.error);
|
1809
|
+
} else {
|
1810
|
+
errors.push(patchRes.error);
|
1811
|
+
}
|
1812
|
+
triedPatches.push(patchId);
|
1813
|
+
break;
|
1814
|
+
}
|
1815
|
+
appliedPatches.push(patchId);
|
1816
|
+
tsSourceFile = patchRes.value;
|
1817
|
+
}
|
1818
|
+
if (errors.length === 0) {
|
1819
|
+
var _this$options;
|
1820
|
+
let sourceFileText = tsSourceFile.getText(tsSourceFile);
|
1821
|
+
if ((_this$options = this.options) !== null && _this$options !== void 0 && _this$options.formatter) {
|
1822
|
+
try {
|
1823
|
+
sourceFileText = this.options.formatter(sourceFileText, path);
|
1824
|
+
} catch (err) {
|
1825
|
+
errors.push({
|
1826
|
+
message: "Could not format source file: " + (err instanceof Error ? err.message : "Unknown error")
|
1827
|
+
});
|
1835
1828
|
}
|
1829
|
+
}
|
1830
|
+
return {
|
1831
|
+
path,
|
1832
|
+
appliedPatches,
|
1833
|
+
result: sourceFileText
|
1836
1834
|
};
|
1835
|
+
} else {
|
1836
|
+
const skippedPatches = patches.slice(appliedPatches.length + triedPatches.length).map(p => p.patchId);
|
1837
|
+
return {
|
1838
|
+
path,
|
1839
|
+
appliedPatches,
|
1840
|
+
triedPatches,
|
1841
|
+
skippedPatches,
|
1842
|
+
errors
|
1843
|
+
};
|
1844
|
+
}
|
1845
|
+
};
|
1846
|
+
const allResults = await Promise.all(Object.entries(patchesByModule).map(([path, patches]) => applySourceFilePatches(path, patches)));
|
1847
|
+
let hasErrors = false;
|
1848
|
+
const sourceFilePatchErrors = {};
|
1849
|
+
const appliedPatches = {};
|
1850
|
+
const triedPatches = {};
|
1851
|
+
const skippedPatches = {};
|
1852
|
+
const patchedSourceFiles = {};
|
1853
|
+
|
1854
|
+
//
|
1855
|
+
const globalAppliedPatches = [];
|
1856
|
+
for (const res of allResults) {
|
1857
|
+
if (res.errors) {
|
1858
|
+
hasErrors = true;
|
1859
|
+
sourceFilePatchErrors[res.path] = res.errors;
|
1860
|
+
appliedPatches[res.path] = res.appliedPatches ?? [];
|
1861
|
+
triedPatches[res.path] = res.triedPatches ?? [];
|
1862
|
+
skippedPatches[res.path] = res.skippedPatches ?? [];
|
1863
|
+
} else {
|
1864
|
+
patchedSourceFiles[res.path] = res.result;
|
1865
|
+
appliedPatches[res.path] = res.appliedPatches ?? [];
|
1866
|
+
}
|
1867
|
+
for (const patchId of res.appliedPatches ?? []) {
|
1868
|
+
globalAppliedPatches.push(patchId);
|
1837
1869
|
}
|
1838
|
-
modules[moduleId].patches.applied.push(patchId);
|
1839
1870
|
}
|
1840
|
-
|
1871
|
+
const patchedBinaryFilesDescriptors = {};
|
1872
|
+
const binaryFilePatchErrors = {};
|
1873
|
+
await Promise.all(Object.entries(fileLastUpdatedByPatchId).map(async ([filePath, patchId]) => {
|
1874
|
+
if (globalAppliedPatches.includes(patchId)) {
|
1875
|
+
// TODO: do we want to make sure the file is there? Then again, it should be rare that it happens (unless there's a Val bug) so it might be enough to fail later (at commit)
|
1876
|
+
// TODO: include sha256? This way we can make sure we pick the right file since theoretically there could be multiple files with the same path in the same patch
|
1877
|
+
// or is that the case? We are picking the latest file by path so, that should be enough?
|
1878
|
+
patchedBinaryFilesDescriptors[filePath] = {
|
1879
|
+
patchId
|
1880
|
+
};
|
1881
|
+
} else {
|
1882
|
+
hasErrors = true;
|
1883
|
+
binaryFilePatchErrors[filePath] = {
|
1884
|
+
message: "Patch not applied"
|
1885
|
+
};
|
1886
|
+
}
|
1887
|
+
}));
|
1888
|
+
const res = {
|
1889
|
+
hasErrors,
|
1890
|
+
sourceFilePatchErrors,
|
1891
|
+
binaryFilePatchErrors,
|
1892
|
+
patchedSourceFiles,
|
1893
|
+
patchedBinaryFilesDescriptors,
|
1894
|
+
appliedPatches,
|
1895
|
+
skippedPatches,
|
1896
|
+
triedPatches
|
1897
|
+
};
|
1898
|
+
return res;
|
1841
1899
|
}
|
1842
1900
|
|
1843
|
-
|
1844
|
-
|
1845
|
-
|
1846
|
-
|
1847
|
-
|
1848
|
-
|
1849
|
-
|
1850
|
-
|
1851
|
-
|
1852
|
-
|
1853
|
-
|
1854
|
-
|
1855
|
-
|
1856
|
-
}
|
1857
|
-
|
1858
|
-
|
1859
|
-
|
1860
|
-
|
1861
|
-
|
1862
|
-
|
1863
|
-
|
1864
|
-
|
1865
|
-
|
1901
|
+
// #region createPatch
|
1902
|
+
async createPatch(path, patch, authorId) {
|
1903
|
+
const {
|
1904
|
+
sources,
|
1905
|
+
schemas,
|
1906
|
+
moduleErrors
|
1907
|
+
} = await this.initTree();
|
1908
|
+
const source = sources[path];
|
1909
|
+
const schema = schemas[path];
|
1910
|
+
const moduleError = moduleErrors.find(e => e.path === path);
|
1911
|
+
if (moduleError) {
|
1912
|
+
return {
|
1913
|
+
error: {
|
1914
|
+
message: `Cannot patch. Module at path: '${path}' has fatal errors: ` + moduleErrors.map(m => `"${m.message}"`).join(" and ")
|
1915
|
+
}
|
1916
|
+
};
|
1917
|
+
}
|
1918
|
+
if (!source) {
|
1919
|
+
return {
|
1920
|
+
error: {
|
1921
|
+
message: `Cannot patch. Module source at path: '${path}' does not exist`
|
1922
|
+
}
|
1923
|
+
};
|
1924
|
+
}
|
1925
|
+
if (!schema) {
|
1926
|
+
return {
|
1927
|
+
error: {
|
1928
|
+
message: `Cannot patch. Module schema at path: '${path}' does not exist`
|
1929
|
+
}
|
1930
|
+
};
|
1931
|
+
}
|
1932
|
+
const sourceFileOps = [];
|
1933
|
+
const files = {};
|
1934
|
+
for (const op of patch) {
|
1935
|
+
if (op.op !== "file") {
|
1936
|
+
sourceFileOps.push(op);
|
1937
|
+
} else {
|
1938
|
+
const {
|
1939
|
+
value,
|
1940
|
+
filePath
|
1941
|
+
} = op;
|
1942
|
+
if (files[filePath]) {
|
1943
|
+
files[filePath] = {
|
1944
|
+
error: new PatchError("Cannot have multiple files with same path in same patch")
|
1945
|
+
};
|
1946
|
+
} else if (typeof value !== "string") {
|
1947
|
+
files[filePath] = {
|
1948
|
+
error: new PatchError("Value is not a string")
|
1949
|
+
};
|
1950
|
+
} else {
|
1951
|
+
const sha256 = Internal.getSHA256Hash(textEncoder$1.encode(value));
|
1952
|
+
files[filePath] = {
|
1953
|
+
value,
|
1954
|
+
sha256,
|
1955
|
+
path: op.path
|
1956
|
+
};
|
1957
|
+
sourceFileOps.push({
|
1958
|
+
op: "file",
|
1959
|
+
path: op.path,
|
1960
|
+
filePath,
|
1961
|
+
value: {
|
1962
|
+
sha256
|
1963
|
+
}
|
1964
|
+
});
|
1965
|
+
}
|
1866
1966
|
}
|
1867
|
-
controller.close();
|
1868
1967
|
}
|
1869
|
-
|
1870
|
-
|
1968
|
+
const saveRes = await this.saveSourceFilePatch(path, sourceFileOps, authorId);
|
1969
|
+
if (saveRes.error) {
|
1970
|
+
return {
|
1971
|
+
error: saveRes.error
|
1972
|
+
};
|
1973
|
+
}
|
1974
|
+
const patchId = saveRes.patchId;
|
1975
|
+
const saveFileRes = await Promise.all(Object.entries(files).map(async ([filePath, data]) => {
|
1976
|
+
if (data.error) {
|
1977
|
+
return {
|
1978
|
+
filePath,
|
1979
|
+
error: data.error
|
1980
|
+
};
|
1981
|
+
} else {
|
1982
|
+
var _lastRes;
|
1983
|
+
let type;
|
1984
|
+
const modulePath = Internal.patchPathToModulePath(data.path);
|
1985
|
+
try {
|
1986
|
+
const {
|
1987
|
+
schema: schemaAtPath
|
1988
|
+
} = Internal.resolvePath(modulePath, source, schema);
|
1989
|
+
type = schemaAtPath instanceof ImageSchema ? "image" : schemaAtPath instanceof FileSchema ? "file" : schema.serialize().type;
|
1990
|
+
} catch (e) {
|
1991
|
+
if (e instanceof Error) {
|
1992
|
+
return {
|
1993
|
+
filePath,
|
1994
|
+
error: new PatchError(`Could not resolve file type at: ${modulePath}. Error: ${e.message}`)
|
1995
|
+
};
|
1996
|
+
}
|
1997
|
+
return {
|
1998
|
+
filePath,
|
1999
|
+
error: new PatchError(`Could not resolve file type at: ${modulePath}. Unknown error.`)
|
2000
|
+
};
|
2001
|
+
}
|
2002
|
+
if (type !== "image" && type !== "file") {
|
2003
|
+
return {
|
2004
|
+
filePath,
|
2005
|
+
error: new PatchError("Unknown file type (resolved from schema): " + type)
|
2006
|
+
};
|
2007
|
+
}
|
2008
|
+
const mimeType = getMimeTypeFromBase64(data.value);
|
2009
|
+
if (!mimeType) {
|
2010
|
+
return {
|
2011
|
+
filePath,
|
2012
|
+
error: new PatchError("Could not get mimeType from base 64 encoded value. First chars were: " + data.value.slice(0, 20))
|
2013
|
+
};
|
2014
|
+
}
|
2015
|
+
const buffer = bufferFromDataUrl(data.value);
|
2016
|
+
if (!buffer) {
|
2017
|
+
return {
|
2018
|
+
filePath,
|
2019
|
+
error: new PatchError("Could not create buffer from base 64 encoded value")
|
2020
|
+
};
|
2021
|
+
}
|
2022
|
+
const metadataOps = createMetadataFromBuffer(type, mimeType, buffer);
|
2023
|
+
if (metadataOps.errors) {
|
2024
|
+
return {
|
2025
|
+
filePath,
|
2026
|
+
error: new PatchError(`Could not get metadata. Errors: ${metadataOps.errors.map(error => error.message).join(", ")}`)
|
2027
|
+
};
|
2028
|
+
}
|
2029
|
+
const MaxRetries = 3;
|
2030
|
+
let lastRes;
|
2031
|
+
for (let i = 0; i < MaxRetries; i++) {
|
2032
|
+
lastRes = await this.saveBase64EncodedBinaryFileFromPatch(filePath, patchId, data.value, type, metadataOps.metadata);
|
2033
|
+
if (!lastRes.error) {
|
2034
|
+
return {
|
2035
|
+
filePath
|
2036
|
+
};
|
2037
|
+
}
|
2038
|
+
}
|
2039
|
+
return {
|
2040
|
+
filePath,
|
2041
|
+
error: new PatchError(((_lastRes = lastRes) === null || _lastRes === void 0 || (_lastRes = _lastRes.error) === null || _lastRes === void 0 ? void 0 : _lastRes.message) || "Unexpectedly could not save patch file")
|
2042
|
+
};
|
2043
|
+
}
|
2044
|
+
}));
|
2045
|
+
return {
|
2046
|
+
patchId,
|
2047
|
+
files: saveFileRes,
|
2048
|
+
createdAt: new Date().toISOString()
|
2049
|
+
};
|
2050
|
+
}
|
2051
|
+
|
2052
|
+
// #region abstract ops
|
1871
2053
|
}
|
1872
|
-
|
1873
|
-
|
1874
|
-
|
1875
|
-
|
1876
|
-
sameSite: "lax"
|
2054
|
+
function isOnlyFileCheckValidationError(validationError) {
|
2055
|
+
var _validationError$fixe;
|
2056
|
+
if ((_validationError$fixe = validationError.fixes) !== null && _validationError$fixe !== void 0 && _validationError$fixe.every(f => f === "file:check-metadata" || f === "image:replace-metadata")) {
|
2057
|
+
return true;
|
1877
2058
|
}
|
1878
|
-
|
1879
|
-
|
1880
|
-
|
1881
|
-
|
1882
|
-
|
1883
|
-
|
1884
|
-
|
1885
|
-
|
2059
|
+
return false;
|
2060
|
+
}
|
2061
|
+
function isFileSource(value) {
|
2062
|
+
if (typeof value === "object" && value !== null && FILE_REF_PROP in value && VAL_EXTENSION in value && value[VAL_EXTENSION] === "file" && FILE_REF_PROP) {
|
2063
|
+
return true;
|
2064
|
+
}
|
2065
|
+
return false;
|
2066
|
+
}
|
2067
|
+
function getFieldsForType(type) {
|
2068
|
+
if (type === "file") {
|
2069
|
+
return ["sha256", "mimeType"];
|
2070
|
+
} else if (type === "image") {
|
2071
|
+
return ["sha256", "mimeType", "height", "width"];
|
2072
|
+
}
|
2073
|
+
throw new Error("Unknown type: " + type);
|
2074
|
+
}
|
2075
|
+
function createMetadataFromBuffer(type, mimeType, buffer) {
|
2076
|
+
const sha256 = getSha256(mimeType, buffer);
|
2077
|
+
const errors = [];
|
2078
|
+
let availableMetadata;
|
2079
|
+
if (type === "image") {
|
2080
|
+
const {
|
2081
|
+
width,
|
2082
|
+
height,
|
2083
|
+
type
|
2084
|
+
} = sizeOf(buffer);
|
2085
|
+
const normalizedType = type === "jpg" ? "jpeg" : type;
|
2086
|
+
if (type !== undefined && `image/${normalizedType}` !== mimeType) {
|
2087
|
+
return {
|
2088
|
+
errors: [{
|
2089
|
+
message: `Mime type does not match image type: ${mimeType} vs ${type}`
|
2090
|
+
}]
|
2091
|
+
};
|
2092
|
+
}
|
2093
|
+
availableMetadata = {
|
2094
|
+
sha256: sha256,
|
2095
|
+
mimeType,
|
2096
|
+
height,
|
2097
|
+
width
|
2098
|
+
};
|
2099
|
+
} else {
|
2100
|
+
availableMetadata = {
|
2101
|
+
sha256: sha256,
|
2102
|
+
mimeType
|
1886
2103
|
};
|
1887
2104
|
}
|
1888
|
-
|
1889
|
-
|
2105
|
+
const metadata = {};
|
2106
|
+
for (const field of getFieldsForType(type)) {
|
2107
|
+
const foundFieldData = field in availableMetadata ? availableMetadata[field] : null;
|
2108
|
+
if (foundFieldData !== undefined && foundFieldData !== null) {
|
2109
|
+
metadata[field] = foundFieldData;
|
2110
|
+
} else {
|
2111
|
+
errors.push({
|
2112
|
+
message: `Field not found: '${field}'`,
|
2113
|
+
field
|
2114
|
+
});
|
2115
|
+
}
|
1890
2116
|
}
|
1891
|
-
|
2117
|
+
if (errors.length > 0) {
|
2118
|
+
return {
|
2119
|
+
errors
|
2120
|
+
};
|
2121
|
+
}
|
2122
|
+
return {
|
2123
|
+
metadata
|
2124
|
+
};
|
1892
2125
|
}
|
1893
2126
|
const base64DataAttr = "data:";
|
1894
2127
|
function getMimeTypeFromBase64(content) {
|
@@ -1896,658 +2129,1184 @@ function getMimeTypeFromBase64(content) {
|
|
1896
2129
|
const base64Index = content.indexOf(";base64,");
|
1897
2130
|
if (dataIndex > -1 || base64Index > -1) {
|
1898
2131
|
const mimeType = content.slice(dataIndex + base64DataAttr.length, base64Index);
|
1899
|
-
|
2132
|
+
const normalizedMimeType = mimeType === "image/jpg" ? "image/jpeg" : mimeType;
|
2133
|
+
return normalizedMimeType;
|
1900
2134
|
}
|
1901
2135
|
return null;
|
1902
2136
|
}
|
1903
|
-
function bufferFromDataUrl(dataUrl
|
2137
|
+
function bufferFromDataUrl(dataUrl) {
|
1904
2138
|
let base64Data;
|
1905
|
-
|
1906
|
-
|
1907
|
-
|
1908
|
-
base64Data = dataUrl.slice(base64Index + ";base64,".length);
|
1909
|
-
}
|
1910
|
-
} else {
|
1911
|
-
const dataUrlEncodingHeader = `${base64DataAttr}${contentType};base64,`;
|
1912
|
-
if (dataUrl.slice(0, dataUrlEncodingHeader.length) === dataUrlEncodingHeader) {
|
1913
|
-
base64Data = dataUrl.slice(dataUrlEncodingHeader.length);
|
1914
|
-
}
|
2139
|
+
const base64Index = dataUrl.indexOf(";base64,");
|
2140
|
+
if (base64Index > -1) {
|
2141
|
+
base64Data = dataUrl.slice(base64Index + ";base64,".length);
|
1915
2142
|
}
|
1916
2143
|
if (base64Data) {
|
1917
2144
|
return Buffer.from(base64Data, "base64" // TODO: why does it not work with base64url?
|
1918
2145
|
);
|
1919
2146
|
}
|
1920
2147
|
}
|
1921
|
-
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
1922
|
-
const COMMON_MIME_TYPES = {
|
1923
|
-
aac: "audio/aac",
|
1924
|
-
abw: "application/x-abiword",
|
1925
|
-
arc: "application/x-freearc",
|
1926
|
-
avif: "image/avif",
|
1927
|
-
avi: "video/x-msvideo",
|
1928
|
-
azw: "application/vnd.amazon.ebook",
|
1929
|
-
bin: "application/octet-stream",
|
1930
|
-
bmp: "image/bmp",
|
1931
|
-
bz: "application/x-bzip",
|
1932
|
-
bz2: "application/x-bzip2",
|
1933
|
-
cda: "application/x-cdf",
|
1934
|
-
csh: "application/x-csh",
|
1935
|
-
css: "text/css",
|
1936
|
-
csv: "text/csv",
|
1937
|
-
doc: "application/msword",
|
1938
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
1939
|
-
eot: "application/vnd.ms-fontobject",
|
1940
|
-
epub: "application/epub+zip",
|
1941
|
-
gz: "application/gzip",
|
1942
|
-
gif: "image/gif",
|
1943
|
-
htm: "text/html",
|
1944
|
-
html: "text/html",
|
1945
|
-
ico: "image/vnd.microsoft.icon",
|
1946
|
-
ics: "text/calendar",
|
1947
|
-
jar: "application/java-archive",
|
1948
|
-
jpeg: "image/jpeg",
|
1949
|
-
jpg: "image/jpeg",
|
1950
|
-
js: "text/javascript",
|
1951
|
-
json: "application/json",
|
1952
|
-
jsonld: "application/ld+json",
|
1953
|
-
mid: "audio/midi",
|
1954
|
-
midi: "audio/midi",
|
1955
|
-
mjs: "text/javascript",
|
1956
|
-
mp3: "audio/mpeg",
|
1957
|
-
mp4: "video/mp4",
|
1958
|
-
mpeg: "video/mpeg",
|
1959
|
-
mpkg: "application/vnd.apple.installer+xml",
|
1960
|
-
odp: "application/vnd.oasis.opendocument.presentation",
|
1961
|
-
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
1962
|
-
odt: "application/vnd.oasis.opendocument.text",
|
1963
|
-
oga: "audio/ogg",
|
1964
|
-
ogv: "video/ogg",
|
1965
|
-
ogx: "application/ogg",
|
1966
|
-
opus: "audio/opus",
|
1967
|
-
otf: "font/otf",
|
1968
|
-
png: "image/png",
|
1969
|
-
pdf: "application/pdf",
|
1970
|
-
php: "application/x-httpd-php",
|
1971
|
-
ppt: "application/vnd.ms-powerpoint",
|
1972
|
-
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
1973
|
-
rar: "application/vnd.rar",
|
1974
|
-
rtf: "application/rtf",
|
1975
|
-
sh: "application/x-sh",
|
1976
|
-
svg: "image/svg+xml",
|
1977
|
-
tar: "application/x-tar",
|
1978
|
-
tif: "image/tiff",
|
1979
|
-
tiff: "image/tiff",
|
1980
|
-
ts: "video/mp2t",
|
1981
|
-
ttf: "font/ttf",
|
1982
|
-
txt: "text/plain",
|
1983
|
-
vsd: "application/vnd.visio",
|
1984
|
-
wav: "audio/wav",
|
1985
|
-
weba: "audio/webm",
|
1986
|
-
webm: "video/webm",
|
1987
|
-
webp: "image/webp",
|
1988
|
-
woff: "font/woff",
|
1989
|
-
woff2: "font/woff2",
|
1990
|
-
xhtml: "application/xhtml+xml",
|
1991
|
-
xls: "application/vnd.ms-excel",
|
1992
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
1993
|
-
xml: "application/xml",
|
1994
|
-
xul: "application/vnd.mozilla.xul+xml",
|
1995
|
-
zip: "application/zip",
|
1996
|
-
"3gp": "video/3gpp; audio/3gpp if it doesn't contain video",
|
1997
|
-
"3g2": "video/3gpp2; audio/3gpp2 if it doesn't contain video",
|
1998
|
-
"7z": "application/x-7z-compressed"
|
1999
|
-
};
|
2000
|
-
function guessMimeTypeFromPath(filePath) {
|
2001
|
-
const fileExt = filePath.split(".").pop();
|
2002
|
-
if (fileExt) {
|
2003
|
-
return COMMON_MIME_TYPES[fileExt.toLowerCase()] || null;
|
2004
|
-
}
|
2005
|
-
return null;
|
2006
|
-
}
|
2007
|
-
function isCachedPatchFileOp(op) {
|
2008
|
-
return !!(op.op === "file" && typeof op.filePath === "string" && op.value && typeof op.value === "object" && !Array.isArray(op.value) && "sha256" in op.value && typeof op.value.sha256 === "string");
|
2009
|
-
}
|
2010
|
-
async function debugTiming(id, fn) {
|
2011
|
-
if (process.env["VAL_DEBUG_TIMING"] === "true") {
|
2012
|
-
const start = Date.now();
|
2013
|
-
const r = await fn();
|
2014
|
-
console.log(`Timing: ${id} took: ${Date.now() - start}ms (${new Date().toISOString()})`);
|
2015
|
-
return r;
|
2016
|
-
} else {
|
2017
|
-
return fn();
|
2018
|
-
}
|
2019
|
-
}
|
2020
2148
|
|
2021
|
-
const
|
2022
|
-
|
2023
|
-
|
2024
|
-
|
2025
|
-
|
2026
|
-
|
2027
|
-
|
2028
|
-
|
2029
|
-
|
2030
|
-
|
2149
|
+
const JSONValueT = z$1.lazy(() => z$1.union([z$1.string(), z$1.number(), z$1.boolean(), z$1.null(), z$1.array(JSONValueT), z$1.record(JSONValueT)]));
|
2150
|
+
|
2151
|
+
/**
|
2152
|
+
* Raw JSON patch operation.
|
2153
|
+
*/
|
2154
|
+
const OperationJSONT = z$1.discriminatedUnion("op", [z$1.object({
|
2155
|
+
op: z$1.literal("add"),
|
2156
|
+
path: z$1.string(),
|
2157
|
+
value: JSONValueT
|
2158
|
+
}).strict(), z$1.object({
|
2159
|
+
op: z$1.literal("remove"),
|
2160
|
+
/**
|
2161
|
+
* Must be non-root
|
2162
|
+
*/
|
2163
|
+
path: z$1.string()
|
2164
|
+
}).strict(), z$1.object({
|
2165
|
+
op: z$1.literal("replace"),
|
2166
|
+
path: z$1.string(),
|
2167
|
+
value: JSONValueT
|
2168
|
+
}).strict(), z$1.object({
|
2169
|
+
op: z$1.literal("move"),
|
2170
|
+
/**
|
2171
|
+
* Must be non-root and not a proper prefix of "path".
|
2172
|
+
*/
|
2173
|
+
from: z$1.string(),
|
2174
|
+
path: z$1.string()
|
2175
|
+
}).strict(), z$1.object({
|
2176
|
+
op: z$1.literal("copy"),
|
2177
|
+
from: z$1.string(),
|
2178
|
+
path: z$1.string()
|
2179
|
+
}).strict(), z$1.object({
|
2180
|
+
op: z$1.literal("test"),
|
2181
|
+
path: z$1.string(),
|
2182
|
+
value: JSONValueT
|
2183
|
+
}).strict(), z$1.object({
|
2184
|
+
op: z$1.literal("file"),
|
2185
|
+
path: z$1.string(),
|
2186
|
+
filePath: z$1.string(),
|
2187
|
+
value: z$1.string()
|
2188
|
+
}).strict()]);
|
2189
|
+
const PatchJSON = z$1.array(OperationJSONT);
|
2190
|
+
/**
|
2191
|
+
* Raw JSON patch operation.
|
2192
|
+
*/
|
2193
|
+
const OperationT = z$1.discriminatedUnion("op", [z$1.object({
|
2194
|
+
op: z$1.literal("add"),
|
2195
|
+
path: z$1.array(z$1.string()),
|
2196
|
+
value: JSONValueT
|
2197
|
+
}).strict(), z$1.object({
|
2198
|
+
op: z$1.literal("remove"),
|
2199
|
+
path: z$1.array(z$1.string()).nonempty()
|
2200
|
+
}).strict(), z$1.object({
|
2201
|
+
op: z$1.literal("replace"),
|
2202
|
+
path: z$1.array(z$1.string()),
|
2203
|
+
value: JSONValueT
|
2204
|
+
}).strict(), z$1.object({
|
2205
|
+
op: z$1.literal("move"),
|
2206
|
+
from: z$1.array(z$1.string()).nonempty(),
|
2207
|
+
path: z$1.array(z$1.string())
|
2208
|
+
}).strict(), z$1.object({
|
2209
|
+
op: z$1.literal("copy"),
|
2210
|
+
from: z$1.array(z$1.string()),
|
2211
|
+
path: z$1.array(z$1.string())
|
2212
|
+
}).strict(), z$1.object({
|
2213
|
+
op: z$1.literal("test"),
|
2214
|
+
path: z$1.array(z$1.string()),
|
2215
|
+
value: JSONValueT
|
2216
|
+
}).strict(), z$1.object({
|
2217
|
+
op: z$1.literal("file"),
|
2218
|
+
path: z$1.array(z$1.string()),
|
2219
|
+
filePath: z$1.string(),
|
2220
|
+
value: z$1.union([z$1.string(), z$1.object({
|
2221
|
+
sha256: z$1.string()
|
2222
|
+
})])
|
2223
|
+
}).strict()]);
|
2224
|
+
const Patch = z$1.array(OperationT);
|
2225
|
+
|
2226
|
+
class ValOpsFS extends ValOps {
|
2227
|
+
static VAL_DIR = ".val";
|
2228
|
+
constructor(rootDir, valModules, options) {
|
2229
|
+
super(valModules, options);
|
2230
|
+
this.rootDir = rootDir;
|
2231
|
+
this.host = new FSOpsHost();
|
2232
|
+
}
|
2233
|
+
async onInit() {
|
2234
|
+
// do nothing
|
2031
2235
|
}
|
2032
|
-
async
|
2033
|
-
|
2034
|
-
|
2035
|
-
|
2036
|
-
|
2037
|
-
|
2236
|
+
async readPatches(includes) {
|
2237
|
+
const patchesCacheDir = this.getPatchesDir();
|
2238
|
+
let patchJsonFiles = [];
|
2239
|
+
if (!this.host.directoryExists || this.host.directoryExists && this.host.directoryExists(patchesCacheDir)) {
|
2240
|
+
patchJsonFiles = this.host.readDirectory(patchesCacheDir, ["patch.json"], [], []);
|
2241
|
+
}
|
2242
|
+
const patches = {};
|
2243
|
+
const errors = {};
|
2244
|
+
const sortedPatchIds = patchJsonFiles.map(file => parseInt(fsPath__default.basename(fsPath__default.dirname(file)), 10)).sort();
|
2245
|
+
for (const patchIdNum of sortedPatchIds) {
|
2246
|
+
if (Number.isNaN(patchIdNum)) {
|
2247
|
+
throw new Error("Could not parse patch id from file name. Files found: " + patchJsonFiles.join(", "));
|
2038
2248
|
}
|
2039
|
-
|
2040
|
-
|
2041
|
-
async deletePatches(query) {
|
2042
|
-
const deletedPatches = [];
|
2043
|
-
for (const patchId of query.id ?? []) {
|
2044
|
-
const rawPatchFileContent = this.host.readFile(this.getPatchFilePath(patchId));
|
2045
|
-
if (!rawPatchFileContent) {
|
2046
|
-
console.warn("Val: Patch not found", patchId);
|
2249
|
+
const patchId = patchIdNum.toString();
|
2250
|
+
if (includes && !includes.includes(patchId)) {
|
2047
2251
|
continue;
|
2048
2252
|
}
|
2049
|
-
const
|
2050
|
-
|
2051
|
-
|
2253
|
+
const parsedFSPatchRes = this.parseJsonFile(this.getPatchFilePath(patchId), FSPatch);
|
2254
|
+
let parsedFSPatchBaseRes = undefined;
|
2255
|
+
if (this.host.fileExists(this.getPatchBaseFile(patchId))) {
|
2256
|
+
parsedFSPatchBaseRes = this.parseJsonFile(this.getPatchBaseFile(patchId), FSPatchBase);
|
2257
|
+
}
|
2258
|
+
if (parsedFSPatchRes.error) {
|
2259
|
+
errors[patchId] = parsedFSPatchRes.error;
|
2260
|
+
} else if (parsedFSPatchBaseRes && parsedFSPatchBaseRes.error) {
|
2261
|
+
errors[patchId] = parsedFSPatchBaseRes.error;
|
2262
|
+
} else {
|
2263
|
+
patches[patchId] = {
|
2264
|
+
...parsedFSPatchRes.data,
|
2265
|
+
appliedAt: parsedFSPatchBaseRes ? parsedFSPatchBaseRes.data : null
|
2266
|
+
};
|
2267
|
+
}
|
2268
|
+
}
|
2269
|
+
if (Object.keys(errors).length > 0) {
|
2270
|
+
return {
|
2271
|
+
patches,
|
2272
|
+
errors
|
2273
|
+
};
|
2274
|
+
}
|
2275
|
+
return {
|
2276
|
+
patches
|
2277
|
+
};
|
2278
|
+
}
|
2279
|
+
async getPatchOpsById(patchIds) {
|
2280
|
+
return this.readPatches(patchIds);
|
2281
|
+
}
|
2282
|
+
async findPatches(filters) {
|
2283
|
+
const patches = {};
|
2284
|
+
const errors = {};
|
2285
|
+
const {
|
2286
|
+
errors: allErrors,
|
2287
|
+
patches: allPatches
|
2288
|
+
} = await this.readPatches();
|
2289
|
+
for (const [patchIdS, patch] of Object.entries(allPatches)) {
|
2290
|
+
const patchId = patchIdS;
|
2291
|
+
if (filters.authors && !(patch.authorId === null || filters.authors.includes(patch.authorId))) {
|
2052
2292
|
continue;
|
2053
2293
|
}
|
2054
|
-
|
2055
|
-
|
2056
|
-
|
2057
|
-
|
2058
|
-
|
2059
|
-
|
2060
|
-
|
2294
|
+
patches[patchId] = {
|
2295
|
+
path: patch.path,
|
2296
|
+
createdAt: patch.createdAt,
|
2297
|
+
authorId: patch.authorId,
|
2298
|
+
appliedAt: patch.appliedAt
|
2299
|
+
};
|
2300
|
+
const error = allErrors && allErrors[patchId];
|
2301
|
+
if (error) {
|
2302
|
+
errors[patchId] = error;
|
2061
2303
|
}
|
2062
|
-
|
2063
|
-
|
2304
|
+
}
|
2305
|
+
if (errors && Object.keys(errors).length > 0) {
|
2306
|
+
return {
|
2307
|
+
patches,
|
2308
|
+
errors
|
2309
|
+
};
|
2064
2310
|
}
|
2065
2311
|
return {
|
2066
|
-
|
2067
|
-
json: deletedPatches
|
2312
|
+
patches
|
2068
2313
|
};
|
2069
2314
|
}
|
2070
|
-
|
2071
|
-
|
2072
|
-
|
2315
|
+
|
2316
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2317
|
+
parseJsonFile(filePath, parser) {
|
2318
|
+
if (!this.host.fileExists(filePath)) {
|
2073
2319
|
return {
|
2074
|
-
|
2075
|
-
|
2076
|
-
|
2077
|
-
|
2320
|
+
error: {
|
2321
|
+
message: `File not found: ${filePath}`,
|
2322
|
+
filePath
|
2323
|
+
}
|
2324
|
+
};
|
2325
|
+
}
|
2326
|
+
const data = this.host.readUtf8File(filePath);
|
2327
|
+
if (!data) {
|
2328
|
+
return {
|
2329
|
+
error: {
|
2330
|
+
message: `File is empty: ${filePath}`,
|
2331
|
+
filePath
|
2332
|
+
}
|
2333
|
+
};
|
2334
|
+
}
|
2335
|
+
let jsonData;
|
2336
|
+
try {
|
2337
|
+
jsonData = JSON.parse(data);
|
2338
|
+
} catch (err) {
|
2339
|
+
if (typeof err === "object" && err && "message" in err && typeof err.message === "string") {
|
2340
|
+
return {
|
2341
|
+
error: {
|
2342
|
+
message: `Could not parse JSON of file: ${filePath}. Message: ${err.message}`,
|
2343
|
+
filePath
|
2344
|
+
}
|
2345
|
+
};
|
2346
|
+
}
|
2347
|
+
return {
|
2348
|
+
error: {
|
2349
|
+
message: "Unknown error",
|
2350
|
+
filePath
|
2078
2351
|
}
|
2079
2352
|
};
|
2080
2353
|
}
|
2354
|
+
if (!parser) {
|
2355
|
+
return {
|
2356
|
+
data: jsonData
|
2357
|
+
};
|
2358
|
+
}
|
2359
|
+
try {
|
2360
|
+
const parsed = parser.safeParse(jsonData);
|
2361
|
+
if (!parsed.success) {
|
2362
|
+
return {
|
2363
|
+
error: {
|
2364
|
+
message: `Could not parse file: ${filePath}. Details: ${JSON.stringify(fromError(parsed.error).toString())}`,
|
2365
|
+
details: parsed.error,
|
2366
|
+
filePath
|
2367
|
+
}
|
2368
|
+
};
|
2369
|
+
}
|
2370
|
+
return {
|
2371
|
+
data: parsed.data
|
2372
|
+
};
|
2373
|
+
} catch (err) {
|
2374
|
+
if (typeof err === "object" && err && "message" in err && typeof err.message === "string") {
|
2375
|
+
return {
|
2376
|
+
error: {
|
2377
|
+
message: `Could not parse JSON of file: ${filePath}. Message: ${err.message}`,
|
2378
|
+
filePath
|
2379
|
+
}
|
2380
|
+
};
|
2381
|
+
}
|
2382
|
+
return {
|
2383
|
+
error: {
|
2384
|
+
message: "Unknown error",
|
2385
|
+
filePath
|
2386
|
+
}
|
2387
|
+
};
|
2388
|
+
}
|
2389
|
+
}
|
2390
|
+
async saveSourceFilePatch(path, patch, authorId) {
|
2081
2391
|
let fileId = Date.now();
|
2082
|
-
|
2083
|
-
|
2084
|
-
|
2392
|
+
try {
|
2393
|
+
while (this.host.fileExists(this.getPatchFilePath(fileId.toString()))) {
|
2394
|
+
// ensure unique file / patch id
|
2395
|
+
fileId++;
|
2396
|
+
}
|
2397
|
+
const patchId = fileId.toString();
|
2398
|
+
const data = {
|
2399
|
+
patch,
|
2400
|
+
path,
|
2401
|
+
authorId,
|
2402
|
+
coreVersion: Internal.VERSION.core,
|
2403
|
+
createdAt: new Date().toISOString()
|
2404
|
+
};
|
2405
|
+
this.host.writeUf8File(this.getPatchFilePath(patchId), JSON.stringify(data));
|
2406
|
+
return {
|
2407
|
+
patchId
|
2408
|
+
};
|
2409
|
+
} catch (err) {
|
2410
|
+
if (err instanceof Error) {
|
2411
|
+
return {
|
2412
|
+
error: {
|
2413
|
+
message: err.message
|
2414
|
+
}
|
2415
|
+
};
|
2416
|
+
}
|
2417
|
+
return {
|
2418
|
+
error: {
|
2419
|
+
message: "Unknown error"
|
2420
|
+
}
|
2421
|
+
};
|
2085
2422
|
}
|
2086
|
-
|
2087
|
-
|
2088
|
-
const
|
2089
|
-
|
2090
|
-
|
2091
|
-
|
2092
|
-
|
2093
|
-
|
2094
|
-
|
2095
|
-
|
2096
|
-
|
2097
|
-
|
2098
|
-
|
2099
|
-
|
2100
|
-
|
2101
|
-
|
2102
|
-
|
2103
|
-
|
2104
|
-
|
2105
|
-
|
2106
|
-
|
2107
|
-
|
2108
|
-
|
2109
|
-
|
2423
|
+
}
|
2424
|
+
async getSourceFile(path) {
|
2425
|
+
const filePath = fsPath__default.join(this.rootDir, path);
|
2426
|
+
if (!this.host.fileExists(filePath)) {
|
2427
|
+
return {
|
2428
|
+
error: {
|
2429
|
+
message: `File not found: ${filePath}`
|
2430
|
+
}
|
2431
|
+
};
|
2432
|
+
}
|
2433
|
+
return {
|
2434
|
+
data: this.host.readUtf8File(filePath)
|
2435
|
+
};
|
2436
|
+
}
|
2437
|
+
async saveSourceFile(path, data) {
|
2438
|
+
const filePath = fsPath__default.join(this.rootDir, ...path.split("/"));
|
2439
|
+
try {
|
2440
|
+
this.host.writeUf8File(filePath, data);
|
2441
|
+
return {
|
2442
|
+
path
|
2443
|
+
};
|
2444
|
+
} catch (err) {
|
2445
|
+
if (err instanceof Error) {
|
2446
|
+
return {
|
2447
|
+
error: {
|
2448
|
+
message: err.message
|
2110
2449
|
}
|
2111
|
-
|
2112
|
-
|
2113
|
-
|
2114
|
-
|
2450
|
+
};
|
2451
|
+
}
|
2452
|
+
return {
|
2453
|
+
error: {
|
2454
|
+
message: "Unknown error"
|
2455
|
+
}
|
2456
|
+
};
|
2457
|
+
}
|
2458
|
+
}
|
2459
|
+
async saveBase64EncodedBinaryFileFromPatch(filePath, patchId, data, _type, metadata) {
|
2460
|
+
const patchFilePath = this.getBinaryFilePath(filePath, patchId);
|
2461
|
+
const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchId);
|
2462
|
+
try {
|
2463
|
+
const buffer = bufferFromDataUrl(data);
|
2464
|
+
if (!buffer) {
|
2465
|
+
return {
|
2466
|
+
error: {
|
2467
|
+
message: "Could not create buffer from data url. Not a data url? First chars were: " + data.slice(0, 20)
|
2115
2468
|
}
|
2116
|
-
|
2117
|
-
|
2118
|
-
|
2119
|
-
|
2120
|
-
|
2121
|
-
|
2122
|
-
|
2123
|
-
|
2124
|
-
|
2125
|
-
|
2126
|
-
|
2127
|
-
|
2128
|
-
|
2129
|
-
|
2130
|
-
|
2131
|
-
|
2132
|
-
|
2469
|
+
};
|
2470
|
+
}
|
2471
|
+
this.host.writeUf8File(metadataFilePath, JSON.stringify(metadata));
|
2472
|
+
this.host.writeBinaryFile(patchFilePath, buffer);
|
2473
|
+
return {
|
2474
|
+
patchId,
|
2475
|
+
filePath
|
2476
|
+
};
|
2477
|
+
} catch (err) {
|
2478
|
+
if (err instanceof Error) {
|
2479
|
+
return {
|
2480
|
+
error: {
|
2481
|
+
message: err.message
|
2482
|
+
}
|
2483
|
+
};
|
2484
|
+
}
|
2485
|
+
return {
|
2486
|
+
error: {
|
2487
|
+
message: "Unknown error"
|
2133
2488
|
}
|
2489
|
+
};
|
2490
|
+
}
|
2491
|
+
}
|
2492
|
+
async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
|
2493
|
+
const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchId);
|
2494
|
+
if (!this.host.fileExists(metadataFilePath)) {
|
2495
|
+
return {
|
2496
|
+
errors: [{
|
2497
|
+
message: "Metadata file not found",
|
2498
|
+
filePath
|
2499
|
+
}]
|
2500
|
+
};
|
2501
|
+
}
|
2502
|
+
const metadataParseRes = this.parseJsonFile(metadataFilePath, z.record(z.union([z.string(), z.number()])));
|
2503
|
+
if (metadataParseRes.error) {
|
2504
|
+
return {
|
2505
|
+
errors: [metadataParseRes.error]
|
2506
|
+
};
|
2507
|
+
}
|
2508
|
+
const parsed = metadataParseRes.data;
|
2509
|
+
const expectedFields = getFieldsForType(type);
|
2510
|
+
const fieldErrors = [];
|
2511
|
+
for (const field of expectedFields) {
|
2512
|
+
if (!(field in parsed)) {
|
2513
|
+
fieldErrors.push({
|
2514
|
+
message: `Expected fields for type: ${type}. Field not found: '${field}'`,
|
2515
|
+
field
|
2516
|
+
});
|
2134
2517
|
}
|
2135
2518
|
}
|
2136
|
-
|
2519
|
+
if (fieldErrors.length > 0) {
|
2520
|
+
return {
|
2521
|
+
errors: fieldErrors
|
2522
|
+
};
|
2523
|
+
}
|
2137
2524
|
return {
|
2138
|
-
|
2139
|
-
|
2525
|
+
metadata: parsed
|
2526
|
+
};
|
2527
|
+
}
|
2528
|
+
async getBase64EncodedBinaryFileFromPatch(filePath, patchId) {
|
2529
|
+
const absPath = this.getBinaryFilePath(filePath, patchId);
|
2530
|
+
if (!this.host.fileExists(absPath)) {
|
2531
|
+
return null;
|
2532
|
+
}
|
2533
|
+
return this.host.readBinaryFile(absPath);
|
2534
|
+
}
|
2535
|
+
async deletePatches(patchIds) {
|
2536
|
+
const deleted = [];
|
2537
|
+
let errors = null;
|
2538
|
+
for (const patchId of patchIds) {
|
2539
|
+
try {
|
2540
|
+
this.host.deleteDir(this.getPatchDir(patchId));
|
2541
|
+
deleted.push(patchId);
|
2542
|
+
} catch (err) {
|
2543
|
+
if (!errors) {
|
2544
|
+
errors = {};
|
2545
|
+
}
|
2546
|
+
errors[patchId] = {
|
2547
|
+
message: err instanceof Error ? err.message : "Unknown error"
|
2548
|
+
};
|
2549
|
+
}
|
2550
|
+
}
|
2551
|
+
if (errors) {
|
2552
|
+
return {
|
2553
|
+
deleted,
|
2554
|
+
errors
|
2555
|
+
};
|
2556
|
+
}
|
2557
|
+
return {
|
2558
|
+
deleted
|
2559
|
+
};
|
2560
|
+
}
|
2561
|
+
async saveFiles(preparedCommit) {
|
2562
|
+
const updatedFiles = [];
|
2563
|
+
const errors = {};
|
2564
|
+
for (const [filePath, data] of Object.entries(preparedCommit.patchedSourceFiles)) {
|
2565
|
+
const absPath = fsPath__default.join(this.rootDir, ...filePath.split("/"));
|
2566
|
+
try {
|
2567
|
+
this.host.writeUf8File(absPath, data);
|
2568
|
+
updatedFiles.push(absPath);
|
2569
|
+
} catch (err) {
|
2570
|
+
errors[absPath] = {
|
2571
|
+
message: err instanceof Error ? err.message : "Unknown error",
|
2572
|
+
filePath
|
2573
|
+
};
|
2574
|
+
}
|
2575
|
+
}
|
2576
|
+
for (const [filePath, {
|
2577
|
+
patchId
|
2578
|
+
}] of Object.entries(preparedCommit.patchedBinaryFilesDescriptors)) {
|
2579
|
+
const absPath = fsPath__default.join(this.rootDir, ...filePath.split("/"));
|
2580
|
+
try {
|
2581
|
+
this.host.copyFile(this.getBinaryFilePath(filePath, patchId), absPath);
|
2582
|
+
updatedFiles.push(absPath);
|
2583
|
+
} catch (err) {
|
2584
|
+
errors[absPath] = {
|
2585
|
+
message: err instanceof Error ? err.message : "Unknown error",
|
2586
|
+
filePath
|
2587
|
+
};
|
2588
|
+
}
|
2589
|
+
}
|
2590
|
+
for (const patchId of Object.values(preparedCommit.appliedPatches).flat()) {
|
2591
|
+
const appliedAt = {
|
2592
|
+
baseSha: await this.getBaseSha(),
|
2593
|
+
timestamp: new Date().toISOString()
|
2594
|
+
};
|
2595
|
+
const absPath = this.getPatchBaseFile(patchId);
|
2596
|
+
try {
|
2597
|
+
this.host.writeUf8File(absPath, JSON.stringify(appliedAt));
|
2598
|
+
} catch (err) {
|
2599
|
+
errors[absPath] = {
|
2600
|
+
message: err instanceof Error ? err.message : "Unknown error",
|
2601
|
+
filePath: absPath
|
2602
|
+
};
|
2603
|
+
}
|
2604
|
+
}
|
2605
|
+
return {
|
2606
|
+
updatedFiles,
|
2607
|
+
errors
|
2608
|
+
};
|
2609
|
+
}
|
2610
|
+
async getBinaryFile(filePath) {
|
2611
|
+
const absPath = fsPath__default.join(this.rootDir, ...filePath.split("/"));
|
2612
|
+
if (!this.host.fileExists(absPath)) {
|
2613
|
+
return null;
|
2614
|
+
}
|
2615
|
+
const buffer = this.host.readBinaryFile(absPath);
|
2616
|
+
return buffer;
|
2617
|
+
}
|
2618
|
+
async getBinaryFileMetadata(filePath, type) {
|
2619
|
+
const buffer = await this.getBinaryFile(filePath);
|
2620
|
+
if (!buffer) {
|
2621
|
+
return {
|
2622
|
+
errors: [{
|
2623
|
+
message: "File not found",
|
2624
|
+
filePath
|
2625
|
+
}]
|
2626
|
+
};
|
2627
|
+
}
|
2628
|
+
const mimeType = guessMimeTypeFromPath(filePath);
|
2629
|
+
if (!mimeType) {
|
2630
|
+
return {
|
2631
|
+
errors: [{
|
2632
|
+
message: `Could not guess mime type of file ext: ${fsPath__default.extname(filePath)}`,
|
2633
|
+
filePath
|
2634
|
+
}]
|
2635
|
+
};
|
2636
|
+
}
|
2637
|
+
return createMetadataFromBuffer(type, mimeType, buffer);
|
2638
|
+
}
|
2639
|
+
|
2640
|
+
// #region fs file path helpers
|
2641
|
+
getPatchesDir() {
|
2642
|
+
return fsPath__default.join(this.rootDir, ValOpsFS.VAL_DIR, "patches");
|
2643
|
+
}
|
2644
|
+
getPatchDir(patchId) {
|
2645
|
+
return fsPath__default.join(this.getPatchesDir(), patchId);
|
2646
|
+
}
|
2647
|
+
getBinaryFilePath(filename, patchId) {
|
2648
|
+
return fsPath__default.join(this.getPatchDir(patchId), "files", filename, fsPath__default.basename(filename));
|
2649
|
+
}
|
2650
|
+
getBinaryFileMetadataPath(filename, patchId) {
|
2651
|
+
return fsPath__default.join(this.getPatchDir(patchId), "files", filename, "metadata.json");
|
2652
|
+
}
|
2653
|
+
getPatchFilePath(patchId) {
|
2654
|
+
return fsPath__default.join(this.getPatchDir(patchId), "patch.json");
|
2655
|
+
}
|
2656
|
+
getPatchBaseFile(patchId) {
|
2657
|
+
return fsPath__default.join(this.getPatchDir(patchId), "base.json");
|
2658
|
+
}
|
2659
|
+
}
|
2660
|
+
class FSOpsHost {
|
2661
|
+
constructor() {}
|
2662
|
+
|
2663
|
+
// TODO: do we want async operations here?
|
2664
|
+
deleteDir(dir) {
|
2665
|
+
if (this.directoryExists(dir)) {
|
2666
|
+
fs.rmdirSync(dir, {
|
2667
|
+
recursive: true
|
2668
|
+
});
|
2669
|
+
}
|
2670
|
+
}
|
2671
|
+
directoryExists(path) {
|
2672
|
+
return ts.sys.directoryExists(path);
|
2673
|
+
}
|
2674
|
+
readDirectory(path, extensions, exclude, include) {
|
2675
|
+
return ts.sys.readDirectory(path, extensions, exclude, include);
|
2676
|
+
}
|
2677
|
+
fileExists(path) {
|
2678
|
+
return ts.sys.fileExists(path);
|
2679
|
+
}
|
2680
|
+
readBinaryFile(path) {
|
2681
|
+
return fs.readFileSync(path);
|
2682
|
+
}
|
2683
|
+
readUtf8File(path) {
|
2684
|
+
return fs.readFileSync(path, "utf-8");
|
2685
|
+
}
|
2686
|
+
writeUf8File(path, data) {
|
2687
|
+
fs.mkdirSync(fsPath__default.dirname(path), {
|
2688
|
+
recursive: true
|
2689
|
+
});
|
2690
|
+
fs.writeFileSync(path, data, "utf-8");
|
2691
|
+
}
|
2692
|
+
writeBinaryFile(path, data) {
|
2693
|
+
fs.mkdirSync(fsPath__default.dirname(path), {
|
2694
|
+
recursive: true
|
2695
|
+
});
|
2696
|
+
fs.writeFileSync(path, data, "base64url");
|
2697
|
+
}
|
2698
|
+
copyFile(from, to) {
|
2699
|
+
fs.mkdirSync(fsPath__default.dirname(to), {
|
2700
|
+
recursive: true
|
2701
|
+
});
|
2702
|
+
fs.copyFileSync(from, to);
|
2703
|
+
}
|
2704
|
+
}
|
2705
|
+
const FSPatch = z.object({
|
2706
|
+
path: z.string().refine(p => p.startsWith("/") && p.includes(".val."), "Path is not valid. Must start with '/' and include '.val.'"),
|
2707
|
+
patch: Patch,
|
2708
|
+
authorId: z.string().refine(p => true).nullable(),
|
2709
|
+
createdAt: z.string().datetime(),
|
2710
|
+
coreVersion: z.string().nullable() // TODO: use this to check if patch is compatible with current core version?
|
2711
|
+
});
|
2712
|
+
const FSPatchBase = z.object({
|
2713
|
+
baseSha: z.string().refine(p => true),
|
2714
|
+
timestamp: z.string().datetime()
|
2715
|
+
});
|
2716
|
+
|
2717
|
+
const textEncoder = new TextEncoder();
|
2718
|
+
const PatchId = z.string().refine(s => !!s); // TODO: validate
|
2719
|
+
const CommitSha = z.string().refine(s => !!s); // TODO: validate
|
2720
|
+
const BaseSha = z.string().refine(s => !!s); // TODO: validate
|
2721
|
+
const AuthorId = z.string().refine(s => !!s); // TODO: validate
|
2722
|
+
const ModuleFilePath = z.string().refine(s => !!s); // TODO: validate
|
2723
|
+
const Metadata = z.union([z.object({
|
2724
|
+
sha256: z.string(),
|
2725
|
+
mimeType: z.string(),
|
2726
|
+
width: z.number(),
|
2727
|
+
height: z.number()
|
2728
|
+
}), z.object({
|
2729
|
+
sha256: z.string(),
|
2730
|
+
mimeType: z.string()
|
2731
|
+
})]);
|
2732
|
+
const MetadataRes = z.object({
|
2733
|
+
filePath: ModuleFilePath,
|
2734
|
+
metadata: Metadata,
|
2735
|
+
type: z.union([z.literal("file"), z.literal("image")]).nullable()
|
2736
|
+
});
|
2737
|
+
const BasePatchResponse = z.object({
|
2738
|
+
path: ModuleFilePath,
|
2739
|
+
patchId: PatchId,
|
2740
|
+
authorId: AuthorId.nullable(),
|
2741
|
+
createdAt: z.string().datetime(),
|
2742
|
+
applied: z.object({
|
2743
|
+
baseSha: BaseSha,
|
2744
|
+
commitSha: CommitSha,
|
2745
|
+
appliedAt: z.string().datetime()
|
2746
|
+
}).nullable()
|
2747
|
+
});
|
2748
|
+
const GetPatches = z.object({
|
2749
|
+
patches: z.array(z.intersection(z.object({
|
2750
|
+
patch: Patch
|
2751
|
+
}), BasePatchResponse)),
|
2752
|
+
errors: z.array(z.object({
|
2753
|
+
patchId: PatchId.optional(),
|
2754
|
+
message: z.string()
|
2755
|
+
})).optional()
|
2756
|
+
});
|
2757
|
+
const SearchPatches = z.object({
|
2758
|
+
patches: z.array(BasePatchResponse)
|
2759
|
+
});
|
2760
|
+
const FilesResponse = z.object({
|
2761
|
+
files: z.array(z.union([z.object({
|
2762
|
+
filePath: z.string(),
|
2763
|
+
location: z.literal("patch"),
|
2764
|
+
patchId: PatchId,
|
2765
|
+
value: z.string()
|
2766
|
+
}), z.object({
|
2767
|
+
filePath: z.string(),
|
2768
|
+
location: z.literal("repo"),
|
2769
|
+
commitSha: CommitSha,
|
2770
|
+
value: z.string()
|
2771
|
+
})])),
|
2772
|
+
errors: z.array(z.union([z.object({
|
2773
|
+
filePath: z.string(),
|
2774
|
+
location: z.literal("patch"),
|
2775
|
+
patchId: PatchId,
|
2776
|
+
message: z.string()
|
2777
|
+
}), z.object({
|
2778
|
+
filePath: z.string(),
|
2779
|
+
location: z.literal("repo"),
|
2780
|
+
commitSha: CommitSha,
|
2781
|
+
message: z.string()
|
2782
|
+
})])).optional()
|
2783
|
+
});
|
2784
|
+
const SavePatchResponse = z.object({
|
2785
|
+
patchId: PatchId
|
2786
|
+
});
|
2787
|
+
const DeletePatchesResponse = z.object({
|
2788
|
+
deleted: z.array(PatchId),
|
2789
|
+
errors: z.array(z.object({
|
2790
|
+
message: z.string(),
|
2791
|
+
patchId: PatchId
|
2792
|
+
})).optional()
|
2793
|
+
});
|
2794
|
+
const SavePatchFileResponse = z.object({
|
2795
|
+
patchId: PatchId,
|
2796
|
+
filePath: ModuleFilePath
|
2797
|
+
});
|
2798
|
+
const CommitResponse = z.object({
|
2799
|
+
updatedFiles: z.array(z.string()),
|
2800
|
+
commit: CommitSha,
|
2801
|
+
branch: z.string()
|
2802
|
+
});
|
2803
|
+
class ValOpsHttp extends ValOps {
|
2804
|
+
constructor(hostUrl, project, commitSha, branch, apiKey, valModules, options) {
|
2805
|
+
super(valModules, options);
|
2806
|
+
this.hostUrl = hostUrl;
|
2807
|
+
this.project = project;
|
2808
|
+
this.commitSha = commitSha;
|
2809
|
+
this.branch = branch;
|
2810
|
+
this.authHeaders = {
|
2811
|
+
Authorization: `Bearer ${apiKey}`
|
2140
2812
|
};
|
2813
|
+
this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
|
2141
2814
|
}
|
2142
|
-
async
|
2143
|
-
|
2815
|
+
async onInit() {
|
2816
|
+
// TODO: unused for now. Implement or remove
|
2144
2817
|
}
|
2145
|
-
async
|
2146
|
-
|
2147
|
-
|
2148
|
-
|
2149
|
-
|
2150
|
-
|
2151
|
-
|
2152
|
-
|
2153
|
-
|
2154
|
-
|
2155
|
-
|
2818
|
+
async getPatchOpsById(patchIds) {
|
2819
|
+
const params = new URLSearchParams();
|
2820
|
+
params.set("branch", this.branch);
|
2821
|
+
if (patchIds.length > 0) {
|
2822
|
+
params.set("patch_ids", encodeURIComponent(patchIds.join(",")));
|
2823
|
+
}
|
2824
|
+
return fetch(`${this.hostUrl}/v1/${this.project}/patches${params.size > 0 ? "?" + params : ""}`, {
|
2825
|
+
headers: {
|
2826
|
+
...this.authHeaders,
|
2827
|
+
"Content-Type": "application/json"
|
2828
|
+
}
|
2829
|
+
}).then(async res => {
|
2830
|
+
const patches = {};
|
2831
|
+
if (res.ok) {
|
2832
|
+
const json = await res.json();
|
2833
|
+
const parsed = GetPatches.safeParse(json);
|
2834
|
+
if (parsed.success) {
|
2835
|
+
const data = parsed.data;
|
2836
|
+
const errors = {};
|
2837
|
+
for (const patchesRes of data.patches) {
|
2838
|
+
patches[patchesRes.patchId] = {
|
2839
|
+
path: patchesRes.path,
|
2840
|
+
authorId: patchesRes.authorId,
|
2841
|
+
createdAt: patchesRes.createdAt,
|
2842
|
+
appliedAt: patchesRes.applied && {
|
2843
|
+
baseSha: patchesRes.applied.baseSha,
|
2844
|
+
timestamp: patchesRes.applied.appliedAt,
|
2845
|
+
git: {
|
2846
|
+
commitSha: patchesRes.applied.commitSha
|
2847
|
+
}
|
2848
|
+
},
|
2849
|
+
patch: patchesRes.patch
|
2850
|
+
};
|
2851
|
+
}
|
2852
|
+
return {
|
2853
|
+
patches,
|
2854
|
+
errors
|
2855
|
+
};
|
2156
2856
|
}
|
2157
|
-
const metadata = JSON.parse(metadataFileContent);
|
2158
2857
|
return {
|
2159
|
-
|
2160
|
-
|
2161
|
-
|
2162
|
-
|
2163
|
-
"Cache-Control": "public, max-age=31536000, immutable"
|
2164
|
-
},
|
2165
|
-
body: bufferToReadableStream(fileContent)
|
2858
|
+
patches,
|
2859
|
+
error: {
|
2860
|
+
message: `Could not parse get patches response. Error: ${fromError(parsed.error)}`
|
2861
|
+
}
|
2166
2862
|
};
|
2167
2863
|
}
|
2168
|
-
}
|
2169
|
-
const buffer = await this.readStaticBinaryFile(path__default.join(this.cwd, filePath));
|
2170
|
-
const mimeType = guessMimeTypeFromPath(filePath) || "application/octet-stream";
|
2171
|
-
if (!buffer) {
|
2172
2864
|
return {
|
2173
|
-
|
2174
|
-
|
2175
|
-
message: "
|
2865
|
+
patches,
|
2866
|
+
error: {
|
2867
|
+
message: "Could not get patches. HTTP error: " + res.status + " " + res.statusText
|
2176
2868
|
}
|
2177
2869
|
};
|
2870
|
+
});
|
2871
|
+
}
|
2872
|
+
async findPatches(filters) {
|
2873
|
+
const params = new URLSearchParams();
|
2874
|
+
params.set("branch", this.branch);
|
2875
|
+
if (filters.authors && filters.authors.length > 0) {
|
2876
|
+
params.set("author_ids", encodeURIComponent(filters.authors.join(",")));
|
2178
2877
|
}
|
2179
|
-
|
2180
|
-
|
2181
|
-
|
2878
|
+
return fetch(`${this.hostUrl}/v1/${this.project}/search/patches${params.size > 0 ? "?" + params : ""}`, {
|
2879
|
+
headers: {
|
2880
|
+
...this.authHeaders,
|
2881
|
+
"Content-Type": "application/json"
|
2882
|
+
}
|
2883
|
+
}).then(async res => {
|
2884
|
+
const patches = {};
|
2885
|
+
if (res.ok) {
|
2886
|
+
const parsed = SearchPatches.safeParse(await res.json());
|
2887
|
+
if (parsed.success) {
|
2888
|
+
for (const patchesRes of parsed.data.patches) {
|
2889
|
+
patches[patchesRes.patchId] = {
|
2890
|
+
path: patchesRes.path,
|
2891
|
+
authorId: patchesRes.authorId,
|
2892
|
+
createdAt: patchesRes.createdAt,
|
2893
|
+
appliedAt: patchesRes.applied && {
|
2894
|
+
baseSha: patchesRes.applied.baseSha,
|
2895
|
+
timestamp: patchesRes.applied.appliedAt,
|
2896
|
+
git: {
|
2897
|
+
commitSha: patchesRes.applied.commitSha
|
2898
|
+
}
|
2899
|
+
}
|
2900
|
+
};
|
2901
|
+
}
|
2902
|
+
return {
|
2903
|
+
patches
|
2904
|
+
};
|
2905
|
+
}
|
2182
2906
|
return {
|
2183
|
-
|
2184
|
-
|
2185
|
-
|
2186
|
-
|
2187
|
-
"Cache-Control": "public, max-age=31536000, immutable"
|
2188
|
-
},
|
2189
|
-
body: bufferToReadableStream(buffer)
|
2907
|
+
patches,
|
2908
|
+
error: {
|
2909
|
+
message: `Could not parse search patches response. Error: ${fromError(parsed.error)}`
|
2910
|
+
}
|
2190
2911
|
};
|
2191
2912
|
}
|
2192
|
-
|
2193
|
-
|
2194
|
-
|
2913
|
+
return {
|
2914
|
+
patches,
|
2915
|
+
error: {
|
2916
|
+
message: "Could not find patches. HTTP error: " + res.status + " " + res.statusText
|
2917
|
+
}
|
2918
|
+
};
|
2919
|
+
});
|
2920
|
+
}
|
2921
|
+
async saveSourceFilePatch(path, patch, authorId) {
|
2922
|
+
return fetch(`${this.hostUrl}/v1/${this.project}/patches`, {
|
2923
|
+
method: "POST",
|
2195
2924
|
headers: {
|
2196
|
-
|
2197
|
-
"Content-
|
2925
|
+
...this.authHeaders,
|
2926
|
+
"Content-Type": "application/json"
|
2198
2927
|
},
|
2199
|
-
body:
|
2200
|
-
|
2201
|
-
|
2202
|
-
|
2203
|
-
|
2204
|
-
|
2205
|
-
|
2206
|
-
|
2207
|
-
|
2208
|
-
|
2209
|
-
|
2210
|
-
|
2211
|
-
}
|
2212
|
-
const res = {};
|
2213
|
-
const sortedPatchIds = files.map(file => parseInt(path__default.basename(file), 10)).sort();
|
2214
|
-
for (const patchIdStr of sortedPatchIds) {
|
2215
|
-
const patchId = patchIdStr.toString();
|
2216
|
-
if (query.id && query.id.length > 0 && !query.id.includes(patchId)) {
|
2217
|
-
continue;
|
2218
|
-
}
|
2219
|
-
try {
|
2220
|
-
const currentParsedPatches = z$1.record(Patch).safeParse(JSON.parse(this.host.readFile(path__default.join(this.patchesRootPath, LocalValServer.PATCHES_DIR, `${patchId}`)) || ""));
|
2221
|
-
if (!currentParsedPatches.success) {
|
2222
|
-
const msg = "Unexpected error reading patch. Patch did not parse correctly. Is there a mismatch in Val versions? Perhaps Val is misconfigured?";
|
2223
|
-
console.error(`Val: ${msg}`, {
|
2224
|
-
patchId,
|
2225
|
-
error: currentParsedPatches.error
|
2226
|
-
});
|
2928
|
+
body: JSON.stringify({
|
2929
|
+
path,
|
2930
|
+
patch,
|
2931
|
+
authorId,
|
2932
|
+
commit: this.commitSha,
|
2933
|
+
branch: this.branch,
|
2934
|
+
coreVersion: Internal.VERSION.core
|
2935
|
+
})
|
2936
|
+
}).then(async res => {
|
2937
|
+
if (res.ok) {
|
2938
|
+
const parsed = SavePatchResponse.safeParse(await res.json());
|
2939
|
+
if (parsed.success) {
|
2227
2940
|
return {
|
2228
|
-
|
2229
|
-
json: {
|
2230
|
-
message: msg,
|
2231
|
-
details: {
|
2232
|
-
patchId,
|
2233
|
-
error: currentParsedPatches.error
|
2234
|
-
}
|
2235
|
-
}
|
2941
|
+
patchId: parsed.data.patchId
|
2236
2942
|
};
|
2237
2943
|
}
|
2238
|
-
const createdAt = patchId;
|
2239
|
-
for (const moduleIdStr in currentParsedPatches.data) {
|
2240
|
-
const moduleId = moduleIdStr;
|
2241
|
-
if (!res[moduleId]) {
|
2242
|
-
res[moduleId] = [];
|
2243
|
-
}
|
2244
|
-
res[moduleId].push({
|
2245
|
-
patch: currentParsedPatches.data[moduleId],
|
2246
|
-
patch_id: patchId,
|
2247
|
-
created_at: new Date(Number(createdAt)).toISOString()
|
2248
|
-
});
|
2249
|
-
}
|
2250
|
-
} catch (err) {
|
2251
|
-
const msg = `Unexpected error while reading patch file. The cache may be corrupted or Val may be misconfigured. Try deleting the cache directory.`;
|
2252
|
-
console.error(`Val: ${msg}`, {
|
2253
|
-
patchId,
|
2254
|
-
error: err,
|
2255
|
-
dir: this.patchesRootPath
|
2256
|
-
});
|
2257
2944
|
return {
|
2258
|
-
|
2259
|
-
|
2260
|
-
message: msg,
|
2261
|
-
details: {
|
2262
|
-
patchId,
|
2263
|
-
error: err === null || err === void 0 ? void 0 : err.toString()
|
2264
|
-
}
|
2945
|
+
error: {
|
2946
|
+
message: `Could not parse save patch response. Error: ${fromError(parsed.error)}`
|
2265
2947
|
}
|
2266
2948
|
};
|
2267
2949
|
}
|
2268
|
-
|
2269
|
-
|
2270
|
-
|
2271
|
-
|
2272
|
-
|
2273
|
-
|
2274
|
-
|
2275
|
-
|
2276
|
-
|
2277
|
-
|
2278
|
-
|
2279
|
-
|
2280
|
-
getPatchFilePath(patchId) {
|
2281
|
-
return path__default.join(this.patchesRootPath, LocalValServer.PATCHES_DIR, patchId.toString());
|
2950
|
+
return {
|
2951
|
+
error: {
|
2952
|
+
message: "Could not save patch. HTTP error: " + res.status + " " + res.statusText
|
2953
|
+
}
|
2954
|
+
};
|
2955
|
+
}).catch(e => {
|
2956
|
+
return {
|
2957
|
+
error: {
|
2958
|
+
message: `Could save source file patch (connection error?): ${e instanceof Error ? e.message : e.toString()}`
|
2959
|
+
}
|
2960
|
+
};
|
2961
|
+
});
|
2282
2962
|
}
|
2283
|
-
|
2284
|
-
return {
|
2285
|
-
|
2286
|
-
|
2287
|
-
|
2963
|
+
async saveBase64EncodedBinaryFileFromPatch(filePath, patchId, data, type, metadata) {
|
2964
|
+
return fetch(`${this.hostUrl}/v1/${this.project}/patches/${patchId}/files`, {
|
2965
|
+
method: "POST",
|
2966
|
+
headers: {
|
2967
|
+
...this.authHeaders,
|
2968
|
+
"Content-Type": "application/json"
|
2969
|
+
},
|
2970
|
+
body: JSON.stringify({
|
2971
|
+
filePath: filePath,
|
2972
|
+
data,
|
2973
|
+
type,
|
2974
|
+
metadata
|
2975
|
+
})
|
2976
|
+
}).then(async res => {
|
2977
|
+
if (res.ok) {
|
2978
|
+
const parsed = SavePatchFileResponse.safeParse(await res.json());
|
2979
|
+
if (parsed.success) {
|
2980
|
+
return {
|
2981
|
+
patchId: parsed.data.patchId,
|
2982
|
+
filePath: parsed.data.filePath
|
2983
|
+
};
|
2984
|
+
}
|
2985
|
+
return {
|
2986
|
+
error: {
|
2987
|
+
message: `Could not parse save patch file response. Error: ${fromError(parsed.error)}`
|
2988
|
+
}
|
2989
|
+
};
|
2288
2990
|
}
|
2289
|
-
|
2290
|
-
|
2291
|
-
|
2292
|
-
|
2293
|
-
|
2294
|
-
|
2295
|
-
|
2296
|
-
|
2297
|
-
|
2298
|
-
|
2991
|
+
return {
|
2992
|
+
error: {
|
2993
|
+
message: "Could not save patch file. HTTP error: " + res.status + " " + res.statusText
|
2994
|
+
}
|
2995
|
+
};
|
2996
|
+
}).catch(e => {
|
2997
|
+
return {
|
2998
|
+
error: {
|
2999
|
+
message: `Could save source binary file in patch (connection error?): ${e.toString()}`
|
3000
|
+
}
|
3001
|
+
};
|
2299
3002
|
});
|
2300
3003
|
}
|
2301
|
-
async
|
2302
|
-
const
|
2303
|
-
|
2304
|
-
|
3004
|
+
async getHttpFiles(files) {
|
3005
|
+
const params = new URLSearchParams();
|
3006
|
+
const stringifiedFiles = JSON.stringify({
|
3007
|
+
files,
|
3008
|
+
root: this.root
|
3009
|
+
});
|
3010
|
+
params.set("body_sha",
|
3011
|
+
// We use this for cache invalidation
|
3012
|
+
Internal.getSHA256Hash(textEncoder.encode(stringifiedFiles)));
|
3013
|
+
return fetch(`${this.hostUrl}/v1/${this.project}/files?${params}`, {
|
3014
|
+
method: "PUT",
|
3015
|
+
// Yes, PUT is weird. Weirder to have a body in a GET request.
|
3016
|
+
headers: {
|
3017
|
+
...this.authHeaders,
|
3018
|
+
"Content-Type": "application/json"
|
3019
|
+
},
|
3020
|
+
body: stringifiedFiles
|
3021
|
+
}).then(async res => {
|
3022
|
+
if (res.ok) {
|
3023
|
+
const json = await res.json();
|
3024
|
+
// TODO: Check that all requested paths are in the response
|
3025
|
+
const parsedFileResponse = FilesResponse.safeParse(json);
|
3026
|
+
if (parsedFileResponse.success) {
|
3027
|
+
return parsedFileResponse.data;
|
3028
|
+
}
|
3029
|
+
return {
|
3030
|
+
error: {
|
3031
|
+
message: `Could not parse file response. Error: ${fromError(parsedFileResponse.error)}`
|
3032
|
+
}
|
3033
|
+
};
|
2305
3034
|
}
|
2306
|
-
return
|
2307
|
-
|
2308
|
-
|
3035
|
+
return {
|
3036
|
+
error: {
|
3037
|
+
message: "Could not get files. HTTP error: " + res.status + " " + res.statusText
|
3038
|
+
}
|
3039
|
+
};
|
3040
|
+
}).catch(e => {
|
3041
|
+
return {
|
3042
|
+
error: {
|
3043
|
+
message: `Could not get file (connection error?): ${e instanceof Error ? e.message : e.toString()}`
|
3044
|
+
}
|
3045
|
+
};
|
3046
|
+
});
|
2309
3047
|
}
|
2310
|
-
async
|
2311
|
-
|
2312
|
-
|
2313
|
-
|
2314
|
-
|
2315
|
-
|
2316
|
-
|
2317
|
-
|
3048
|
+
async getSourceFile(path) {
|
3049
|
+
const filesRes = await this.getHttpFiles([{
|
3050
|
+
filePath: path,
|
3051
|
+
location: "repo",
|
3052
|
+
root: this.root,
|
3053
|
+
commitSha: this.commitSha
|
3054
|
+
}]);
|
3055
|
+
if (filesRes.error) {
|
3056
|
+
return filesRes;
|
3057
|
+
}
|
3058
|
+
const file = filesRes.files.find(f => f.filePath === path);
|
3059
|
+
if (!file) {
|
3060
|
+
return {
|
3061
|
+
error: {
|
3062
|
+
message: `Could not find file ${path} in response`
|
3063
|
+
}
|
3064
|
+
};
|
2318
3065
|
}
|
2319
3066
|
return {
|
2320
|
-
|
2321
|
-
json: await this.getPatchedModules(patches)
|
3067
|
+
data: Buffer.from(file.value, "base64").toString("utf-8")
|
2322
3068
|
};
|
2323
3069
|
}
|
2324
|
-
|
2325
|
-
|
2326
|
-
|
2327
|
-
|
2328
|
-
|
2329
|
-
|
2330
|
-
|
2331
|
-
|
2332
|
-
|
2333
|
-
async logout() {
|
2334
|
-
return this.badRequest();
|
2335
|
-
}
|
2336
|
-
}
|
2337
|
-
|
2338
|
-
function decodeJwt(token, secretKey) {
|
2339
|
-
const [headerBase64, payloadBase64, signatureBase64, ...rest] = token.split(".");
|
2340
|
-
if (!headerBase64 || !payloadBase64 || !signatureBase64 || rest.length > 0) {
|
2341
|
-
console.debug("Invalid JWT: format is not exactly {header}.{payload}.{signature}", token);
|
2342
|
-
return null;
|
2343
|
-
}
|
2344
|
-
try {
|
2345
|
-
const parsedHeader = JSON.parse(Buffer.from(headerBase64, "base64").toString("utf8"));
|
2346
|
-
const headerVerification = JwtHeaderSchema.safeParse(parsedHeader);
|
2347
|
-
if (!headerVerification.success) {
|
2348
|
-
console.debug("Invalid JWT: invalid header", parsedHeader);
|
3070
|
+
async getBinaryFile(filePath) {
|
3071
|
+
// We could also just get this from public/ on the running server. Current approach feels more clean, but will be slower / puts more server load... We might want to change this
|
3072
|
+
const filesRes = await this.getHttpFiles([{
|
3073
|
+
filePath: filePath,
|
3074
|
+
location: "repo",
|
3075
|
+
root: this.root,
|
3076
|
+
commitSha: this.commitSha
|
3077
|
+
}]);
|
3078
|
+
if (filesRes.error) {
|
2349
3079
|
return null;
|
2350
3080
|
}
|
2351
|
-
|
2352
|
-
|
3081
|
+
const file = filesRes.files.find(f => f.filePath === filePath);
|
3082
|
+
if (!file) {
|
2353
3083
|
return null;
|
2354
3084
|
}
|
2355
|
-
|
2356
|
-
|
3085
|
+
return Buffer.from(file.value, "base64");
|
3086
|
+
}
|
3087
|
+
async getBase64EncodedBinaryFileFromPatch(filePath, patchId) {
|
3088
|
+
const filesRes = await this.getHttpFiles([{
|
3089
|
+
filePath: filePath,
|
3090
|
+
location: "patch",
|
3091
|
+
patchId
|
3092
|
+
}]);
|
3093
|
+
if (filesRes.error) {
|
2357
3094
|
return null;
|
2358
3095
|
}
|
2359
|
-
|
2360
|
-
|
2361
|
-
return null;
|
2362
|
-
}
|
2363
|
-
if (secretKey) {
|
2364
|
-
const signature = crypto.createHmac("sha256", secretKey).update(`${headerBase64}.${payloadBase64}`).digest("base64");
|
2365
|
-
if (signature !== signatureBase64) {
|
2366
|
-
console.debug("Invalid JWT: invalid signature");
|
3096
|
+
const file = filesRes.files.find(f => f.filePath === filePath);
|
3097
|
+
if (!file) {
|
2367
3098
|
return null;
|
2368
3099
|
}
|
3100
|
+
return Buffer.from(file.value, "base64");
|
2369
3101
|
}
|
2370
|
-
|
2371
|
-
const
|
2372
|
-
|
2373
|
-
|
2374
|
-
|
2375
|
-
|
2376
|
-
|
2377
|
-
|
2378
|
-
|
2379
|
-
|
2380
|
-
|
2381
|
-
const
|
2382
|
-
|
2383
|
-
|
2384
|
-
|
2385
|
-
|
2386
|
-
|
2387
|
-
|
2388
|
-
|
2389
|
-
|
2390
|
-
|
2391
|
-
|
2392
|
-
|
2393
|
-
|
2394
|
-
}
|
2395
|
-
|
2396
|
-
|
2397
|
-
|
2398
|
-
|
2399
|
-
|
2400
|
-
|
2401
|
-
|
2402
|
-
|
2403
|
-
|
2404
|
-
|
2405
|
-
|
2406
|
-
|
2407
|
-
/** Remote FS dependent methods: */
|
2408
|
-
|
2409
|
-
async getModule(moduleId,
|
2410
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
2411
|
-
_options) {
|
2412
|
-
if (this.moduleCache) {
|
2413
|
-
return this.moduleCache[moduleId];
|
3102
|
+
async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
|
3103
|
+
const params = new URLSearchParams();
|
3104
|
+
params.set("file_path", filePath);
|
3105
|
+
try {
|
3106
|
+
const metadataRes = await fetch(`${this.hostUrl}/v1/${this.project}/patches/${patchId}/metadata?${params}`, {
|
3107
|
+
headers: {
|
3108
|
+
...this.authHeaders,
|
3109
|
+
"Content-Type": "application/json"
|
3110
|
+
}
|
3111
|
+
});
|
3112
|
+
if (metadataRes.ok) {
|
3113
|
+
const json = await metadataRes.json();
|
3114
|
+
const parsed = MetadataRes.safeParse(json);
|
3115
|
+
if (parsed.success) {
|
3116
|
+
return {
|
3117
|
+
metadata: parsed.data.metadata
|
3118
|
+
};
|
3119
|
+
}
|
3120
|
+
return {
|
3121
|
+
errors: [{
|
3122
|
+
message: `Could not parse metadata response. Error: ${fromError(parsed.error)}`,
|
3123
|
+
filePath
|
3124
|
+
}]
|
3125
|
+
};
|
3126
|
+
}
|
3127
|
+
return {
|
3128
|
+
errors: [{
|
3129
|
+
message: "Could not get metadata. HTTP error: " + metadataRes.status + " " + metadataRes.statusText,
|
3130
|
+
filePath
|
3131
|
+
}]
|
3132
|
+
};
|
3133
|
+
} catch (err) {
|
3134
|
+
return {
|
3135
|
+
errors: [{
|
3136
|
+
message: "Could not get metadata (connection error?): " + (err instanceof Error ? err.message : (err === null || err === void 0 ? void 0 : err.toString()) || "unknown error")
|
3137
|
+
}]
|
3138
|
+
};
|
2414
3139
|
}
|
2415
|
-
throw new Error("Module cache not initialized");
|
2416
3140
|
}
|
2417
|
-
async
|
2418
|
-
|
2419
|
-
|
2420
|
-
|
2421
|
-
|
3141
|
+
async getBinaryFileMetadata(filePath, type) {
|
3142
|
+
// TODO: call get metadata on this instance which caches + returns the metadata for this filepath / commit
|
3143
|
+
// something like this:
|
3144
|
+
// const params = new URLSearchParams();
|
3145
|
+
// params.set("path", filePath);
|
3146
|
+
// params.set("type", type);
|
3147
|
+
// return fetch(new URL(`${this.route}/files/metadata?${params}`, baseUrl)).then(
|
3148
|
+
// (res) => {
|
3149
|
+
// return res.json();
|
3150
|
+
// }
|
3151
|
+
// );
|
3152
|
+
return {
|
3153
|
+
errors: [{
|
3154
|
+
message: "Not implemented: " + type,
|
3155
|
+
filePath
|
3156
|
+
}]
|
3157
|
+
};
|
2422
3158
|
}
|
2423
|
-
|
2424
|
-
return
|
2425
|
-
|
2426
|
-
|
2427
|
-
|
2428
|
-
|
2429
|
-
|
2430
|
-
|
2431
|
-
|
2432
|
-
|
3159
|
+
async deletePatches(patchIds) {
|
3160
|
+
return fetch(`${this.hostUrl}/v1/${this.project}/patches`, {
|
3161
|
+
method: "DELETE",
|
3162
|
+
headers: {
|
3163
|
+
...this.authHeaders,
|
3164
|
+
"Content-Type": "application/json"
|
3165
|
+
},
|
3166
|
+
body: JSON.stringify({
|
3167
|
+
patchIds
|
3168
|
+
})
|
3169
|
+
}).then(async res => {
|
3170
|
+
if (res.ok) {
|
3171
|
+
const parsed = DeletePatchesResponse.safeParse(await res.json());
|
3172
|
+
if (parsed.success) {
|
3173
|
+
const errors = {};
|
3174
|
+
for (const err of parsed.data.errors || []) {
|
3175
|
+
errors[err.patchId] = err;
|
2433
3176
|
}
|
2434
|
-
|
2435
|
-
|
2436
|
-
|
2437
|
-
|
2438
|
-
|
2439
|
-
|
2440
|
-
|
2441
|
-
|
2442
|
-
|
2443
|
-
|
2444
|
-
const patchIds = patches.map(([patchId]) => patchId);
|
2445
|
-
const fetchRes = await fetch(url, {
|
2446
|
-
method: "POST",
|
2447
|
-
headers: getAuthHeaders(token, "application/json"),
|
2448
|
-
body: JSON.stringify({
|
2449
|
-
patchIds
|
2450
|
-
})
|
2451
|
-
});
|
2452
|
-
if (fetchRes.status === 200) {
|
3177
|
+
if (Object.keys(errors).length === 0) {
|
3178
|
+
return {
|
3179
|
+
deleted: parsed.data.deleted
|
3180
|
+
};
|
3181
|
+
}
|
3182
|
+
return {
|
3183
|
+
deleted: parsed.data.deleted,
|
3184
|
+
errors
|
3185
|
+
};
|
3186
|
+
}
|
2453
3187
|
return {
|
2454
|
-
|
2455
|
-
|
3188
|
+
error: {
|
3189
|
+
message: `Could not parse delete patches response. Error: ${fromError(parsed.error)}`
|
3190
|
+
}
|
2456
3191
|
};
|
2457
|
-
} else {
|
2458
|
-
return createJsonError(fetchRes);
|
2459
3192
|
}
|
3193
|
+
return {
|
3194
|
+
error: {
|
3195
|
+
message: "Could not delete patches. HTTP error: " + res.status + " " + res.statusText
|
3196
|
+
}
|
3197
|
+
};
|
3198
|
+
}).catch(e => {
|
3199
|
+
return {
|
3200
|
+
error: {
|
3201
|
+
message: `Could not delete patches (connection error?): ${e instanceof Error ? e.message : e.toString()}`
|
3202
|
+
}
|
3203
|
+
};
|
2460
3204
|
});
|
2461
3205
|
}
|
2462
|
-
async
|
2463
|
-
const params = createParams({
|
2464
|
-
root: this.apiOptions.root,
|
2465
|
-
commit,
|
2466
|
-
ext: ["ts", "js", "json"],
|
2467
|
-
package: ["@valbuild/core@" + this.options.versions.core, "@valbuild/next@" + this.options.versions.next],
|
2468
|
-
include: ["**/*.val.{js,ts},package.json,tsconfig.json,jsconfig.json"]
|
2469
|
-
});
|
2470
|
-
const url = new URL(`/v1/eval/${this.options.remote}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
|
3206
|
+
async commit(prepared, message, committer, newBranch) {
|
2471
3207
|
try {
|
2472
|
-
const
|
2473
|
-
|
2474
|
-
|
2475
|
-
|
2476
|
-
|
2477
|
-
|
2478
|
-
|
2479
|
-
|
2480
|
-
|
2481
|
-
|
2482
|
-
|
2483
|
-
|
2484
|
-
|
2485
|
-
|
2486
|
-
|
2487
|
-
|
2488
|
-
|
2489
|
-
|
2490
|
-
|
2491
|
-
|
2492
|
-
|
2493
|
-
|
2494
|
-
|
2495
|
-
details: "Invalid response: missing modules"
|
2496
|
-
};
|
2497
|
-
}
|
2498
|
-
if (error) {
|
2499
|
-
console.error("Could not initialize remote modules", error);
|
3208
|
+
const existingBranch = this.branch;
|
3209
|
+
const res = await fetch(`${this.hostUrl}/v1/${this.project}/commit`, {
|
3210
|
+
method: "POST",
|
3211
|
+
headers: {
|
3212
|
+
...this.authHeaders,
|
3213
|
+
"Content-Type": "application/json"
|
3214
|
+
},
|
3215
|
+
body: JSON.stringify({
|
3216
|
+
patchedSourceFiles: prepared.patchedSourceFiles,
|
3217
|
+
patchedBinaryFilesDescriptors: prepared.patchedBinaryFilesDescriptors,
|
3218
|
+
appliedPatches: prepared.appliedPatches,
|
3219
|
+
commit: this.commitSha,
|
3220
|
+
root: this.root,
|
3221
|
+
baseSha: await this.getBaseSha(),
|
3222
|
+
committer,
|
3223
|
+
message,
|
3224
|
+
existingBranch,
|
3225
|
+
newBranch
|
3226
|
+
})
|
3227
|
+
});
|
3228
|
+
if (res.ok) {
|
3229
|
+
const parsed = CommitResponse.safeParse(await res.json());
|
3230
|
+
if (parsed.success) {
|
2500
3231
|
return {
|
2501
|
-
|
2502
|
-
|
2503
|
-
|
2504
|
-
...error
|
2505
|
-
}
|
3232
|
+
updatedFiles: parsed.data.updatedFiles,
|
3233
|
+
commit: parsed.data.commit,
|
3234
|
+
branch: parsed.data.branch
|
2506
3235
|
};
|
2507
3236
|
}
|
2508
|
-
this.moduleCache = json.modules;
|
2509
3237
|
return {
|
2510
|
-
|
3238
|
+
error: {
|
3239
|
+
message: `Could not parse commit response. Error: ${fromError(parsed.error)}`
|
3240
|
+
}
|
2511
3241
|
};
|
2512
|
-
} else {
|
2513
|
-
return createJsonError(fetchRes);
|
2514
3242
|
}
|
3243
|
+
return {
|
3244
|
+
error: {
|
3245
|
+
message: "Could not commit. HTTP error: " + res.status + " " + res.statusText
|
3246
|
+
}
|
3247
|
+
};
|
2515
3248
|
} catch (err) {
|
2516
3249
|
return {
|
2517
|
-
|
2518
|
-
|
2519
|
-
message: "Failed to fetch: check network connection"
|
3250
|
+
error: {
|
3251
|
+
message: `Could not commit (connection error?): ${err instanceof Error ? err.message : (err === null || err === void 0 ? void 0 : err.toString()) || "unknown error"}`
|
2520
3252
|
}
|
2521
3253
|
};
|
2522
3254
|
}
|
2523
3255
|
}
|
2524
|
-
|
2525
|
-
|
2526
|
-
|
2527
|
-
|
2528
|
-
|
2529
|
-
|
2530
|
-
|
2531
|
-
|
3256
|
+
}
|
3257
|
+
|
3258
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
3259
|
+
class ValServer {
|
3260
|
+
constructor(valModules, options, callbacks) {
|
3261
|
+
this.valModules = valModules;
|
3262
|
+
this.options = options;
|
3263
|
+
this.callbacks = callbacks;
|
3264
|
+
if (options.mode === "fs") {
|
3265
|
+
this.serverOps = new ValOpsFS(options.cwd, valModules, {
|
3266
|
+
formatter: options.formatter
|
3267
|
+
});
|
3268
|
+
} else if (options.mode === "http") {
|
3269
|
+
this.serverOps = new ValOpsHttp(options.valContentUrl, options.project, options.commit, options.branch, options.apiKey, valModules, {
|
3270
|
+
formatter: options.formatter,
|
3271
|
+
root: options.root
|
2532
3272
|
});
|
2533
|
-
}
|
2534
|
-
const res = await withAuth(this.options.valSecret, cookies, errorMessageType, async data => {
|
2535
|
-
if (!this.moduleCache) {
|
2536
|
-
return this.init(commit, data.token);
|
2537
|
-
} else {
|
2538
|
-
return {
|
2539
|
-
status: 200
|
2540
|
-
};
|
2541
|
-
}
|
2542
|
-
});
|
2543
|
-
if (res.status === 200) {
|
2544
|
-
return result.ok(undefined);
|
2545
3273
|
} else {
|
2546
|
-
|
3274
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
3275
|
+
throw new Error("Invalid mode: " + (options === null || options === void 0 ? void 0 : options.mode));
|
2547
3276
|
}
|
2548
3277
|
}
|
2549
|
-
/* Auth endpoints */
|
2550
3278
|
|
3279
|
+
//#region auth
|
3280
|
+
async enable(query) {
|
3281
|
+
const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
|
3282
|
+
if (typeof redirectToRes !== "string") {
|
3283
|
+
return redirectToRes;
|
3284
|
+
}
|
3285
|
+
await this.callbacks.onEnable(true);
|
3286
|
+
return {
|
3287
|
+
cookies: {
|
3288
|
+
[VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
|
3289
|
+
},
|
3290
|
+
status: 302,
|
3291
|
+
redirectTo: redirectToRes
|
3292
|
+
};
|
3293
|
+
}
|
3294
|
+
async disable(query) {
|
3295
|
+
const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
|
3296
|
+
if (typeof redirectToRes !== "string") {
|
3297
|
+
return redirectToRes;
|
3298
|
+
}
|
3299
|
+
await this.callbacks.onDisable(true);
|
3300
|
+
return {
|
3301
|
+
cookies: {
|
3302
|
+
[VAL_ENABLE_COOKIE_NAME]: {
|
3303
|
+
value: "false"
|
3304
|
+
}
|
3305
|
+
},
|
3306
|
+
status: 302,
|
3307
|
+
redirectTo: redirectToRes
|
3308
|
+
};
|
3309
|
+
}
|
2551
3310
|
async authorize(query) {
|
2552
3311
|
if (typeof query.redirect_to !== "string") {
|
2553
3312
|
return {
|
@@ -2579,6 +3338,28 @@ class ProxyValServer extends ValServer {
|
|
2579
3338
|
};
|
2580
3339
|
}
|
2581
3340
|
async callback(query, cookies) {
|
3341
|
+
if (!this.options.project) {
|
3342
|
+
return {
|
3343
|
+
status: 302,
|
3344
|
+
cookies: {
|
3345
|
+
[VAL_STATE_COOKIE$1]: {
|
3346
|
+
value: null
|
3347
|
+
}
|
3348
|
+
},
|
3349
|
+
redirectTo: this.getAppErrorUrl("Project is not set")
|
3350
|
+
};
|
3351
|
+
}
|
3352
|
+
if (!this.options.valSecret) {
|
3353
|
+
return {
|
3354
|
+
status: 302,
|
3355
|
+
cookies: {
|
3356
|
+
[VAL_STATE_COOKIE$1]: {
|
3357
|
+
value: null
|
3358
|
+
}
|
3359
|
+
},
|
3360
|
+
redirectTo: this.getAppErrorUrl("Secret is not set")
|
3361
|
+
};
|
3362
|
+
}
|
2582
3363
|
const {
|
2583
3364
|
success: callbackReqSuccess,
|
2584
3365
|
error: callbackReqError
|
@@ -2607,10 +3388,22 @@ class ProxyValServer extends ValServer {
|
|
2607
3388
|
};
|
2608
3389
|
}
|
2609
3390
|
const exp = getExpire();
|
3391
|
+
const valSecret = this.options.valSecret;
|
3392
|
+
if (!valSecret) {
|
3393
|
+
return {
|
3394
|
+
status: 302,
|
3395
|
+
cookies: {
|
3396
|
+
[VAL_STATE_COOKIE$1]: {
|
3397
|
+
value: null
|
3398
|
+
}
|
3399
|
+
},
|
3400
|
+
redirectTo: this.getAppErrorUrl("Setup is not correct: secret is missing")
|
3401
|
+
};
|
3402
|
+
}
|
2610
3403
|
const cookie = encodeJwt({
|
2611
3404
|
...data,
|
2612
3405
|
exp // this is the client side exp
|
2613
|
-
},
|
3406
|
+
}, valSecret);
|
2614
3407
|
return {
|
2615
3408
|
status: 302,
|
2616
3409
|
cookies: {
|
@@ -2632,7 +3425,7 @@ class ProxyValServer extends ValServer {
|
|
2632
3425
|
redirectTo: callbackReqSuccess.redirect_uri || "/"
|
2633
3426
|
};
|
2634
3427
|
}
|
2635
|
-
async
|
3428
|
+
async log() {
|
2636
3429
|
return {
|
2637
3430
|
status: 200,
|
2638
3431
|
cookies: {
|
@@ -2646,8 +3439,41 @@ class ProxyValServer extends ValServer {
|
|
2646
3439
|
};
|
2647
3440
|
}
|
2648
3441
|
async session(cookies) {
|
3442
|
+
if (this.serverOps instanceof ValOpsFS) {
|
3443
|
+
return {
|
3444
|
+
status: 200,
|
3445
|
+
json: {
|
3446
|
+
mode: "local",
|
3447
|
+
enabled: await this.callbacks.isEnabled()
|
3448
|
+
}
|
3449
|
+
};
|
3450
|
+
}
|
3451
|
+
if (!this.options.project) {
|
3452
|
+
return {
|
3453
|
+
status: 500,
|
3454
|
+
json: {
|
3455
|
+
message: "Project is not set"
|
3456
|
+
}
|
3457
|
+
};
|
3458
|
+
}
|
3459
|
+
if (!this.options.valSecret) {
|
3460
|
+
return {
|
3461
|
+
status: 500,
|
3462
|
+
json: {
|
3463
|
+
message: "Secret is not set"
|
3464
|
+
}
|
3465
|
+
};
|
3466
|
+
}
|
2649
3467
|
return withAuth(this.options.valSecret, cookies, "session", async data => {
|
2650
|
-
|
3468
|
+
if (!this.options.valBuildUrl) {
|
3469
|
+
return {
|
3470
|
+
status: 500,
|
3471
|
+
json: {
|
3472
|
+
message: "Val is not correctly setup. Build url is missing"
|
3473
|
+
}
|
3474
|
+
};
|
3475
|
+
}
|
3476
|
+
const url = new URL(`/api/val/${this.options.project}/auth/session`, this.options.valBuildUrl);
|
2651
3477
|
const fetchRes = await fetch(url, {
|
2652
3478
|
headers: getAuthHeaders(data.token, "application/json")
|
2653
3479
|
});
|
@@ -2656,7 +3482,7 @@ class ProxyValServer extends ValServer {
|
|
2656
3482
|
status: fetchRes.status,
|
2657
3483
|
json: {
|
2658
3484
|
mode: "proxy",
|
2659
|
-
|
3485
|
+
d: await this.callbacks.isEnabled(),
|
2660
3486
|
...(await fetchRes.json())
|
2661
3487
|
}
|
2662
3488
|
};
|
@@ -2672,8 +3498,17 @@ class ProxyValServer extends ValServer {
|
|
2672
3498
|
});
|
2673
3499
|
}
|
2674
3500
|
async consumeCode(code) {
|
2675
|
-
|
3501
|
+
if (!this.options.project) {
|
3502
|
+
throw new Error("Project is not set");
|
3503
|
+
}
|
3504
|
+
if (!this.options.valBuildUrl) {
|
3505
|
+
throw new Error("Val build url is not set");
|
3506
|
+
}
|
3507
|
+
const url = new URL(`/api/val/${this.options.project}/auth/token`, this.options.valBuildUrl);
|
2676
3508
|
url.searchParams.set("code", encodeURIComponent(code));
|
3509
|
+
if (!this.options.apiKey) {
|
3510
|
+
return null;
|
3511
|
+
}
|
2677
3512
|
return fetch(url, {
|
2678
3513
|
method: "POST",
|
2679
3514
|
headers: getAuthHeaders(this.options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
|
@@ -2697,186 +3532,453 @@ class ProxyValServer extends ValServer {
|
|
2697
3532
|
return null;
|
2698
3533
|
});
|
2699
3534
|
}
|
2700
|
-
getAuthorizeUrl(
|
2701
|
-
|
2702
|
-
|
3535
|
+
getAuthorizeUrl(publicValApiRe, token) {
|
3536
|
+
if (!this.options.project) {
|
3537
|
+
throw new Error("Project is not set");
|
3538
|
+
}
|
3539
|
+
if (!this.options.valBuildUrl) {
|
3540
|
+
throw new Error("Val build url is not set");
|
3541
|
+
}
|
3542
|
+
const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
|
3543
|
+
url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRe}/callback`));
|
2703
3544
|
url.searchParams.set("state", token);
|
2704
3545
|
return url.toString();
|
2705
3546
|
}
|
2706
3547
|
getAppErrorUrl(error) {
|
2707
|
-
|
3548
|
+
if (!this.options.project) {
|
3549
|
+
throw new Error("Project is not set");
|
3550
|
+
}
|
3551
|
+
if (!this.options.valBuildUrl) {
|
3552
|
+
throw new Error("Val build url is not set");
|
3553
|
+
}
|
3554
|
+
const url = new URL(`/auth/${this.options.project}/authorize`, this.options.valBuildUrl);
|
2708
3555
|
url.searchParams.set("error", encodeURIComponent(error));
|
2709
3556
|
return url.toString();
|
2710
3557
|
}
|
3558
|
+
getAuth(cookies) {
|
3559
|
+
const cookie = cookies[VAL_SESSION_COOKIE$1];
|
3560
|
+
if (!this.options.valSecret) {
|
3561
|
+
if (this.serverOps instanceof ValOpsFS) {
|
3562
|
+
return {
|
3563
|
+
error: null,
|
3564
|
+
id: null
|
3565
|
+
};
|
3566
|
+
} else {
|
3567
|
+
return {
|
3568
|
+
error: "Setup is not correct: secret is missing"
|
3569
|
+
};
|
3570
|
+
}
|
3571
|
+
}
|
3572
|
+
if (typeof cookie === "string") {
|
3573
|
+
const decodedToken = decodeJwt(cookie, this.options.valSecret);
|
3574
|
+
if (!decodedToken) {
|
3575
|
+
if (this.serverOps instanceof ValOpsFS) {
|
3576
|
+
return {
|
3577
|
+
error: null,
|
3578
|
+
id: null
|
3579
|
+
};
|
3580
|
+
}
|
3581
|
+
return {
|
3582
|
+
error: "Could not verify session (invalid token). You will need to login again."
|
3583
|
+
};
|
3584
|
+
}
|
3585
|
+
const verification = IntegratedServerJwtPayload.safeParse(decodedToken);
|
3586
|
+
if (!verification.success) {
|
3587
|
+
if (this.serverOps instanceof ValOpsFS) {
|
3588
|
+
return {
|
3589
|
+
error: null,
|
3590
|
+
id: null
|
3591
|
+
};
|
3592
|
+
}
|
3593
|
+
return {
|
3594
|
+
error: "Session invalid or, most likely, expired. You will need to login again."
|
3595
|
+
};
|
3596
|
+
}
|
3597
|
+
return {
|
3598
|
+
id: verification.data.sub
|
3599
|
+
};
|
3600
|
+
} else {
|
3601
|
+
if (this.serverOps instanceof ValOpsFS) {
|
3602
|
+
return {
|
3603
|
+
error: null,
|
3604
|
+
id: null
|
3605
|
+
};
|
3606
|
+
}
|
3607
|
+
return {
|
3608
|
+
error: "Login required: cookie not found"
|
3609
|
+
};
|
3610
|
+
}
|
3611
|
+
}
|
3612
|
+
async logout() {
|
3613
|
+
return {
|
3614
|
+
status: 200,
|
3615
|
+
cookies: {
|
3616
|
+
[VAL_SESSION_COOKIE$1]: {
|
3617
|
+
value: null
|
3618
|
+
},
|
3619
|
+
[VAL_STATE_COOKIE$1]: {
|
3620
|
+
value: null
|
3621
|
+
}
|
3622
|
+
}
|
3623
|
+
};
|
3624
|
+
}
|
2711
3625
|
|
2712
|
-
|
2713
|
-
async
|
2714
|
-
|
2715
|
-
|
2716
|
-
|
2717
|
-
|
2718
|
-
|
2719
|
-
|
2720
|
-
|
2721
|
-
|
2722
|
-
|
2723
|
-
|
2724
|
-
|
2725
|
-
|
2726
|
-
|
2727
|
-
|
2728
|
-
|
3626
|
+
//#region patches
|
3627
|
+
async getPatches(query, cookies) {
|
3628
|
+
const auth = this.getAuth(cookies);
|
3629
|
+
if (auth.error) {
|
3630
|
+
return {
|
3631
|
+
status: 401,
|
3632
|
+
json: {
|
3633
|
+
message: auth.error
|
3634
|
+
}
|
3635
|
+
};
|
3636
|
+
}
|
3637
|
+
const authors = query.authors;
|
3638
|
+
const patches = await this.serverOps.findPatches({
|
3639
|
+
authors
|
3640
|
+
});
|
3641
|
+
if (patches.errors && Object.keys(patches.errors).length > 0) {
|
3642
|
+
console.error("Val: Failed to get patches", patches.errors);
|
3643
|
+
return {
|
3644
|
+
status: 500,
|
3645
|
+
json: {
|
3646
|
+
message: "Failed to get patches",
|
3647
|
+
details: patches.errors
|
3648
|
+
}
|
3649
|
+
};
|
3650
|
+
}
|
3651
|
+
const res = {};
|
3652
|
+
for (const [patchIdS, patchData] of Object.entries(patches.patches)) {
|
3653
|
+
var _patchData$appliedAt;
|
3654
|
+
const patchId = patchIdS;
|
3655
|
+
if (!res[patchData.path]) {
|
3656
|
+
res[patchData.path] = [];
|
3657
|
+
}
|
3658
|
+
res[patchData.path].push({
|
3659
|
+
patch_id: patchId,
|
3660
|
+
created_at: patchData.createdAt,
|
3661
|
+
applied_at_base_sha: ((_patchData$appliedAt = patchData.appliedAt) === null || _patchData$appliedAt === void 0 ? void 0 : _patchData$appliedAt.baseSha) || null,
|
3662
|
+
author: patchData.authorId ?? undefined
|
2729
3663
|
});
|
2730
|
-
|
2731
|
-
|
2732
|
-
|
2733
|
-
|
3664
|
+
}
|
3665
|
+
return {
|
3666
|
+
status: 200,
|
3667
|
+
json: res
|
3668
|
+
};
|
3669
|
+
}
|
3670
|
+
async deletePatches(query, cookies) {
|
3671
|
+
const auth = this.getAuth(cookies);
|
3672
|
+
if (auth.error) {
|
3673
|
+
return {
|
3674
|
+
status: 401,
|
3675
|
+
json: {
|
3676
|
+
message: auth.error
|
3677
|
+
}
|
3678
|
+
};
|
3679
|
+
}
|
3680
|
+
if (this.options.mode === "http" && !("id" in auth)) {
|
3681
|
+
return {
|
3682
|
+
status: 401,
|
3683
|
+
json: {
|
3684
|
+
message: "Unauthorized"
|
3685
|
+
}
|
3686
|
+
};
|
3687
|
+
}
|
3688
|
+
const ids = query.id;
|
3689
|
+
const deleteRes = await this.serverOps.deletePatches(ids);
|
3690
|
+
if (deleteRes.errors && Object.keys(deleteRes.errors).length > 0) {
|
3691
|
+
console.error("Val: Failed to delete patches", deleteRes.errors);
|
3692
|
+
return {
|
3693
|
+
status: 500,
|
3694
|
+
json: {
|
3695
|
+
message: "Failed to delete patches",
|
3696
|
+
details: deleteRes.errors
|
3697
|
+
}
|
3698
|
+
};
|
3699
|
+
}
|
3700
|
+
return {
|
3701
|
+
status: 200,
|
3702
|
+
json: ids
|
3703
|
+
};
|
3704
|
+
}
|
3705
|
+
|
3706
|
+
//#region tree ops
|
3707
|
+
async getSchema(cookies) {
|
3708
|
+
const auth = this.getAuth(cookies);
|
3709
|
+
if (auth.error) {
|
3710
|
+
return {
|
3711
|
+
status: 401,
|
3712
|
+
json: {
|
3713
|
+
message: auth.error
|
3714
|
+
}
|
3715
|
+
};
|
3716
|
+
}
|
3717
|
+
const moduleErrors = await this.serverOps.getModuleErrors();
|
3718
|
+
if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
|
3719
|
+
console.error("Val: Module errors", moduleErrors);
|
3720
|
+
return {
|
3721
|
+
status: 500,
|
3722
|
+
json: {
|
3723
|
+
message: "Val is not correctly setup. Check the val.modules file",
|
3724
|
+
details: moduleErrors
|
3725
|
+
}
|
3726
|
+
};
|
3727
|
+
}
|
3728
|
+
const schemaSha = await this.serverOps.getSchemaSha();
|
3729
|
+
const schemas = await this.serverOps.getSchemas();
|
3730
|
+
const serializedSchemas = {};
|
3731
|
+
for (const [moduleFilePathS, schema] of Object.entries(schemas)) {
|
3732
|
+
const moduleFilePath = moduleFilePathS;
|
3733
|
+
serializedSchemas[moduleFilePath] = schema.serialize();
|
3734
|
+
}
|
3735
|
+
return {
|
3736
|
+
status: 200,
|
3737
|
+
json: {
|
3738
|
+
schemaSha,
|
3739
|
+
schemas: serializedSchemas
|
3740
|
+
}
|
3741
|
+
};
|
3742
|
+
}
|
3743
|
+
async putTree(body, treePath, query, cookies) {
|
3744
|
+
var _bodyRes$data, _bodyRes$data2, _bodyRes$data3;
|
3745
|
+
const auth = this.getAuth(cookies);
|
3746
|
+
if (auth.error) {
|
3747
|
+
return {
|
3748
|
+
status: 401,
|
3749
|
+
json: {
|
3750
|
+
message: auth.error
|
3751
|
+
}
|
3752
|
+
};
|
3753
|
+
}
|
3754
|
+
// TODO: move
|
3755
|
+
const PutTreeBody = z.object({
|
3756
|
+
patchIds: z.array(z.string().refine(id => true // TODO:
|
3757
|
+
)).optional(),
|
3758
|
+
addPatch: z.object({
|
3759
|
+
path: z.string().refine(path => true // TODO:
|
3760
|
+
),
|
3761
|
+
patch: Patch
|
3762
|
+
}).optional()
|
3763
|
+
}).optional();
|
3764
|
+
const moduleErrors = await this.serverOps.getModuleErrors();
|
3765
|
+
if ((moduleErrors === null || moduleErrors === void 0 ? void 0 : moduleErrors.length) > 0) {
|
3766
|
+
console.error("Val: Module errors", moduleErrors);
|
3767
|
+
return {
|
3768
|
+
status: 500,
|
3769
|
+
json: {
|
3770
|
+
message: "Val is not correctly setup. Check the val.modules file",
|
3771
|
+
details: moduleErrors
|
3772
|
+
}
|
3773
|
+
};
|
3774
|
+
}
|
3775
|
+
const bodyRes = PutTreeBody.safeParse(body);
|
3776
|
+
if (!bodyRes.success) {
|
3777
|
+
return {
|
3778
|
+
status: 400,
|
3779
|
+
json: {
|
3780
|
+
message: "Invalid body: " + fromError(bodyRes.error).toString(),
|
3781
|
+
details: bodyRes.error.errors
|
3782
|
+
}
|
3783
|
+
};
|
3784
|
+
}
|
3785
|
+
let tree;
|
3786
|
+
let patchAnalysis = null;
|
3787
|
+
if ((_bodyRes$data = bodyRes.data) !== null && _bodyRes$data !== void 0 && _bodyRes$data.patchIds && ((_bodyRes$data2 = bodyRes.data) === null || _bodyRes$data2 === void 0 || (_bodyRes$data2 = _bodyRes$data2.patchIds) === null || _bodyRes$data2 === void 0 ? void 0 : _bodyRes$data2.length) > 0 || (_bodyRes$data3 = bodyRes.data) !== null && _bodyRes$data3 !== void 0 && _bodyRes$data3.addPatch) {
|
3788
|
+
var _bodyRes$data4, _bodyRes$data5;
|
3789
|
+
// TODO: validate patches_sha
|
3790
|
+
const patchIds = (_bodyRes$data4 = bodyRes.data) === null || _bodyRes$data4 === void 0 ? void 0 : _bodyRes$data4.patchIds;
|
3791
|
+
const patchOps = patchIds && patchIds.length > 0 ? await this.serverOps.getPatchOpsById(patchIds) : {
|
3792
|
+
patches: {}
|
3793
|
+
};
|
3794
|
+
let patchErrors = undefined;
|
3795
|
+
for (const [patchIdS, error] of Object.entries(patchOps.errors || {})) {
|
3796
|
+
const patchId = patchIdS;
|
3797
|
+
if (!patchErrors) {
|
3798
|
+
patchErrors = {};
|
3799
|
+
}
|
3800
|
+
patchErrors[patchId] = {
|
3801
|
+
message: error.message
|
3802
|
+
};
|
3803
|
+
}
|
3804
|
+
if ((_bodyRes$data5 = bodyRes.data) !== null && _bodyRes$data5 !== void 0 && _bodyRes$data5.addPatch) {
|
3805
|
+
const newPatchModuleFilePath = bodyRes.data.addPatch.path;
|
3806
|
+
const newPatchOps = bodyRes.data.addPatch.patch;
|
3807
|
+
const authorId = null; // TODO:
|
3808
|
+
const createPatchRes = await this.serverOps.createPatch(newPatchModuleFilePath, newPatchOps, authorId);
|
3809
|
+
if (createPatchRes.error) {
|
3810
|
+
return {
|
3811
|
+
status: 500,
|
3812
|
+
json: {
|
3813
|
+
message: "Failed to create patch: " + createPatchRes.error.message,
|
3814
|
+
details: createPatchRes.error
|
3815
|
+
}
|
3816
|
+
};
|
3817
|
+
}
|
3818
|
+
for (const fileRes of createPatchRes.files) {
|
3819
|
+
if (fileRes.error) {
|
3820
|
+
// clean up broken patch:
|
3821
|
+
await this.serverOps.deletePatches([createPatchRes.patchId]);
|
3822
|
+
return {
|
3823
|
+
status: 500,
|
3824
|
+
json: {
|
3825
|
+
message: "Failed to create patch",
|
3826
|
+
details: fileRes.error
|
3827
|
+
}
|
3828
|
+
};
|
3829
|
+
}
|
3830
|
+
}
|
3831
|
+
patchOps.patches[createPatchRes.patchId] = {
|
3832
|
+
path: newPatchModuleFilePath,
|
3833
|
+
patch: newPatchOps,
|
3834
|
+
authorId,
|
3835
|
+
createdAt: createPatchRes.createdAt,
|
3836
|
+
appliedAt: null
|
2734
3837
|
};
|
2735
|
-
} else {
|
2736
|
-
return createJsonError(fetchRes);
|
2737
3838
|
}
|
2738
|
-
|
2739
|
-
|
2740
|
-
|
2741
|
-
|
2742
|
-
|
2743
|
-
|
2744
|
-
|
2745
|
-
|
2746
|
-
|
2747
|
-
|
2748
|
-
|
2749
|
-
|
3839
|
+
// TODO: errors
|
3840
|
+
patchAnalysis = this.serverOps.analyzePatches(patchOps.patches);
|
3841
|
+
tree = {
|
3842
|
+
...(await this.serverOps.getTree({
|
3843
|
+
...patchAnalysis,
|
3844
|
+
...patchOps
|
3845
|
+
}))
|
3846
|
+
};
|
3847
|
+
if (query.validate_all === "true") {
|
3848
|
+
const allTree = await this.serverOps.getTree();
|
3849
|
+
tree = {
|
3850
|
+
sources: {
|
3851
|
+
...allTree.sources,
|
3852
|
+
...tree.sources
|
3853
|
+
},
|
3854
|
+
errors: {
|
3855
|
+
...allTree.errors,
|
3856
|
+
...tree.errors
|
2750
3857
|
}
|
2751
3858
|
};
|
2752
3859
|
}
|
2753
|
-
|
2754
|
-
|
2755
|
-
|
2756
|
-
|
2757
|
-
|
2758
|
-
|
2759
|
-
|
2760
|
-
|
2761
|
-
|
2762
|
-
|
2763
|
-
|
2764
|
-
|
3860
|
+
} else {
|
3861
|
+
tree = await this.serverOps.getTree();
|
3862
|
+
}
|
3863
|
+
if (tree.errors && Object.keys(tree.errors).length > 0) {
|
3864
|
+
console.error("Val: Failed to get tree", JSON.stringify(tree.errors));
|
3865
|
+
}
|
3866
|
+
if (query.validate_sources === "true" || query.validate_binary_files === "true") {
|
3867
|
+
const schemas = await this.serverOps.getSchemas();
|
3868
|
+
const sourcesValidation = await this.serverOps.validateSources(schemas, tree.sources);
|
3869
|
+
|
3870
|
+
// TODO: send validation errors
|
3871
|
+
if (query.validate_binary_files === "true") {
|
3872
|
+
await this.serverOps.validateFiles(schemas, tree.sources, sourcesValidation.files);
|
3873
|
+
}
|
3874
|
+
}
|
3875
|
+
const schemaSha = await this.serverOps.getSchemaSha();
|
3876
|
+
const modules = {};
|
3877
|
+
for (const [moduleFilePathS, module] of Object.entries(tree.sources)) {
|
3878
|
+
const moduleFilePath = moduleFilePathS;
|
3879
|
+
if (moduleFilePath.startsWith(treePath)) {
|
3880
|
+
modules[moduleFilePath] = {
|
3881
|
+
source: module,
|
3882
|
+
patches: patchAnalysis ? {
|
3883
|
+
applied: patchAnalysis.patchesByModule[moduleFilePath].map(p => p.patchId)
|
3884
|
+
} : undefined
|
2765
3885
|
};
|
2766
|
-
} else {
|
2767
|
-
return createJsonError(fetchRes);
|
2768
3886
|
}
|
2769
|
-
}
|
3887
|
+
}
|
3888
|
+
return {
|
3889
|
+
status: 200,
|
3890
|
+
json: {
|
3891
|
+
schemaSha,
|
3892
|
+
modules
|
3893
|
+
}
|
3894
|
+
};
|
2770
3895
|
}
|
2771
|
-
async
|
2772
|
-
const
|
2773
|
-
if (
|
3896
|
+
async postSave(body, cookies) {
|
3897
|
+
const auth = this.getAuth(cookies);
|
3898
|
+
if (auth.error) {
|
2774
3899
|
return {
|
2775
3900
|
status: 401,
|
2776
3901
|
json: {
|
2777
|
-
message:
|
3902
|
+
message: auth.error
|
2778
3903
|
}
|
2779
3904
|
};
|
2780
3905
|
}
|
2781
|
-
const
|
2782
|
-
|
3906
|
+
const PostSaveBody = z.object({
|
3907
|
+
patchIds: z.array(z.string().refine(id => true // TODO:
|
3908
|
+
))
|
2783
3909
|
});
|
2784
|
-
|
2785
|
-
|
2786
|
-
|
2787
|
-
|
2788
|
-
|
2789
|
-
|
2790
|
-
|
2791
|
-
|
2792
|
-
|
2793
|
-
|
2794
|
-
|
2795
|
-
|
2796
|
-
|
2797
|
-
|
2798
|
-
|
2799
|
-
|
2800
|
-
|
2801
|
-
|
2802
|
-
|
2803
|
-
|
2804
|
-
|
2805
|
-
|
2806
|
-
|
3910
|
+
const bodyRes = PostSaveBody.safeParse(body);
|
3911
|
+
if (!bodyRes.success) {
|
3912
|
+
return {
|
3913
|
+
status: 400,
|
3914
|
+
json: {
|
3915
|
+
message: "Invalid body: " + fromError(bodyRes.error).toString(),
|
3916
|
+
details: bodyRes.error.errors
|
3917
|
+
}
|
3918
|
+
};
|
3919
|
+
}
|
3920
|
+
const {
|
3921
|
+
patchIds
|
3922
|
+
} = bodyRes.data;
|
3923
|
+
const patches = await this.serverOps.getPatchOpsById(patchIds);
|
3924
|
+
const analysis = this.serverOps.analyzePatches(patches.patches);
|
3925
|
+
const preparedCommit = await this.serverOps.prepare({
|
3926
|
+
...analysis,
|
3927
|
+
...patches
|
3928
|
+
});
|
3929
|
+
if (this.serverOps instanceof ValOpsFS) {
|
3930
|
+
await this.serverOps.saveFiles(preparedCommit);
|
3931
|
+
return {
|
3932
|
+
status: 200,
|
3933
|
+
json: {} // TODO:
|
3934
|
+
};
|
3935
|
+
} else if (this.serverOps instanceof ValOpsHttp) {
|
3936
|
+
if (auth.error === undefined && auth.id) {
|
3937
|
+
await this.serverOps.commit(preparedCommit, "Update content: " + Object.keys(analysis.patchesByModule) + " modules changed", auth.id);
|
2807
3938
|
return {
|
2808
|
-
status:
|
2809
|
-
json:
|
3939
|
+
status: 200,
|
3940
|
+
json: {} // TODO:
|
2810
3941
|
};
|
2811
|
-
} else {
|
2812
|
-
return createJsonError(fetchRes);
|
2813
|
-
}
|
2814
|
-
});
|
2815
|
-
}
|
2816
|
-
async getMetadata(filePath, sha256) {
|
2817
|
-
const url = new URL(`/v1/metadata/${this.options.remote}${filePath}?commit=${this.options.git.commit}${sha256 ? `&sha256=${sha256}` : ""}`, this.options.valContentUrl);
|
2818
|
-
const fetchRes = await fetch(url, {
|
2819
|
-
headers: {
|
2820
|
-
Authorization: `Bearer ${this.options.apiKey}`
|
2821
|
-
}
|
2822
|
-
});
|
2823
|
-
if (fetchRes.status === 200) {
|
2824
|
-
const json = await fetchRes.json();
|
2825
|
-
if (json.type === "file") {
|
2826
|
-
return json;
|
2827
|
-
} else if (json.type === "image") {
|
2828
|
-
return json;
|
2829
3942
|
}
|
3943
|
+
return {
|
3944
|
+
status: 401,
|
3945
|
+
json: {
|
3946
|
+
message: "Unauthorized"
|
3947
|
+
}
|
3948
|
+
};
|
3949
|
+
} else {
|
3950
|
+
throw new Error("Invalid server ops");
|
2830
3951
|
}
|
2831
|
-
return undefined;
|
2832
3952
|
}
|
2833
|
-
|
2834
|
-
|
2835
|
-
|
2836
|
-
|
3953
|
+
|
3954
|
+
//#region files
|
3955
|
+
async getFiles(filePath, query) {
|
3956
|
+
// NOTE: no auth here since you would need the patch_id to get something that is not published.
|
3957
|
+
// For everything that is published, well they are already public so no auth required there...
|
3958
|
+
// We could imagine adding auth just to be a 200% certain,
|
3959
|
+
// However that won't work since images are requested by the nextjs backend as a part of image optimization (again: as an example) which is a backend-to-backend op (no cookies, ...).
|
3960
|
+
// So: 1) patch ids are not possible to guess (but possible to brute force)
|
3961
|
+
// 2) the process of shimming a patch into the frontend would be quite challenging (so just trying out this attack would require a lot of effort)
|
3962
|
+
// 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
|
3963
|
+
// Thus: attack surface + ease of attack + benefit = low probability of attack
|
3964
|
+
// If we couldn't argue that patch ids are secret enough, then this would be a problem.
|
3965
|
+
let fileBuffer;
|
3966
|
+
if (query.patch_id) {
|
3967
|
+
fileBuffer = await this.serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
|
3968
|
+
} else {
|
3969
|
+
fileBuffer = await this.serverOps.getBinaryFile(filePath);
|
2837
3970
|
}
|
2838
|
-
|
2839
|
-
|
2840
|
-
|
2841
|
-
|
2842
|
-
|
2843
|
-
if (fetchRes.status === 200) {
|
2844
|
-
// TODO: does this stream data?
|
2845
|
-
if (fetchRes.body) {
|
2846
|
-
return {
|
2847
|
-
status: fetchRes.status,
|
2848
|
-
headers: {
|
2849
|
-
"Content-Type": fetchRes.headers.get("Content-Type") || "",
|
2850
|
-
"Content-Length": fetchRes.headers.get("Content-Length") || "0",
|
2851
|
-
"Cache-Control": fetchRes.headers.get("Cache-Control") || ""
|
2852
|
-
},
|
2853
|
-
body: fetchRes.body
|
2854
|
-
};
|
2855
|
-
} else {
|
2856
|
-
return {
|
2857
|
-
status: 500,
|
2858
|
-
json: {
|
2859
|
-
message: "No body in response"
|
2860
|
-
}
|
2861
|
-
};
|
2862
|
-
}
|
3971
|
+
if (fileBuffer) {
|
3972
|
+
return {
|
3973
|
+
status: 200,
|
3974
|
+
body: bufferToReadableStream(fileBuffer)
|
3975
|
+
};
|
2863
3976
|
} else {
|
2864
|
-
if (!(reqHeaders.host && reqHeaders["x-forwarded-proto"])) {
|
2865
|
-
return {
|
2866
|
-
status: 500,
|
2867
|
-
json: {
|
2868
|
-
message: "Missing host or x-forwarded-proto header"
|
2869
|
-
}
|
2870
|
-
};
|
2871
|
-
}
|
2872
|
-
const host = `${reqHeaders["x-forwarded-proto"]}://${reqHeaders["host"]}`;
|
2873
|
-
const staticPublicUrl = new URL(filePath.slice("/public".length), host).toString();
|
2874
|
-
const fetchRes = await fetch(staticPublicUrl);
|
2875
3977
|
return {
|
2876
|
-
status:
|
2877
|
-
|
2878
|
-
|
2879
|
-
|
3978
|
+
status: 404,
|
3979
|
+
json: {
|
3980
|
+
message: "File not found"
|
3981
|
+
}
|
2880
3982
|
};
|
2881
3983
|
}
|
2882
3984
|
}
|
@@ -2981,37 +4083,21 @@ function getStateFromCookie(stateCookie) {
|
|
2981
4083
|
};
|
2982
4084
|
}
|
2983
4085
|
}
|
2984
|
-
async function createJsonError(fetchRes) {
|
2985
|
-
var _fetchRes$headers$get;
|
2986
|
-
if ((_fetchRes$headers$get = fetchRes.headers.get("Content-Type")) !== null && _fetchRes$headers$get !== void 0 && _fetchRes$headers$get.includes("application/json")) {
|
2987
|
-
return {
|
2988
|
-
status: fetchRes.status,
|
2989
|
-
json: await fetchRes.json()
|
2990
|
-
};
|
2991
|
-
}
|
2992
|
-
console.error("Unexpected failure (did not get a json) - Val down?", fetchRes.status, await fetchRes.text());
|
2993
|
-
return {
|
2994
|
-
status: fetchRes.status,
|
2995
|
-
json: {
|
2996
|
-
message: "Unexpected failure (did not get a json) - Val down?"
|
2997
|
-
}
|
2998
|
-
};
|
2999
|
-
}
|
3000
4086
|
function createStateCookie(state) {
|
3001
4087
|
return Buffer.from(JSON.stringify(state), "utf8").toString("base64");
|
3002
4088
|
}
|
3003
|
-
const ValAppJwtPayload = z
|
3004
|
-
sub: z
|
3005
|
-
exp: z
|
3006
|
-
project: z
|
3007
|
-
org: z
|
4089
|
+
const ValAppJwtPayload = z.object({
|
4090
|
+
sub: z.string(),
|
4091
|
+
exp: z.number(),
|
4092
|
+
project: z.string(),
|
4093
|
+
org: z.string()
|
3008
4094
|
});
|
3009
|
-
const IntegratedServerJwtPayload = z
|
3010
|
-
sub: z
|
3011
|
-
exp: z
|
3012
|
-
token: z
|
3013
|
-
org: z
|
3014
|
-
project: z
|
4095
|
+
const IntegratedServerJwtPayload = z.object({
|
4096
|
+
sub: z.string(),
|
4097
|
+
exp: z.number(),
|
4098
|
+
token: z.string(),
|
4099
|
+
org: z.string(),
|
4100
|
+
project: z.string()
|
3015
4101
|
});
|
3016
4102
|
async function withAuth(secret, cookies, errorMessageType, handler) {
|
3017
4103
|
const cookie = cookies[VAL_SESSION_COOKIE$1];
|
@@ -3068,42 +4154,147 @@ function getAuthHeaders(token, type) {
|
|
3068
4154
|
Authorization: `Bearer ${token}`
|
3069
4155
|
};
|
3070
4156
|
}
|
3071
|
-
|
3072
|
-
|
3073
|
-
|
3074
|
-
|
3075
|
-
|
3076
|
-
|
3077
|
-
|
3078
|
-
|
4157
|
+
const ENABLE_COOKIE_VALUE = {
|
4158
|
+
value: "true",
|
4159
|
+
options: {
|
4160
|
+
httpOnly: false,
|
4161
|
+
sameSite: "lax"
|
4162
|
+
}
|
4163
|
+
};
|
4164
|
+
const chunkSize = 1024 * 1024;
|
4165
|
+
function bufferToReadableStream(buffer) {
|
4166
|
+
const stream = new ReadableStream({
|
4167
|
+
start(controller) {
|
4168
|
+
let offset = 0;
|
4169
|
+
while (offset < buffer.length) {
|
4170
|
+
const chunk = buffer.subarray(offset, offset + chunkSize);
|
4171
|
+
controller.enqueue(chunk);
|
4172
|
+
offset += chunkSize;
|
3079
4173
|
}
|
3080
|
-
|
3081
|
-
paramsString += `${key}=${encodeURIComponent(param)}`;
|
3082
|
-
}
|
3083
|
-
if (paramIdx < Object.keys(params).length - 1) {
|
3084
|
-
paramsString += "&";
|
4174
|
+
controller.close();
|
3085
4175
|
}
|
3086
|
-
|
4176
|
+
});
|
4177
|
+
return stream;
|
4178
|
+
}
|
4179
|
+
function getRedirectUrl(query, overrideHost) {
|
4180
|
+
if (typeof query.redirect_to !== "string") {
|
4181
|
+
return {
|
4182
|
+
status: 400,
|
4183
|
+
json: {
|
4184
|
+
message: "Missing redirect_to query param"
|
4185
|
+
}
|
4186
|
+
};
|
3087
4187
|
}
|
3088
|
-
|
4188
|
+
if (overrideHost) {
|
4189
|
+
return overrideHost + "?redirect_to=" + encodeURIComponent(query.redirect_to);
|
4190
|
+
}
|
4191
|
+
return query.redirect_to;
|
3089
4192
|
}
|
3090
4193
|
|
3091
|
-
|
3092
|
-
|
3093
|
-
|
3094
|
-
|
3095
|
-
|
3096
|
-
|
3097
|
-
|
4194
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
4195
|
+
const COMMON_MIME_TYPES = {
|
4196
|
+
aac: "audio/aac",
|
4197
|
+
abw: "application/x-abiword",
|
4198
|
+
arc: "application/x-freearc",
|
4199
|
+
avif: "image/avif",
|
4200
|
+
avi: "video/x-msvideo",
|
4201
|
+
azw: "application/vnd.amazon.ebook",
|
4202
|
+
bin: "application/octet-stream",
|
4203
|
+
bmp: "image/bmp",
|
4204
|
+
bz: "application/x-bzip",
|
4205
|
+
bz2: "application/x-bzip2",
|
4206
|
+
cda: "application/x-cdf",
|
4207
|
+
csh: "application/x-csh",
|
4208
|
+
css: "text/css",
|
4209
|
+
csv: "text/csv",
|
4210
|
+
doc: "application/msword",
|
4211
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
4212
|
+
eot: "application/vnd.ms-fontobject",
|
4213
|
+
epub: "application/epub+zip",
|
4214
|
+
gz: "application/gzip",
|
4215
|
+
gif: "image/gif",
|
4216
|
+
htm: "text/html",
|
4217
|
+
html: "text/html",
|
4218
|
+
ico: "image/vnd.microsoft.icon",
|
4219
|
+
ics: "text/calendar",
|
4220
|
+
jar: "application/java-archive",
|
4221
|
+
jpeg: "image/jpeg",
|
4222
|
+
jpg: "image/jpeg",
|
4223
|
+
js: "text/javascript",
|
4224
|
+
json: "application/json",
|
4225
|
+
jsonld: "application/ld+json",
|
4226
|
+
mid: "audio/midi",
|
4227
|
+
midi: "audio/midi",
|
4228
|
+
mjs: "text/javascript",
|
4229
|
+
mp3: "audio/mpeg",
|
4230
|
+
mp4: "video/mp4",
|
4231
|
+
mpeg: "video/mpeg",
|
4232
|
+
mpkg: "application/vnd.apple.installer+xml",
|
4233
|
+
odp: "application/vnd.oasis.opendocument.presentation",
|
4234
|
+
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
4235
|
+
odt: "application/vnd.oasis.opendocument.text",
|
4236
|
+
oga: "audio/ogg",
|
4237
|
+
ogv: "video/ogg",
|
4238
|
+
ogx: "application/ogg",
|
4239
|
+
opus: "audio/opus",
|
4240
|
+
otf: "font/otf",
|
4241
|
+
png: "image/png",
|
4242
|
+
pdf: "application/pdf",
|
4243
|
+
php: "application/x-httpd-php",
|
4244
|
+
ppt: "application/vnd.ms-powerpoint",
|
4245
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
4246
|
+
rar: "application/vnd.rar",
|
4247
|
+
rtf: "application/rtf",
|
4248
|
+
sh: "application/x-sh",
|
4249
|
+
svg: "image/svg+xml",
|
4250
|
+
tar: "application/x-tar",
|
4251
|
+
tif: "image/tiff",
|
4252
|
+
tiff: "image/tiff",
|
4253
|
+
ts: "video/mp2t",
|
4254
|
+
ttf: "font/ttf",
|
4255
|
+
txt: "text/plain",
|
4256
|
+
vsd: "application/vnd.visio",
|
4257
|
+
wav: "audio/wav",
|
4258
|
+
weba: "audio/webm",
|
4259
|
+
webm: "video/webm",
|
4260
|
+
webp: "image/webp",
|
4261
|
+
woff: "font/woff",
|
4262
|
+
woff2: "font/woff2",
|
4263
|
+
xhtml: "application/xhtml+xml",
|
4264
|
+
xls: "application/vnd.ms-excel",
|
4265
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
4266
|
+
xml: "application/xml",
|
4267
|
+
xul: "application/vnd.mozilla.xul+xml",
|
4268
|
+
zip: "application/zip",
|
4269
|
+
"3gp": "video/3gpp; audio/3gpp if it doesn't contain video",
|
4270
|
+
"3g2": "video/3gpp2; audio/3gpp2 if it doesn't contain video",
|
4271
|
+
"7z": "application/x-7z-compressed"
|
4272
|
+
};
|
4273
|
+
function guessMimeTypeFromPath(filePath) {
|
4274
|
+
const fileExt = filePath.split(".").pop();
|
4275
|
+
if (fileExt) {
|
4276
|
+
return COMMON_MIME_TYPES[fileExt.toLowerCase()] || null;
|
3098
4277
|
}
|
4278
|
+
return null;
|
4279
|
+
}
|
4280
|
+
|
4281
|
+
async function createValServer(valModules, route, opts, callbacks, formatter) {
|
4282
|
+
const valServerConfig = await initHandlerOptions(route, opts);
|
4283
|
+
return new ValServer(valModules, {
|
4284
|
+
formatter,
|
4285
|
+
...valServerConfig
|
4286
|
+
}, callbacks);
|
3099
4287
|
}
|
3100
4288
|
async function initHandlerOptions(route, opts) {
|
3101
4289
|
const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
|
3102
4290
|
const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
|
3103
4291
|
const isProxyMode = opts.mode === "proxy" || opts.mode === undefined && (maybeApiKey || maybeValSecret);
|
4292
|
+
const valEnableRedirectUrl = opts.valEnableRedirectUrl || process.env.VAL_ENABLE_REDIRECT_URL;
|
4293
|
+
const valDisableRedirectUrl = opts.valDisableRedirectUrl || process.env.VAL_DISABLE_REDIRECT_URL;
|
4294
|
+
const maybeValProject = opts.project || process.env.VAL_PROJECT;
|
4295
|
+
const valBuildUrl = opts.valBuildUrl || process.env.VAL_BUILD_URL || "https://app.val.build";
|
3104
4296
|
if (isProxyMode) {
|
3105
4297
|
var _opts$versions, _opts$versions2;
|
3106
|
-
const valBuildUrl = opts.valBuildUrl || process.env.VAL_BUILD_URL || "https://app.val.build";
|
3107
4298
|
if (!maybeApiKey || !maybeValSecret) {
|
3108
4299
|
throw new Error("VAL_API_KEY and VAL_SECRET env vars must both be set in proxy mode");
|
3109
4300
|
}
|
@@ -3116,9 +4307,8 @@ async function initHandlerOptions(route, opts) {
|
|
3116
4307
|
if (!maybeGitBranch) {
|
3117
4308
|
throw new Error("VAL_GIT_BRANCH env var must be set in proxy mode");
|
3118
4309
|
}
|
3119
|
-
|
3120
|
-
|
3121
|
-
throw new Error("Proxy mode does not work unless the 'remote' option in val.config is defined or the VAL_REMOTE env var is set.");
|
4310
|
+
if (!maybeValProject) {
|
4311
|
+
throw new Error("Proxy mode does not work unless the 'project' option in val.config is defined or the VAL_PROJECT env var is set.");
|
3122
4312
|
}
|
3123
4313
|
const coreVersion = (_opts$versions = opts.versions) === null || _opts$versions === void 0 ? void 0 : _opts$versions.core;
|
3124
4314
|
if (!coreVersion) {
|
@@ -3129,43 +4319,40 @@ async function initHandlerOptions(route, opts) {
|
|
3129
4319
|
throw new Error("Could not determine version of @valbuild/next");
|
3130
4320
|
}
|
3131
4321
|
return {
|
3132
|
-
mode: "
|
4322
|
+
mode: "http",
|
3133
4323
|
route,
|
3134
4324
|
apiKey: maybeApiKey,
|
3135
4325
|
valSecret: maybeValSecret,
|
3136
|
-
|
4326
|
+
commit: maybeGitCommit,
|
4327
|
+
branch: maybeGitBranch,
|
4328
|
+
root: opts.root,
|
4329
|
+
project: maybeValProject,
|
4330
|
+
valEnableRedirectUrl,
|
4331
|
+
valDisableRedirectUrl,
|
3137
4332
|
valContentUrl,
|
3138
|
-
|
3139
|
-
core: coreVersion,
|
3140
|
-
next: nextVersion
|
3141
|
-
},
|
3142
|
-
git: {
|
3143
|
-
commit: maybeGitCommit,
|
3144
|
-
branch: maybeGitBranch
|
3145
|
-
},
|
3146
|
-
remote: maybeValRemote,
|
3147
|
-
valEnableRedirectUrl: opts.valEnableRedirectUrl || process.env.VAL_ENABLE_REDIRECT_URL,
|
3148
|
-
valDisableRedirectUrl: opts.valDisableRedirectUrl || process.env.VAL_DISABLE_REDIRECT_URL
|
4333
|
+
valBuildUrl
|
3149
4334
|
};
|
3150
4335
|
} else {
|
3151
4336
|
const cwd = process.cwd();
|
3152
|
-
const
|
3153
|
-
const git = await safeReadGit(cwd);
|
4337
|
+
const valBuildUrl = opts.valBuildUrl || process.env.VAL_BUILD_URL || "https://app.val.build";
|
3154
4338
|
return {
|
3155
|
-
mode: "
|
3156
|
-
|
3157
|
-
|
3158
|
-
valDisableRedirectUrl
|
3159
|
-
|
3160
|
-
|
3161
|
-
|
3162
|
-
|
4339
|
+
mode: "fs",
|
4340
|
+
cwd,
|
4341
|
+
route,
|
4342
|
+
valDisableRedirectUrl,
|
4343
|
+
valEnableRedirectUrl,
|
4344
|
+
valBuildUrl,
|
4345
|
+
apiKey: maybeApiKey,
|
4346
|
+
valSecret: maybeValSecret,
|
4347
|
+
project: maybeValProject
|
3163
4348
|
};
|
3164
4349
|
}
|
3165
4350
|
}
|
4351
|
+
|
4352
|
+
// TODO: remove
|
3166
4353
|
async function safeReadGit(cwd) {
|
3167
4354
|
async function findGitHead(currentDir, depth) {
|
3168
|
-
const gitHeadPath =
|
4355
|
+
const gitHeadPath = fsPath.join(currentDir, ".git", "HEAD");
|
3169
4356
|
if (depth > 1000) {
|
3170
4357
|
console.error(`Reached max depth while scanning for .git folder. Current working dir: ${cwd}.`);
|
3171
4358
|
return {
|
@@ -3189,7 +4376,7 @@ async function safeReadGit(cwd) {
|
|
3189
4376
|
};
|
3190
4377
|
}
|
3191
4378
|
} catch (error) {
|
3192
|
-
const parentDir =
|
4379
|
+
const parentDir = fsPath.dirname(currentDir);
|
3193
4380
|
|
3194
4381
|
// We've reached the root directory
|
3195
4382
|
if (parentDir === currentDir) {
|
@@ -3213,7 +4400,7 @@ async function safeReadGit(cwd) {
|
|
3213
4400
|
}
|
3214
4401
|
async function readCommit(gitDir, branchName) {
|
3215
4402
|
try {
|
3216
|
-
return (await promises.readFile(
|
4403
|
+
return (await promises.readFile(fsPath.join(gitDir, ".git", "refs", "heads", branchName), "utf-8")).trim();
|
3217
4404
|
} catch (err) {
|
3218
4405
|
return undefined;
|
3219
4406
|
}
|
@@ -3230,10 +4417,6 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
3230
4417
|
return async req => {
|
3231
4418
|
var _req$method;
|
3232
4419
|
const valServer = await valServerPromise;
|
3233
|
-
const requestHeaders = {
|
3234
|
-
host: req.headers.get("host"),
|
3235
|
-
"x-forwarded-proto": req.headers.get("x-forwarded-proto")
|
3236
|
-
};
|
3237
4420
|
const url = new URL(req.url);
|
3238
4421
|
if (!url.pathname.startsWith(route)) {
|
3239
4422
|
const error = {
|
@@ -3294,35 +4477,40 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
3294
4477
|
return convert(await valServer.disable({
|
3295
4478
|
redirect_to: url.searchParams.get("redirect_to") || undefined
|
3296
4479
|
}));
|
3297
|
-
} else if (method === "POST" && path === "/
|
3298
|
-
const body = await req.json();
|
3299
|
-
return convert(await valServer.postCommit(body, getCookies(req, [VAL_SESSION_COOKIE]), requestHeaders));
|
3300
|
-
} else if (method === "POST" && path === "/validate") {
|
4480
|
+
} else if (method === "POST" && path === "/save") {
|
3301
4481
|
const body = await req.json();
|
3302
|
-
return convert(await valServer.
|
3303
|
-
|
3304
|
-
|
3305
|
-
|
3306
|
-
|
3307
|
-
|
3308
|
-
|
3309
|
-
|
4482
|
+
return convert(await valServer.postSave(body, getCookies(req, [VAL_SESSION_COOKIE])));
|
4483
|
+
// } else if (method === "POST" && path === "/validate") {
|
4484
|
+
// const body = (await req.json()) as unknown;
|
4485
|
+
// return convert(
|
4486
|
+
// await valServer.postValidate(
|
4487
|
+
// body,
|
4488
|
+
// getCookies(req, [VAL_SESSION_COOKIE]),
|
4489
|
+
// requestHeaders
|
4490
|
+
// )
|
4491
|
+
// );
|
4492
|
+
} else if (method === "GET" && path === "/schema") {
|
4493
|
+
return convert(await valServer.getSchema(getCookies(req, [VAL_SESSION_COOKIE])));
|
4494
|
+
} else if (method === "PUT" && path.startsWith(TREE_PATH_PREFIX)) {
|
4495
|
+
return withTreePath(path, TREE_PATH_PREFIX)(async treePath => convert(await valServer.putTree(await req.json(), treePath, {
|
4496
|
+
patches_sha: url.searchParams.get("patches_sha") || undefined,
|
4497
|
+
validate_all: url.searchParams.get("validate_all") || undefined,
|
4498
|
+
validate_binary_files: url.searchParams.get("validate_binary_files") || undefined,
|
4499
|
+
validate_sources: url.searchParams.get("validate_sources") || undefined
|
4500
|
+
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
3310
4501
|
} else if (method === "GET" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
3311
4502
|
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.getPatches({
|
3312
|
-
|
4503
|
+
authors: url.searchParams.getAll("author")
|
3313
4504
|
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
3314
|
-
} else if (method === "POST" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
3315
|
-
const body = await req.json();
|
3316
|
-
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.postPatches(body, getCookies(req, [VAL_SESSION_COOKIE]))));
|
3317
4505
|
} else if (method === "DELETE" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
3318
4506
|
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.deletePatches({
|
3319
4507
|
id: url.searchParams.getAll("id")
|
3320
4508
|
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
3321
|
-
} else if (path.startsWith(FILES_PATH_PREFIX)) {
|
4509
|
+
} else if (method === "GET" && path.startsWith(FILES_PATH_PREFIX)) {
|
3322
4510
|
const treePath = path.slice(FILES_PATH_PREFIX.length);
|
3323
4511
|
return convert(await valServer.getFiles(treePath, {
|
3324
|
-
|
3325
|
-
}
|
4512
|
+
patch_id: url.searchParams.get("patch_id") || undefined
|
4513
|
+
}));
|
3326
4514
|
} else {
|
3327
4515
|
return convert({
|
3328
4516
|
status: 404,
|
@@ -3374,10 +4562,10 @@ class ValFSHost {
|
|
3374
4562
|
return this.currentDirectory;
|
3375
4563
|
}
|
3376
4564
|
getCanonicalFileName(fileName) {
|
3377
|
-
if (
|
3378
|
-
return
|
4565
|
+
if (fsPath__default.isAbsolute(fileName)) {
|
4566
|
+
return fsPath__default.normalize(fileName);
|
3379
4567
|
}
|
3380
|
-
return
|
4568
|
+
return fsPath__default.resolve(this.getCurrentDirectory(), fileName);
|
3381
4569
|
}
|
3382
4570
|
fileExists(fileName) {
|
3383
4571
|
return this.valFS.fileExists(fileName);
|
@@ -3390,6 +4578,14 @@ class ValFSHost {
|
|
3390
4578
|
}
|
3391
4579
|
}
|
3392
4580
|
|
4581
|
+
function getValidationErrorFileRef(validationError) {
|
4582
|
+
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;
|
4583
|
+
if (!maybeRef) {
|
4584
|
+
return null;
|
4585
|
+
}
|
4586
|
+
return maybeRef;
|
4587
|
+
}
|
4588
|
+
|
3393
4589
|
// TODO: find a better name? transformFixesToPatch?
|
3394
4590
|
async function createFixPatch(config, apply, sourcePath, validationError) {
|
3395
4591
|
async function getImageMetadata() {
|
@@ -3398,7 +4594,7 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
3398
4594
|
// TODO:
|
3399
4595
|
throw Error("Cannot fix image without a file reference");
|
3400
4596
|
}
|
3401
|
-
const filename =
|
4597
|
+
const filename = fsPath__default.join(config.projectRoot, fileRef);
|
3402
4598
|
const buffer = fs.readFileSync(filename);
|
3403
4599
|
return extractImageMetadata(filename, buffer);
|
3404
4600
|
}
|
@@ -3408,7 +4604,7 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
3408
4604
|
// TODO:
|
3409
4605
|
throw Error("Cannot fix file without a file reference");
|
3410
4606
|
}
|
3411
|
-
const filename =
|
4607
|
+
const filename = fsPath__default.join(config.projectRoot, fileRef);
|
3412
4608
|
const buffer = fs.readFileSync(filename);
|
3413
4609
|
return extractFileMetadata(fileRef, buffer);
|
3414
4610
|
}
|
@@ -3574,4 +4770,4 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
3574
4770
|
};
|
3575
4771
|
}
|
3576
4772
|
|
3577
|
-
export {
|
4773
|
+
export { Patch, PatchJSON, Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createFixPatch, createService, createValApiRouter, createValServer, decodeJwt, encodeJwt, formatSyntaxErrorTree, getCompilerOptions, getExpire, patchSourceFile, safeReadGit };
|