@valbuild/server 0.61.0 → 0.62.1
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 +92 -133
- package/dist/declarations/src/createValApiRouter.d.ts +6 -7
- package/dist/declarations/src/index.d.ts +0 -1
- package/dist/valbuild-server.cjs.dev.js +2620 -1456
- package/dist/valbuild-server.cjs.prod.js +2620 -1456
- package/dist/valbuild-server.esm.js +2624 -1459
- package/package.json +7 -10
- package/dist/declarations/src/LocalValServer.d.ts +0 -62
@@ -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,560 +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) {
|
1389
1331
|
this.valModules = valModules;
|
1390
1332
|
this.options = options;
|
1391
|
-
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));
|
1392
1347
|
}
|
1393
1348
|
|
1394
|
-
|
1395
|
-
async
|
1396
|
-
|
1397
|
-
|
1398
|
-
|
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;
|
1399
1423
|
}
|
1400
|
-
await this.callbacks.onEnable(true);
|
1401
1424
|
return {
|
1402
|
-
|
1403
|
-
|
1404
|
-
|
1405
|
-
|
1406
|
-
|
1425
|
+
baseSha: this.baseSha,
|
1426
|
+
schemaSha: this.schemaSha,
|
1427
|
+
sources: this.sources,
|
1428
|
+
schemas: this.schemas,
|
1429
|
+
moduleErrors: this.modulesErrors
|
1407
1430
|
};
|
1408
1431
|
}
|
1409
|
-
async
|
1410
|
-
const
|
1411
|
-
|
1412
|
-
|
1413
|
-
}
|
1414
|
-
await this.
|
1415
|
-
return {
|
1416
|
-
cookies: {
|
1417
|
-
[VAL_ENABLE_COOKIE_NAME]: {
|
1418
|
-
value: "false"
|
1419
|
-
}
|
1420
|
-
},
|
1421
|
-
status: 302,
|
1422
|
-
redirectTo: redirectToRes
|
1423
|
-
};
|
1432
|
+
async init() {
|
1433
|
+
const {
|
1434
|
+
baseSha,
|
1435
|
+
schemaSha
|
1436
|
+
} = await this.initTree();
|
1437
|
+
await this.onInit(baseSha, schemaSha);
|
1424
1438
|
}
|
1425
|
-
async
|
1426
|
-
|
1427
|
-
query, cookies, requestHeaders) {
|
1428
|
-
const ensureRes = await debugTiming("ensureInitialized", () => this.ensureInitialized("getTree", cookies));
|
1429
|
-
if (result.isErr(ensureRes)) {
|
1430
|
-
return ensureRes.error;
|
1431
|
-
}
|
1432
|
-
const applyPatches = query.patch === "true";
|
1433
|
-
const includeSource = query.source === "true";
|
1434
|
-
const includeSchema = query.schema === "true";
|
1435
|
-
const moduleIds = await debugTiming("getAllModules", () => this.getAllModules(treePath));
|
1436
|
-
let {
|
1437
|
-
patchIdsByModuleId,
|
1438
|
-
patchesById,
|
1439
|
-
fileUpdates
|
1440
|
-
} = {
|
1441
|
-
patchIdsByModuleId: {},
|
1442
|
-
patchesById: {},
|
1443
|
-
fileUpdates: {}
|
1444
|
-
};
|
1445
|
-
if (applyPatches) {
|
1446
|
-
const res = await debugTiming("readPatches", () => this.readPatches(cookies));
|
1447
|
-
if (result.isErr(res)) {
|
1448
|
-
return res.error;
|
1449
|
-
}
|
1450
|
-
patchIdsByModuleId = res.value.patchIdsByModuleId;
|
1451
|
-
patchesById = res.value.patchesById;
|
1452
|
-
fileUpdates = res.value.fileUpdates;
|
1453
|
-
}
|
1454
|
-
const validate = false;
|
1455
|
-
const possiblyPatchedContent = await debugTiming("applyAllPatchesThenValidate", () => Promise.all(moduleIds.map(async moduleId => {
|
1456
|
-
return this.applyAllPatchesThenValidate(moduleId, patchIdsByModuleId, patchesById, fileUpdates, cookies, requestHeaders, applyPatches, validate, includeSource, includeSchema);
|
1457
|
-
})));
|
1458
|
-
const modules = Object.fromEntries(possiblyPatchedContent.map(serializedModuleContent => {
|
1459
|
-
const module = {
|
1460
|
-
schema: serializedModuleContent.schema,
|
1461
|
-
source: serializedModuleContent.source,
|
1462
|
-
errors: serializedModuleContent.errors
|
1463
|
-
};
|
1464
|
-
return [serializedModuleContent.path, module];
|
1465
|
-
}));
|
1466
|
-
const apiTreeResponse = {
|
1467
|
-
modules,
|
1468
|
-
git: this.options.git
|
1469
|
-
};
|
1470
|
-
return {
|
1471
|
-
status: 200,
|
1472
|
-
json: apiTreeResponse
|
1473
|
-
};
|
1439
|
+
async getBaseSources() {
|
1440
|
+
return this.initTree().then(result => result.sources);
|
1474
1441
|
}
|
1475
|
-
async
|
1476
|
-
|
1477
|
-
if (result.isErr(ensureRes)) {
|
1478
|
-
return ensureRes.error;
|
1479
|
-
}
|
1480
|
-
return this.validateThenMaybeCommit(rawBody, false, cookies, requestHeaders);
|
1442
|
+
async getSchemas() {
|
1443
|
+
return this.initTree().then(result => result.schemas);
|
1481
1444
|
}
|
1482
|
-
async
|
1483
|
-
|
1484
|
-
|
1485
|
-
|
1486
|
-
|
1487
|
-
|
1488
|
-
|
1489
|
-
|
1490
|
-
return {
|
1491
|
-
status: 400,
|
1492
|
-
json: {
|
1493
|
-
...res.json
|
1494
|
-
}
|
1495
|
-
};
|
1496
|
-
}
|
1497
|
-
return {
|
1498
|
-
status: 200,
|
1499
|
-
json: {
|
1500
|
-
...res.json,
|
1501
|
-
git: this.options.git
|
1502
|
-
}
|
1503
|
-
};
|
1504
|
-
}
|
1505
|
-
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);
|
1506
1453
|
}
|
1507
1454
|
|
1508
|
-
|
1509
|
-
|
1510
|
-
const
|
1511
|
-
|
1512
|
-
|
1513
|
-
|
1514
|
-
|
1515
|
-
|
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
|
+
});
|
1516
1477
|
}
|
1517
|
-
const
|
1518
|
-
|
1519
|
-
if (serializedModuleContent.errors && (serializedModuleContent.errors.fatal || serializedModuleContent.errors.invalidModuleId)) {
|
1520
|
-
return serializedModuleContent;
|
1478
|
+
for (const path in patchesByModule) {
|
1479
|
+
patchesByModule[path].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
1521
1480
|
}
|
1522
|
-
|
1523
|
-
|
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
|
+
};
|
1524
1497
|
}
|
1525
|
-
|
1526
|
-
|
1527
|
-
|
1528
|
-
|
1529
|
-
|
1530
|
-
|
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] = [];
|
1531
1508
|
}
|
1532
|
-
|
1533
|
-
|
1534
|
-
|
1535
|
-
}
|
1536
|
-
|
1537
|
-
|
1538
|
-
|
1539
|
-
|
1540
|
-
|
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`)
|
1541
1522
|
});
|
1542
|
-
|
1543
|
-
|
1544
|
-
|
1545
|
-
|
1546
|
-
|
1547
|
-
|
1548
|
-
|
1549
|
-
|
1550
|
-
|
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);
|
1551
1547
|
}
|
1552
|
-
};
|
1553
|
-
}
|
1554
|
-
}
|
1555
|
-
});
|
1556
|
-
if (validate) {
|
1557
|
-
const validationErrors = await debugTiming("validate:" + moduleId, async () => deserializeSchema(schema).validate(moduleId, source));
|
1558
|
-
if (validationErrors) {
|
1559
|
-
const revalidated = await debugTiming("revalidate image/file:" + moduleId, async () => this.revalidateImageAndFileValidation(validationErrors, fileUpdates, cookies, requestHeaders));
|
1560
|
-
return {
|
1561
|
-
path: moduleId,
|
1562
|
-
schema,
|
1563
|
-
source,
|
1564
|
-
errors: revalidated && {
|
1565
|
-
validation: revalidated
|
1566
1548
|
}
|
1567
|
-
|
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
|
+
}
|
1568
1562
|
}
|
1569
1563
|
}
|
1570
1564
|
return {
|
1571
|
-
|
1572
|
-
|
1573
|
-
source,
|
1574
|
-
errors: false
|
1565
|
+
sources: patchedSources,
|
1566
|
+
errors
|
1575
1567
|
};
|
1576
1568
|
}
|
1577
|
-
|
1578
|
-
//
|
1579
|
-
|
1580
|
-
|
1581
|
-
|
1582
|
-
|
1583
|
-
const
|
1584
|
-
for (const
|
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
|
-
}
|
1615
|
-
// try fetch file directly via http
|
1616
|
-
if (fileRef.startsWith("/public")) {
|
1617
|
-
const host = `${reqHeaders["x-forwarded-proto"]}://${reqHeaders["host"]}`;
|
1618
|
-
const fileUrl = fileRef.slice("/public".length);
|
1619
|
-
const fetchRes = await fetch(new URL(fileUrl, host));
|
1620
|
-
if (fetchRes.status === 200) {
|
1621
|
-
fileBuffer = Buffer.from(await fetchRes.arrayBuffer());
|
1622
|
-
} else {
|
1623
|
-
console.error("Val: unexpected error while fetching image / file:", fileRef, {
|
1624
|
-
error: {
|
1625
|
-
status: fetchRes.status,
|
1626
|
-
url: fetchRes.url
|
1627
|
-
}
|
1628
|
-
});
|
1629
|
-
}
|
1630
|
-
} else {
|
1631
|
-
console.error("Val: unexpected while getting public image / file (file reference did not start with /public)", fileRef);
|
1632
|
-
}
|
1633
|
-
if (!fileBuffer) {
|
1634
|
-
revalidatedValidationErrors[errorSourcePath].push({
|
1635
|
-
message: `Could not read file: ${filePath}`
|
1636
|
-
});
|
1637
|
-
continue;
|
1638
|
-
}
|
1639
|
-
if (error.fixes.some(fix => fix === "image:replace-metadata")) {
|
1640
|
-
expectedMetadata = await extractImageMetadata(filePath, fileBuffer);
|
1641
|
-
} else {
|
1642
|
-
expectedMetadata = await extractFileMetadata(filePath, fileBuffer);
|
1643
|
-
}
|
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);
|
1644
1606
|
}
|
1645
|
-
|
1646
|
-
|
1647
|
-
|
1648
|
-
});
|
1649
|
-
} else {
|
1650
|
-
const actualMetadata = getValidationErrorMetadata(error);
|
1651
|
-
const revalidatedError = validateMetadata(actualMetadata, expectedMetadata);
|
1652
|
-
if (!revalidatedError) {
|
1653
|
-
// no errors anymore:
|
1654
|
-
continue;
|
1655
|
-
}
|
1656
|
-
const errorMsgs = (revalidatedError.globalErrors || []).concat(Object.values(revalidatedError.erroneousMetadata || {})).concat(Object.values(revalidatedError.missingMetadata || []).map(missingKey => {
|
1657
|
-
var _expectedMetadata;
|
1658
|
-
return `Required key: '${missingKey}' is not defined. Should be: '${JSON.stringify((_expectedMetadata = expectedMetadata) === null || _expectedMetadata === void 0 ? void 0 : _expectedMetadata[missingKey])}'`;
|
1659
|
-
}));
|
1660
|
-
revalidatedValidationErrors[errorSourcePath].push(...errorMsgs.map(message => ({
|
1661
|
-
message
|
1662
|
-
})));
|
1607
|
+
const value = validationError.value;
|
1608
|
+
if (isFileSource(value)) {
|
1609
|
+
files[sourcePath] = value;
|
1663
1610
|
}
|
1664
1611
|
} else {
|
1665
|
-
|
1666
|
-
|
1667
|
-
|
1668
|
-
|
1669
|
-
}
|
1670
|
-
}
|
1671
|
-
}
|
1672
|
-
const hasErrors = Object.values(revalidatedValidationErrors).some(errors => errors.length > 0);
|
1673
|
-
if (hasErrors) {
|
1674
|
-
return revalidatedValidationErrors;
|
1675
|
-
}
|
1676
|
-
return hasErrors;
|
1677
|
-
}
|
1678
|
-
sortPatchIds(patchesByModule) {
|
1679
|
-
return Object.values(patchesByModule).flatMap(modulePatches => modulePatches).sort((a, b) => {
|
1680
|
-
return a.created_at.localeCompare(b.created_at);
|
1681
|
-
}).map(patchData => patchData.patch_id);
|
1682
|
-
}
|
1683
|
-
|
1684
|
-
// can be overridden if FS cannot read from static assets / public folder (because of bundlers or what not)
|
1685
|
-
async readStaticBinaryFile(filePath) {
|
1686
|
-
return fs.promises.readFile(filePath);
|
1687
|
-
}
|
1688
|
-
async readPatches(cookies) {
|
1689
|
-
const res = await this.getPatches({},
|
1690
|
-
// {} means no ids, so get all patches
|
1691
|
-
cookies);
|
1692
|
-
if (res.status === 400 || res.status === 401 || res.status === 403 || res.status === 404 || res.status === 500 || res.status === 501) {
|
1693
|
-
return result.err(res);
|
1694
|
-
} else if (res.status === 200 || res.status === 201) {
|
1695
|
-
const patchesByModule = res.json;
|
1696
|
-
const patches = [];
|
1697
|
-
const patchIdsByModuleId = {};
|
1698
|
-
const patchesById = {};
|
1699
|
-
for (const [moduleIdS, modulePatchData] of Object.entries(patchesByModule)) {
|
1700
|
-
const moduleId = moduleIdS;
|
1701
|
-
patchIdsByModuleId[moduleId] = modulePatchData.map(patch => patch.patch_id);
|
1702
|
-
for (const patchData of modulePatchData) {
|
1703
|
-
patches.push([patchData.patch_id, moduleId, patchData.patch]);
|
1704
|
-
patchesById[patchData.patch_id] = patchData.patch;
|
1705
|
-
}
|
1706
|
-
}
|
1707
|
-
const fileUpdates = {};
|
1708
|
-
const sortedPatchIds = this.sortPatchIds(patchesByModule);
|
1709
|
-
for (const sortedPatchId of sortedPatchIds) {
|
1710
|
-
const patchId = sortedPatchId;
|
1711
|
-
for (const op of patchesById[patchId] || []) {
|
1712
|
-
if (op.op === "file") {
|
1713
|
-
const parsedFileOp = z$1.object({
|
1714
|
-
sha256: z$1.string(),
|
1715
|
-
mimeType: z$1.string()
|
1716
|
-
}).safeParse(op.value);
|
1717
|
-
if (!parsedFileOp.success) {
|
1718
|
-
return result.err({
|
1719
|
-
status: 500,
|
1720
|
-
json: {
|
1721
|
-
message: "Unexpected error: file op value must be transformed into object",
|
1722
|
-
details: {
|
1723
|
-
value: "First 200 chars: " + JSON.stringify(op.value).slice(0, 200),
|
1724
|
-
patchId
|
1725
|
-
}
|
1726
|
-
}
|
1727
|
-
});
|
1612
|
+
if (!errors[path]) {
|
1613
|
+
errors[path] = {
|
1614
|
+
validations: {}
|
1615
|
+
};
|
1728
1616
|
}
|
1729
|
-
|
1730
|
-
|
1731
|
-
}
|
1617
|
+
if (!errors[path].validations[sourcePath]) {
|
1618
|
+
errors[path].validations[sourcePath] = [];
|
1619
|
+
}
|
1620
|
+
errors[path].validations[sourcePath].push(validationError);
|
1732
1621
|
}
|
1733
1622
|
}
|
1734
1623
|
}
|
1735
|
-
return result.ok({
|
1736
|
-
patches,
|
1737
|
-
patchIdsByModuleId,
|
1738
|
-
patchesById,
|
1739
|
-
fileUpdates
|
1740
|
-
});
|
1741
|
-
} else {
|
1742
|
-
return result.err({
|
1743
|
-
status: 500,
|
1744
|
-
json: {
|
1745
|
-
message: "Unknown error"
|
1746
|
-
}
|
1747
|
-
});
|
1748
1624
|
}
|
1625
|
+
return {
|
1626
|
+
errors,
|
1627
|
+
files
|
1628
|
+
};
|
1749
1629
|
}
|
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
|
-
|
1786
|
-
if (!modules[moduleId]) {
|
1787
|
-
modules[moduleId] = {
|
1788
|
-
patches: {
|
1789
|
-
applied: []
|
1790
|
-
}
|
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
|
+
}]
|
1791
1666
|
};
|
1792
1667
|
}
|
1793
|
-
|
1794
|
-
|
1795
|
-
|
1796
|
-
|
1797
|
-
}
|
1798
|
-
|
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;
|
1799
1686
|
} else {
|
1800
|
-
|
1687
|
+
metadata = patchFileMetadata.metadata;
|
1801
1688
|
}
|
1802
|
-
}
|
1803
|
-
|
1804
|
-
|
1805
|
-
|
1806
|
-
|
1807
|
-
|
1689
|
+
} else {
|
1690
|
+
const patchFileMetadata = await this.getBinaryFileMetadata(filePath, type);
|
1691
|
+
if (patchFileMetadata.errors) {
|
1692
|
+
metadataErrors = patchFileMetadata.errors;
|
1693
|
+
} else {
|
1694
|
+
metadata = patchFileMetadata.metadata;
|
1808
1695
|
}
|
1809
|
-
};
|
1810
|
-
}
|
1811
|
-
let modules;
|
1812
|
-
if (commit) {
|
1813
|
-
const commitRes = await this.execCommit(patches, cookies);
|
1814
|
-
if (commitRes.status !== 200) {
|
1815
|
-
return commitRes;
|
1816
1696
|
}
|
1817
|
-
|
1818
|
-
|
1819
|
-
|
1820
|
-
|
1821
|
-
|
1822
|
-
|
1823
|
-
|
1824
|
-
|
1825
|
-
|
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
|
+
}
|
1826
1756
|
}
|
1757
|
+
return fieldErrors;
|
1827
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);
|
1828
1767
|
}
|
1829
|
-
|
1830
|
-
|
1831
|
-
|
1832
|
-
|
1833
|
-
|
1834
|
-
|
1835
|
-
|
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
|
+
});
|
1836
1828
|
}
|
1829
|
+
}
|
1830
|
+
return {
|
1831
|
+
path,
|
1832
|
+
appliedPatches,
|
1833
|
+
result: sourceFileText
|
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
|
1837
1843
|
};
|
1838
1844
|
}
|
1839
|
-
|
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);
|
1869
|
+
}
|
1840
1870
|
}
|
1841
|
-
|
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;
|
1842
1899
|
}
|
1843
1900
|
|
1844
|
-
|
1845
|
-
|
1846
|
-
|
1847
|
-
|
1848
|
-
|
1849
|
-
|
1850
|
-
|
1851
|
-
|
1852
|
-
|
1853
|
-
|
1854
|
-
|
1855
|
-
|
1856
|
-
|
1857
|
-
}
|
1858
|
-
|
1859
|
-
|
1860
|
-
|
1861
|
-
|
1862
|
-
|
1863
|
-
|
1864
|
-
|
1865
|
-
|
1866
|
-
|
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
|
+
}
|
1867
1966
|
}
|
1868
|
-
controller.close();
|
1869
1967
|
}
|
1870
|
-
|
1871
|
-
|
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
|
1872
2053
|
}
|
1873
|
-
|
1874
|
-
|
1875
|
-
|
1876
|
-
|
1877
|
-
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;
|
1878
2058
|
}
|
1879
|
-
|
1880
|
-
|
1881
|
-
|
1882
|
-
|
1883
|
-
|
1884
|
-
|
1885
|
-
|
1886
|
-
|
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
|
1887
2103
|
};
|
1888
2104
|
}
|
1889
|
-
|
1890
|
-
|
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
|
+
}
|
1891
2116
|
}
|
1892
|
-
|
2117
|
+
if (errors.length > 0) {
|
2118
|
+
return {
|
2119
|
+
errors
|
2120
|
+
};
|
2121
|
+
}
|
2122
|
+
return {
|
2123
|
+
metadata
|
2124
|
+
};
|
1893
2125
|
}
|
1894
2126
|
const base64DataAttr = "data:";
|
1895
2127
|
function getMimeTypeFromBase64(content) {
|
@@ -1897,688 +2129,1184 @@ function getMimeTypeFromBase64(content) {
|
|
1897
2129
|
const base64Index = content.indexOf(";base64,");
|
1898
2130
|
if (dataIndex > -1 || base64Index > -1) {
|
1899
2131
|
const mimeType = content.slice(dataIndex + base64DataAttr.length, base64Index);
|
1900
|
-
|
2132
|
+
const normalizedMimeType = mimeType === "image/jpg" ? "image/jpeg" : mimeType;
|
2133
|
+
return normalizedMimeType;
|
1901
2134
|
}
|
1902
2135
|
return null;
|
1903
2136
|
}
|
1904
|
-
function bufferFromDataUrl(dataUrl
|
2137
|
+
function bufferFromDataUrl(dataUrl) {
|
1905
2138
|
let base64Data;
|
1906
|
-
|
1907
|
-
|
1908
|
-
|
1909
|
-
base64Data = dataUrl.slice(base64Index + ";base64,".length);
|
1910
|
-
}
|
1911
|
-
} else {
|
1912
|
-
const dataUrlEncodingHeader = `${base64DataAttr}${contentType};base64,`;
|
1913
|
-
if (dataUrl.slice(0, dataUrlEncodingHeader.length) === dataUrlEncodingHeader) {
|
1914
|
-
base64Data = dataUrl.slice(dataUrlEncodingHeader.length);
|
1915
|
-
}
|
2139
|
+
const base64Index = dataUrl.indexOf(";base64,");
|
2140
|
+
if (base64Index > -1) {
|
2141
|
+
base64Data = dataUrl.slice(base64Index + ";base64,".length);
|
1916
2142
|
}
|
1917
2143
|
if (base64Data) {
|
1918
2144
|
return Buffer.from(base64Data, "base64" // TODO: why does it not work with base64url?
|
1919
2145
|
);
|
1920
2146
|
}
|
1921
2147
|
}
|
1922
|
-
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
1923
|
-
const COMMON_MIME_TYPES = {
|
1924
|
-
aac: "audio/aac",
|
1925
|
-
abw: "application/x-abiword",
|
1926
|
-
arc: "application/x-freearc",
|
1927
|
-
avif: "image/avif",
|
1928
|
-
avi: "video/x-msvideo",
|
1929
|
-
azw: "application/vnd.amazon.ebook",
|
1930
|
-
bin: "application/octet-stream",
|
1931
|
-
bmp: "image/bmp",
|
1932
|
-
bz: "application/x-bzip",
|
1933
|
-
bz2: "application/x-bzip2",
|
1934
|
-
cda: "application/x-cdf",
|
1935
|
-
csh: "application/x-csh",
|
1936
|
-
css: "text/css",
|
1937
|
-
csv: "text/csv",
|
1938
|
-
doc: "application/msword",
|
1939
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
1940
|
-
eot: "application/vnd.ms-fontobject",
|
1941
|
-
epub: "application/epub+zip",
|
1942
|
-
gz: "application/gzip",
|
1943
|
-
gif: "image/gif",
|
1944
|
-
htm: "text/html",
|
1945
|
-
html: "text/html",
|
1946
|
-
ico: "image/vnd.microsoft.icon",
|
1947
|
-
ics: "text/calendar",
|
1948
|
-
jar: "application/java-archive",
|
1949
|
-
jpeg: "image/jpeg",
|
1950
|
-
jpg: "image/jpeg",
|
1951
|
-
js: "text/javascript",
|
1952
|
-
json: "application/json",
|
1953
|
-
jsonld: "application/ld+json",
|
1954
|
-
mid: "audio/midi",
|
1955
|
-
midi: "audio/midi",
|
1956
|
-
mjs: "text/javascript",
|
1957
|
-
mp3: "audio/mpeg",
|
1958
|
-
mp4: "video/mp4",
|
1959
|
-
mpeg: "video/mpeg",
|
1960
|
-
mpkg: "application/vnd.apple.installer+xml",
|
1961
|
-
odp: "application/vnd.oasis.opendocument.presentation",
|
1962
|
-
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
1963
|
-
odt: "application/vnd.oasis.opendocument.text",
|
1964
|
-
oga: "audio/ogg",
|
1965
|
-
ogv: "video/ogg",
|
1966
|
-
ogx: "application/ogg",
|
1967
|
-
opus: "audio/opus",
|
1968
|
-
otf: "font/otf",
|
1969
|
-
png: "image/png",
|
1970
|
-
pdf: "application/pdf",
|
1971
|
-
php: "application/x-httpd-php",
|
1972
|
-
ppt: "application/vnd.ms-powerpoint",
|
1973
|
-
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
1974
|
-
rar: "application/vnd.rar",
|
1975
|
-
rtf: "application/rtf",
|
1976
|
-
sh: "application/x-sh",
|
1977
|
-
svg: "image/svg+xml",
|
1978
|
-
tar: "application/x-tar",
|
1979
|
-
tif: "image/tiff",
|
1980
|
-
tiff: "image/tiff",
|
1981
|
-
ts: "video/mp2t",
|
1982
|
-
ttf: "font/ttf",
|
1983
|
-
txt: "text/plain",
|
1984
|
-
vsd: "application/vnd.visio",
|
1985
|
-
wav: "audio/wav",
|
1986
|
-
weba: "audio/webm",
|
1987
|
-
webm: "video/webm",
|
1988
|
-
webp: "image/webp",
|
1989
|
-
woff: "font/woff",
|
1990
|
-
woff2: "font/woff2",
|
1991
|
-
xhtml: "application/xhtml+xml",
|
1992
|
-
xls: "application/vnd.ms-excel",
|
1993
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
1994
|
-
xml: "application/xml",
|
1995
|
-
xul: "application/vnd.mozilla.xul+xml",
|
1996
|
-
zip: "application/zip",
|
1997
|
-
"3gp": "video/3gpp; audio/3gpp if it doesn't contain video",
|
1998
|
-
"3g2": "video/3gpp2; audio/3gpp2 if it doesn't contain video",
|
1999
|
-
"7z": "application/x-7z-compressed"
|
2000
|
-
};
|
2001
|
-
function guessMimeTypeFromPath(filePath) {
|
2002
|
-
const fileExt = filePath.split(".").pop();
|
2003
|
-
if (fileExt) {
|
2004
|
-
return COMMON_MIME_TYPES[fileExt.toLowerCase()] || null;
|
2005
|
-
}
|
2006
|
-
return null;
|
2007
|
-
}
|
2008
|
-
function isCachedPatchFileOp(op) {
|
2009
|
-
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");
|
2010
|
-
}
|
2011
|
-
async function debugTiming(id, fn) {
|
2012
|
-
if (process.env["VAL_DEBUG_TIMING"] === "true") {
|
2013
|
-
const start = Date.now();
|
2014
|
-
const r = await fn();
|
2015
|
-
console.log(`Timing: ${id} took: ${Date.now() - start}ms (${new Date().toISOString()})`);
|
2016
|
-
return r;
|
2017
|
-
} else {
|
2018
|
-
return fn();
|
2019
|
-
}
|
2020
|
-
}
|
2021
2148
|
|
2022
|
-
const
|
2023
|
-
|
2024
|
-
|
2025
|
-
|
2026
|
-
|
2027
|
-
|
2028
|
-
|
2029
|
-
|
2030
|
-
|
2031
|
-
|
2032
|
-
|
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
|
2033
2235
|
}
|
2034
|
-
async
|
2035
|
-
|
2036
|
-
|
2037
|
-
|
2038
|
-
|
2039
|
-
|
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(", "));
|
2040
2248
|
}
|
2041
|
-
|
2042
|
-
|
2043
|
-
async deletePatches(query) {
|
2044
|
-
const deletedPatches = [];
|
2045
|
-
for (const patchId of query.id ?? []) {
|
2046
|
-
const rawPatchFileContent = this.host.readFile(this.getPatchFilePath(patchId));
|
2047
|
-
if (!rawPatchFileContent) {
|
2048
|
-
console.warn("Val: Patch not found", patchId);
|
2249
|
+
const patchId = patchIdNum.toString();
|
2250
|
+
if (includes && !includes.includes(patchId)) {
|
2049
2251
|
continue;
|
2050
2252
|
}
|
2051
|
-
const
|
2052
|
-
|
2053
|
-
|
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))) {
|
2054
2292
|
continue;
|
2055
2293
|
}
|
2056
|
-
|
2057
|
-
|
2058
|
-
|
2059
|
-
|
2060
|
-
|
2061
|
-
|
2062
|
-
|
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;
|
2063
2303
|
}
|
2064
|
-
|
2065
|
-
|
2304
|
+
}
|
2305
|
+
if (errors && Object.keys(errors).length > 0) {
|
2306
|
+
return {
|
2307
|
+
patches,
|
2308
|
+
errors
|
2309
|
+
};
|
2066
2310
|
}
|
2067
2311
|
return {
|
2068
|
-
|
2069
|
-
json: deletedPatches
|
2312
|
+
patches
|
2070
2313
|
};
|
2071
2314
|
}
|
2072
|
-
|
2073
|
-
|
2074
|
-
|
2315
|
+
|
2316
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2317
|
+
parseJsonFile(filePath, parser) {
|
2318
|
+
if (!this.host.fileExists(filePath)) {
|
2075
2319
|
return {
|
2076
|
-
|
2077
|
-
|
2078
|
-
|
2079
|
-
details: patches.error.issues
|
2320
|
+
error: {
|
2321
|
+
message: `File not found: ${filePath}`,
|
2322
|
+
filePath
|
2080
2323
|
}
|
2081
2324
|
};
|
2082
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
|
2351
|
+
}
|
2352
|
+
};
|
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) {
|
2083
2391
|
let fileId = Date.now();
|
2084
|
-
|
2085
|
-
|
2086
|
-
|
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
|
+
};
|
2087
2422
|
}
|
2088
|
-
|
2089
|
-
|
2090
|
-
const
|
2091
|
-
|
2092
|
-
|
2093
|
-
|
2094
|
-
|
2095
|
-
|
2096
|
-
|
2097
|
-
|
2098
|
-
|
2099
|
-
|
2100
|
-
|
2101
|
-
|
2102
|
-
|
2103
|
-
|
2104
|
-
|
2105
|
-
|
2106
|
-
|
2107
|
-
|
2108
|
-
|
2109
|
-
|
2110
|
-
|
2111
|
-
|
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
|
2112
2449
|
}
|
2113
|
-
|
2114
|
-
|
2115
|
-
|
2116
|
-
|
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)
|
2117
2468
|
}
|
2118
|
-
|
2119
|
-
|
2120
|
-
|
2121
|
-
|
2122
|
-
|
2123
|
-
|
2124
|
-
|
2125
|
-
|
2126
|
-
|
2127
|
-
|
2128
|
-
|
2129
|
-
|
2130
|
-
|
2131
|
-
|
2132
|
-
|
2133
|
-
|
2134
|
-
|
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"
|
2135
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
|
+
});
|
2136
2517
|
}
|
2137
2518
|
}
|
2138
|
-
|
2519
|
+
if (fieldErrors.length > 0) {
|
2520
|
+
return {
|
2521
|
+
errors: fieldErrors
|
2522
|
+
};
|
2523
|
+
}
|
2139
2524
|
return {
|
2140
|
-
|
2141
|
-
|
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}`
|
2142
2812
|
};
|
2813
|
+
this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
|
2143
2814
|
}
|
2144
|
-
async
|
2145
|
-
|
2815
|
+
async onInit() {
|
2816
|
+
// TODO: unused for now. Implement or remove
|
2146
2817
|
}
|
2147
|
-
async
|
2148
|
-
|
2149
|
-
|
2150
|
-
|
2151
|
-
|
2152
|
-
|
2153
|
-
|
2154
|
-
|
2155
|
-
|
2156
|
-
|
2157
|
-
|
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
|
+
};
|
2158
2856
|
}
|
2159
|
-
const metadata = JSON.parse(metadataFileContent);
|
2160
2857
|
return {
|
2161
|
-
|
2162
|
-
|
2163
|
-
|
2164
|
-
|
2165
|
-
"Cache-Control": "public, max-age=31536000, immutable"
|
2166
|
-
},
|
2167
|
-
body: bufferToReadableStream(fileContent)
|
2858
|
+
patches,
|
2859
|
+
error: {
|
2860
|
+
message: `Could not parse get patches response. Error: ${fromError(parsed.error)}`
|
2861
|
+
}
|
2168
2862
|
};
|
2169
2863
|
}
|
2170
|
-
}
|
2171
|
-
const buffer = await this.readStaticBinaryFile(path__default.join(this.cwd, filePath));
|
2172
|
-
const mimeType = guessMimeTypeFromPath(filePath) || "application/octet-stream";
|
2173
|
-
if (!buffer) {
|
2174
2864
|
return {
|
2175
|
-
|
2176
|
-
|
2177
|
-
message: "
|
2865
|
+
patches,
|
2866
|
+
error: {
|
2867
|
+
message: "Could not get patches. HTTP error: " + res.status + " " + res.statusText
|
2178
2868
|
}
|
2179
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(",")));
|
2180
2877
|
}
|
2181
|
-
|
2182
|
-
|
2183
|
-
|
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
|
+
}
|
2184
2906
|
return {
|
2185
|
-
|
2186
|
-
|
2187
|
-
|
2188
|
-
|
2189
|
-
"Cache-Control": "public, max-age=31536000, immutable"
|
2190
|
-
},
|
2191
|
-
body: bufferToReadableStream(buffer)
|
2907
|
+
patches,
|
2908
|
+
error: {
|
2909
|
+
message: `Could not parse search patches response. Error: ${fromError(parsed.error)}`
|
2910
|
+
}
|
2192
2911
|
};
|
2193
2912
|
}
|
2194
|
-
|
2195
|
-
|
2196
|
-
|
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",
|
2197
2924
|
headers: {
|
2198
|
-
|
2199
|
-
"Content-
|
2925
|
+
...this.authHeaders,
|
2926
|
+
"Content-Type": "application/json"
|
2200
2927
|
},
|
2201
|
-
body:
|
2202
|
-
|
2203
|
-
|
2204
|
-
|
2205
|
-
|
2206
|
-
|
2207
|
-
|
2208
|
-
|
2209
|
-
|
2210
|
-
|
2211
|
-
|
2212
|
-
|
2213
|
-
}
|
2214
|
-
const res = {};
|
2215
|
-
const sortedPatchIds = files.map(file => parseInt(path__default.basename(file), 10)).sort();
|
2216
|
-
for (const patchIdStr of sortedPatchIds) {
|
2217
|
-
const patchId = patchIdStr.toString();
|
2218
|
-
if (query.id && query.id.length > 0 && !query.id.includes(patchId)) {
|
2219
|
-
continue;
|
2220
|
-
}
|
2221
|
-
try {
|
2222
|
-
const currentParsedPatches = z$1.record(Patch).safeParse(JSON.parse(this.host.readFile(path__default.join(this.patchesRootPath, LocalValServer.PATCHES_DIR, `${patchId}`)) || ""));
|
2223
|
-
if (!currentParsedPatches.success) {
|
2224
|
-
const msg = "Unexpected error reading patch. Patch did not parse correctly. Is there a mismatch in Val versions? Perhaps Val is misconfigured?";
|
2225
|
-
console.error(`Val: ${msg}`, {
|
2226
|
-
patchId,
|
2227
|
-
error: currentParsedPatches.error
|
2228
|
-
});
|
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) {
|
2229
2940
|
return {
|
2230
|
-
|
2231
|
-
json: {
|
2232
|
-
message: msg,
|
2233
|
-
details: {
|
2234
|
-
patchId,
|
2235
|
-
error: currentParsedPatches.error
|
2236
|
-
}
|
2237
|
-
}
|
2941
|
+
patchId: parsed.data.patchId
|
2238
2942
|
};
|
2239
2943
|
}
|
2240
|
-
const createdAt = patchId;
|
2241
|
-
for (const moduleIdStr in currentParsedPatches.data) {
|
2242
|
-
const moduleId = moduleIdStr;
|
2243
|
-
if (!res[moduleId]) {
|
2244
|
-
res[moduleId] = [];
|
2245
|
-
}
|
2246
|
-
res[moduleId].push({
|
2247
|
-
patch: currentParsedPatches.data[moduleId],
|
2248
|
-
patch_id: patchId,
|
2249
|
-
created_at: new Date(Number(createdAt)).toISOString()
|
2250
|
-
});
|
2251
|
-
}
|
2252
|
-
} catch (err) {
|
2253
|
-
const msg = `Unexpected error while reading patch file. The cache may be corrupted or Val may be misconfigured. Try deleting the cache directory.`;
|
2254
|
-
console.error(`Val: ${msg}`, {
|
2255
|
-
patchId,
|
2256
|
-
error: err,
|
2257
|
-
dir: this.patchesRootPath
|
2258
|
-
});
|
2259
2944
|
return {
|
2260
|
-
|
2261
|
-
|
2262
|
-
message: msg,
|
2263
|
-
details: {
|
2264
|
-
patchId,
|
2265
|
-
error: err === null || err === void 0 ? void 0 : err.toString()
|
2266
|
-
}
|
2945
|
+
error: {
|
2946
|
+
message: `Could not parse save patch response. Error: ${fromError(parsed.error)}`
|
2267
2947
|
}
|
2268
2948
|
};
|
2269
2949
|
}
|
2270
|
-
|
2271
|
-
|
2272
|
-
|
2273
|
-
|
2274
|
-
|
2275
|
-
|
2276
|
-
|
2277
|
-
|
2278
|
-
|
2279
|
-
|
2280
|
-
|
2281
|
-
|
2282
|
-
getPatchFilePath(patchId) {
|
2283
|
-
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
|
+
});
|
2284
2962
|
}
|
2285
|
-
|
2286
|
-
return {
|
2287
|
-
|
2288
|
-
|
2289
|
-
|
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
|
+
};
|
2290
2990
|
}
|
2291
|
-
|
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
|
+
};
|
3002
|
+
});
|
2292
3003
|
}
|
2293
|
-
async
|
2294
|
-
|
2295
|
-
|
2296
|
-
|
2297
|
-
|
2298
|
-
|
2299
|
-
|
2300
|
-
|
2301
|
-
|
2302
|
-
|
2303
|
-
|
2304
|
-
|
2305
|
-
|
2306
|
-
|
2307
|
-
|
2308
|
-
|
2309
|
-
|
2310
|
-
|
2311
|
-
|
2312
|
-
|
2313
|
-
|
2314
|
-
const
|
2315
|
-
if (
|
2316
|
-
|
2317
|
-
throw Error(`Module defined at pos: ${i} is missing schema`);
|
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;
|
2318
3028
|
}
|
2319
3029
|
return {
|
2320
|
-
|
2321
|
-
|
2322
|
-
|
2323
|
-
//valModule[GetSchema]?.validate(path, source),
|
2324
|
-
schema: schema
|
3030
|
+
error: {
|
3031
|
+
message: `Could not parse file response. Error: ${fromError(parsedFileResponse.error)}`
|
3032
|
+
}
|
2325
3033
|
};
|
2326
|
-
});
|
2327
|
-
}));
|
2328
|
-
}
|
2329
|
-
getModule(moduleId) {
|
2330
|
-
// TODO: do not get all modules - we only should only get the ones we need
|
2331
|
-
return this.getSerializedModules().then(all => {
|
2332
|
-
const found = all.find(valModule => valModule.path === moduleId);
|
2333
|
-
if (!found) {
|
2334
|
-
throw Error(`Module ${moduleId} not found`);
|
2335
3034
|
}
|
2336
|
-
return
|
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
|
+
};
|
2337
3046
|
});
|
2338
3047
|
}
|
2339
|
-
async
|
2340
|
-
const
|
2341
|
-
path
|
2342
|
-
|
2343
|
-
|
2344
|
-
|
2345
|
-
|
2346
|
-
|
2347
|
-
|
2348
|
-
|
2349
|
-
|
2350
|
-
|
2351
|
-
|
2352
|
-
|
2353
|
-
|
2354
|
-
|
2355
|
-
|
2356
|
-
// Other things we could do would be to patch in a temp directory and ONLY when all patches are applied we move back in.
|
2357
|
-
// This would improve reliability
|
2358
|
-
this.host.rmFile(this.getPatchFilePath(patchId));
|
2359
|
-
await this.options.service.patch(moduleId, patch);
|
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
|
+
};
|
2360
3065
|
}
|
2361
3066
|
return {
|
2362
|
-
|
2363
|
-
json: await this.getPatchedModules(patches)
|
3067
|
+
data: Buffer.from(file.value, "base64").toString("utf-8")
|
2364
3068
|
};
|
2365
3069
|
}
|
2366
|
-
|
2367
|
-
|
2368
|
-
|
2369
|
-
|
2370
|
-
|
2371
|
-
|
2372
|
-
|
2373
|
-
|
2374
|
-
|
2375
|
-
async logout() {
|
2376
|
-
return this.badRequest();
|
2377
|
-
}
|
2378
|
-
}
|
2379
|
-
|
2380
|
-
function decodeJwt(token, secretKey) {
|
2381
|
-
const [headerBase64, payloadBase64, signatureBase64, ...rest] = token.split(".");
|
2382
|
-
if (!headerBase64 || !payloadBase64 || !signatureBase64 || rest.length > 0) {
|
2383
|
-
console.debug("Invalid JWT: format is not exactly {header}.{payload}.{signature}", token);
|
2384
|
-
return null;
|
2385
|
-
}
|
2386
|
-
try {
|
2387
|
-
const parsedHeader = JSON.parse(Buffer.from(headerBase64, "base64").toString("utf8"));
|
2388
|
-
const headerVerification = JwtHeaderSchema.safeParse(parsedHeader);
|
2389
|
-
if (!headerVerification.success) {
|
2390
|
-
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) {
|
2391
3079
|
return null;
|
2392
3080
|
}
|
2393
|
-
|
2394
|
-
|
3081
|
+
const file = filesRes.files.find(f => f.filePath === filePath);
|
3082
|
+
if (!file) {
|
2395
3083
|
return null;
|
2396
3084
|
}
|
2397
|
-
|
2398
|
-
|
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) {
|
2399
3094
|
return null;
|
2400
3095
|
}
|
2401
|
-
|
2402
|
-
|
2403
|
-
return null;
|
2404
|
-
}
|
2405
|
-
if (secretKey) {
|
2406
|
-
const signature = crypto.createHmac("sha256", secretKey).update(`${headerBase64}.${payloadBase64}`).digest("base64");
|
2407
|
-
if (signature !== signatureBase64) {
|
2408
|
-
console.debug("Invalid JWT: invalid signature");
|
3096
|
+
const file = filesRes.files.find(f => f.filePath === filePath);
|
3097
|
+
if (!file) {
|
2409
3098
|
return null;
|
2410
3099
|
}
|
3100
|
+
return Buffer.from(file.value, "base64");
|
2411
3101
|
}
|
2412
|
-
|
2413
|
-
const
|
2414
|
-
|
2415
|
-
|
2416
|
-
|
2417
|
-
|
2418
|
-
|
2419
|
-
|
2420
|
-
function getExpire() {
|
2421
|
-
return Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 4; // 4 days
|
2422
|
-
}
|
2423
|
-
const JwtHeaderSchema = z$1.object({
|
2424
|
-
alg: z$1.literal("HS256"),
|
2425
|
-
typ: z$1.literal("JWT")
|
2426
|
-
});
|
2427
|
-
const jwtHeader = {
|
2428
|
-
alg: "HS256",
|
2429
|
-
typ: "JWT"
|
2430
|
-
};
|
2431
|
-
const jwtHeaderBase64 = Buffer.from(JSON.stringify(jwtHeader)).toString("base64");
|
2432
|
-
function encodeJwt(payload, sessionKey) {
|
2433
|
-
// NOTE: this is only used for authentication, not for authorization (i.e. what a user can do) - this is handled when actually doing operations
|
2434
|
-
const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString("base64");
|
2435
|
-
return `${jwtHeaderBase64}.${payloadBase64}.${crypto.createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
|
2436
|
-
}
|
2437
|
-
|
2438
|
-
class ProxyValServer extends ValServer {
|
2439
|
-
moduleCache = null;
|
2440
|
-
constructor(cwd, valModules, options, apiOptions, callbacks) {
|
2441
|
-
super(cwd, valModules, options, callbacks);
|
2442
|
-
this.cwd = cwd;
|
2443
|
-
this.valModules = valModules;
|
2444
|
-
this.options = options;
|
2445
|
-
this.apiOptions = apiOptions;
|
2446
|
-
this.callbacks = callbacks;
|
2447
|
-
this.moduleCache = null;
|
2448
|
-
}
|
2449
|
-
|
2450
|
-
// TODO: restructure this
|
2451
|
-
|
2452
|
-
getSerializedModules() {
|
2453
|
-
return Promise.all(this.valModules.modules.map(({
|
2454
|
-
def
|
2455
|
-
}, i) => {
|
2456
|
-
return def().then(({
|
2457
|
-
default: valModule
|
2458
|
-
}) => {
|
2459
|
-
var _Internal$getSchema;
|
2460
|
-
const path = Internal.getValPath(valModule);
|
2461
|
-
if (!path) {
|
2462
|
-
throw Error(`Module defined at pos: ${i} is missing path`);
|
2463
|
-
}
|
2464
|
-
const source = Internal.getSource(valModule);
|
2465
|
-
if (!source) {
|
2466
|
-
// TODO
|
2467
|
-
throw Error(`Module defined at pos: ${i} is missing source`);
|
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"
|
2468
3110
|
}
|
2469
|
-
|
2470
|
-
|
2471
|
-
|
2472
|
-
|
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
|
+
};
|
2473
3119
|
}
|
2474
3120
|
return {
|
2475
|
-
|
2476
|
-
|
2477
|
-
|
2478
|
-
|
2479
|
-
schema: schema
|
3121
|
+
errors: [{
|
3122
|
+
message: `Could not parse metadata response. Error: ${fromError(parsed.error)}`,
|
3123
|
+
filePath
|
3124
|
+
}]
|
2480
3125
|
};
|
2481
|
-
});
|
2482
|
-
}));
|
2483
|
-
}
|
2484
|
-
getModule(moduleId) {
|
2485
|
-
// TODO: do not get all modules - we only should only get the ones we need
|
2486
|
-
return this.getSerializedModules().then(all => {
|
2487
|
-
const found = all.find(valModule => valModule.path === moduleId);
|
2488
|
-
if (!found) {
|
2489
|
-
throw Error(`Module ${moduleId} not found`);
|
2490
3126
|
}
|
2491
|
-
return
|
2492
|
-
|
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
|
+
};
|
3139
|
+
}
|
2493
3140
|
}
|
2494
|
-
async
|
2495
|
-
|
2496
|
-
|
2497
|
-
|
2498
|
-
|
2499
|
-
|
2500
|
-
|
2501
|
-
|
2502
|
-
|
2503
|
-
|
2504
|
-
|
2505
|
-
return
|
2506
|
-
|
2507
|
-
|
2508
|
-
|
2509
|
-
|
2510
|
-
}
|
2511
|
-
|
2512
|
-
|
2513
|
-
|
2514
|
-
|
2515
|
-
|
2516
|
-
|
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
|
+
};
|
3158
|
+
}
|
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;
|
2517
3176
|
}
|
2518
|
-
|
2519
|
-
|
2520
|
-
|
2521
|
-
|
2522
|
-
|
2523
|
-
|
2524
|
-
|
2525
|
-
|
2526
|
-
|
2527
|
-
|
2528
|
-
const patchIds = patches.map(([patchId]) => patchId);
|
2529
|
-
const fetchRes = await fetch(url, {
|
2530
|
-
method: "POST",
|
2531
|
-
headers: getAuthHeaders(token, "application/json"),
|
2532
|
-
body: JSON.stringify({
|
2533
|
-
patchIds
|
2534
|
-
})
|
2535
|
-
});
|
2536
|
-
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
|
+
}
|
2537
3187
|
return {
|
2538
|
-
|
2539
|
-
|
3188
|
+
error: {
|
3189
|
+
message: `Could not parse delete patches response. Error: ${fromError(parsed.error)}`
|
3190
|
+
}
|
2540
3191
|
};
|
2541
|
-
} else {
|
2542
|
-
return createJsonError(fetchRes);
|
2543
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
|
+
};
|
2544
3204
|
});
|
2545
3205
|
}
|
2546
|
-
async
|
2547
|
-
|
2548
|
-
|
2549
|
-
|
2550
|
-
|
2551
|
-
|
2552
|
-
|
2553
|
-
|
2554
|
-
|
2555
|
-
|
2556
|
-
|
2557
|
-
|
2558
|
-
|
2559
|
-
|
2560
|
-
|
2561
|
-
|
2562
|
-
|
3206
|
+
async commit(prepared, message, committer, newBranch) {
|
3207
|
+
try {
|
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
|
+
})
|
2563
3227
|
});
|
2564
|
-
|
2565
|
-
|
2566
|
-
|
2567
|
-
|
2568
|
-
|
3228
|
+
if (res.ok) {
|
3229
|
+
const parsed = CommitResponse.safeParse(await res.json());
|
3230
|
+
if (parsed.success) {
|
3231
|
+
return {
|
3232
|
+
updatedFiles: parsed.data.updatedFiles,
|
3233
|
+
commit: parsed.data.commit,
|
3234
|
+
branch: parsed.data.branch
|
3235
|
+
};
|
3236
|
+
}
|
2569
3237
|
return {
|
2570
|
-
|
3238
|
+
error: {
|
3239
|
+
message: `Could not parse commit response. Error: ${fromError(parsed.error)}`
|
3240
|
+
}
|
2571
3241
|
};
|
2572
3242
|
}
|
2573
|
-
|
2574
|
-
|
2575
|
-
|
3243
|
+
return {
|
3244
|
+
error: {
|
3245
|
+
message: "Could not commit. HTTP error: " + res.status + " " + res.statusText
|
3246
|
+
}
|
3247
|
+
};
|
3248
|
+
} catch (err) {
|
3249
|
+
return {
|
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"}`
|
3252
|
+
}
|
3253
|
+
};
|
3254
|
+
}
|
3255
|
+
}
|
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
|
3272
|
+
});
|
2576
3273
|
} else {
|
2577
|
-
|
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));
|
2578
3276
|
}
|
2579
3277
|
}
|
2580
|
-
/* Auth endpoints */
|
2581
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
|
+
}
|
2582
3310
|
async authorize(query) {
|
2583
3311
|
if (typeof query.redirect_to !== "string") {
|
2584
3312
|
return {
|
@@ -2610,6 +3338,28 @@ class ProxyValServer extends ValServer {
|
|
2610
3338
|
};
|
2611
3339
|
}
|
2612
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
|
+
}
|
2613
3363
|
const {
|
2614
3364
|
success: callbackReqSuccess,
|
2615
3365
|
error: callbackReqError
|
@@ -2638,10 +3388,22 @@ class ProxyValServer extends ValServer {
|
|
2638
3388
|
};
|
2639
3389
|
}
|
2640
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
|
+
}
|
2641
3403
|
const cookie = encodeJwt({
|
2642
3404
|
...data,
|
2643
3405
|
exp // this is the client side exp
|
2644
|
-
},
|
3406
|
+
}, valSecret);
|
2645
3407
|
return {
|
2646
3408
|
status: 302,
|
2647
3409
|
cookies: {
|
@@ -2663,7 +3425,7 @@ class ProxyValServer extends ValServer {
|
|
2663
3425
|
redirectTo: callbackReqSuccess.redirect_uri || "/"
|
2664
3426
|
};
|
2665
3427
|
}
|
2666
|
-
async
|
3428
|
+
async log() {
|
2667
3429
|
return {
|
2668
3430
|
status: 200,
|
2669
3431
|
cookies: {
|
@@ -2677,8 +3439,41 @@ class ProxyValServer extends ValServer {
|
|
2677
3439
|
};
|
2678
3440
|
}
|
2679
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
|
+
}
|
2680
3467
|
return withAuth(this.options.valSecret, cookies, "session", async data => {
|
2681
|
-
|
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);
|
2682
3477
|
const fetchRes = await fetch(url, {
|
2683
3478
|
headers: getAuthHeaders(data.token, "application/json")
|
2684
3479
|
});
|
@@ -2703,8 +3498,17 @@ class ProxyValServer extends ValServer {
|
|
2703
3498
|
});
|
2704
3499
|
}
|
2705
3500
|
async consumeCode(code) {
|
2706
|
-
|
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);
|
2707
3508
|
url.searchParams.set("code", encodeURIComponent(code));
|
3509
|
+
if (!this.options.apiKey) {
|
3510
|
+
return null;
|
3511
|
+
}
|
2708
3512
|
return fetch(url, {
|
2709
3513
|
method: "POST",
|
2710
3514
|
headers: getAuthHeaders(this.options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
|
@@ -2728,186 +3532,453 @@ class ProxyValServer extends ValServer {
|
|
2728
3532
|
return null;
|
2729
3533
|
});
|
2730
3534
|
}
|
2731
|
-
getAuthorizeUrl(
|
2732
|
-
|
2733
|
-
|
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`));
|
2734
3544
|
url.searchParams.set("state", token);
|
2735
3545
|
return url.toString();
|
2736
3546
|
}
|
2737
3547
|
getAppErrorUrl(error) {
|
2738
|
-
|
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);
|
2739
3555
|
url.searchParams.set("error", encodeURIComponent(error));
|
2740
3556
|
return url.toString();
|
2741
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
|
+
}
|
2742
3625
|
|
2743
|
-
|
2744
|
-
async
|
2745
|
-
|
2746
|
-
|
2747
|
-
|
2748
|
-
|
2749
|
-
|
2750
|
-
|
2751
|
-
|
2752
|
-
|
2753
|
-
|
2754
|
-
|
2755
|
-
|
2756
|
-
|
2757
|
-
|
2758
|
-
|
2759
|
-
|
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
|
2760
3663
|
});
|
2761
|
-
|
2762
|
-
|
2763
|
-
|
2764
|
-
|
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
|
2765
3837
|
};
|
2766
|
-
} else {
|
2767
|
-
return createJsonError(fetchRes);
|
2768
3838
|
}
|
2769
|
-
|
2770
|
-
|
2771
|
-
|
2772
|
-
|
2773
|
-
|
2774
|
-
|
2775
|
-
|
2776
|
-
|
2777
|
-
|
2778
|
-
|
2779
|
-
|
2780
|
-
|
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
|
2781
3857
|
}
|
2782
3858
|
};
|
2783
3859
|
}
|
2784
|
-
|
2785
|
-
|
2786
|
-
|
2787
|
-
|
2788
|
-
|
2789
|
-
|
2790
|
-
|
2791
|
-
|
2792
|
-
|
2793
|
-
|
2794
|
-
|
2795
|
-
|
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
|
2796
3885
|
};
|
2797
|
-
} else {
|
2798
|
-
return createJsonError(fetchRes);
|
2799
3886
|
}
|
2800
|
-
}
|
3887
|
+
}
|
3888
|
+
return {
|
3889
|
+
status: 200,
|
3890
|
+
json: {
|
3891
|
+
schemaSha,
|
3892
|
+
modules
|
3893
|
+
}
|
3894
|
+
};
|
2801
3895
|
}
|
2802
|
-
async
|
2803
|
-
const
|
2804
|
-
if (
|
3896
|
+
async postSave(body, cookies) {
|
3897
|
+
const auth = this.getAuth(cookies);
|
3898
|
+
if (auth.error) {
|
2805
3899
|
return {
|
2806
3900
|
status: 401,
|
2807
3901
|
json: {
|
2808
|
-
message:
|
3902
|
+
message: auth.error
|
2809
3903
|
}
|
2810
3904
|
};
|
2811
3905
|
}
|
2812
|
-
const
|
2813
|
-
|
3906
|
+
const PostSaveBody = z.object({
|
3907
|
+
patchIds: z.array(z.string().refine(id => true // TODO:
|
3908
|
+
))
|
2814
3909
|
});
|
2815
|
-
|
2816
|
-
|
2817
|
-
|
2818
|
-
|
2819
|
-
|
2820
|
-
|
2821
|
-
|
2822
|
-
|
2823
|
-
|
2824
|
-
|
2825
|
-
|
2826
|
-
|
2827
|
-
|
2828
|
-
|
2829
|
-
|
2830
|
-
|
2831
|
-
|
2832
|
-
|
2833
|
-
|
2834
|
-
|
2835
|
-
|
2836
|
-
|
2837
|
-
|
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);
|
2838
3938
|
return {
|
2839
|
-
status:
|
2840
|
-
json:
|
3939
|
+
status: 200,
|
3940
|
+
json: {} // TODO:
|
2841
3941
|
};
|
2842
|
-
} else {
|
2843
|
-
return createJsonError(fetchRes);
|
2844
|
-
}
|
2845
|
-
});
|
2846
|
-
}
|
2847
|
-
async getMetadata(filePath, sha256) {
|
2848
|
-
const url = new URL(`/v1/metadata/${this.options.remote}${filePath}?commit=${this.options.git.commit}${sha256 ? `&sha256=${sha256}` : ""}`, this.options.valContentUrl);
|
2849
|
-
const fetchRes = await fetch(url, {
|
2850
|
-
headers: {
|
2851
|
-
Authorization: `Bearer ${this.options.apiKey}`
|
2852
|
-
}
|
2853
|
-
});
|
2854
|
-
if (fetchRes.status === 200) {
|
2855
|
-
const json = await fetchRes.json();
|
2856
|
-
if (json.type === "file") {
|
2857
|
-
return json;
|
2858
|
-
} else if (json.type === "image") {
|
2859
|
-
return json;
|
2860
3942
|
}
|
3943
|
+
return {
|
3944
|
+
status: 401,
|
3945
|
+
json: {
|
3946
|
+
message: "Unauthorized"
|
3947
|
+
}
|
3948
|
+
};
|
3949
|
+
} else {
|
3950
|
+
throw new Error("Invalid server ops");
|
2861
3951
|
}
|
2862
|
-
return undefined;
|
2863
3952
|
}
|
2864
|
-
|
2865
|
-
|
2866
|
-
|
2867
|
-
|
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);
|
2868
3970
|
}
|
2869
|
-
|
2870
|
-
|
2871
|
-
|
2872
|
-
|
2873
|
-
|
2874
|
-
if (fetchRes.status === 200) {
|
2875
|
-
// TODO: does this stream data?
|
2876
|
-
if (fetchRes.body) {
|
2877
|
-
return {
|
2878
|
-
status: fetchRes.status,
|
2879
|
-
headers: {
|
2880
|
-
"Content-Type": fetchRes.headers.get("Content-Type") || "",
|
2881
|
-
"Content-Length": fetchRes.headers.get("Content-Length") || "0",
|
2882
|
-
"Cache-Control": fetchRes.headers.get("Cache-Control") || ""
|
2883
|
-
},
|
2884
|
-
body: fetchRes.body
|
2885
|
-
};
|
2886
|
-
} else {
|
2887
|
-
return {
|
2888
|
-
status: 500,
|
2889
|
-
json: {
|
2890
|
-
message: "No body in response"
|
2891
|
-
}
|
2892
|
-
};
|
2893
|
-
}
|
3971
|
+
if (fileBuffer) {
|
3972
|
+
return {
|
3973
|
+
status: 200,
|
3974
|
+
body: bufferToReadableStream(fileBuffer)
|
3975
|
+
};
|
2894
3976
|
} else {
|
2895
|
-
if (!(reqHeaders.host && reqHeaders["x-forwarded-proto"])) {
|
2896
|
-
return {
|
2897
|
-
status: 500,
|
2898
|
-
json: {
|
2899
|
-
message: "Missing host or x-forwarded-proto header"
|
2900
|
-
}
|
2901
|
-
};
|
2902
|
-
}
|
2903
|
-
const host = `${reqHeaders["x-forwarded-proto"]}://${reqHeaders["host"]}`;
|
2904
|
-
const staticPublicUrl = new URL(filePath.slice("/public".length), host).toString();
|
2905
|
-
const fetchRes = await fetch(staticPublicUrl);
|
2906
3977
|
return {
|
2907
|
-
status:
|
2908
|
-
|
2909
|
-
|
2910
|
-
|
3978
|
+
status: 404,
|
3979
|
+
json: {
|
3980
|
+
message: "File not found"
|
3981
|
+
}
|
2911
3982
|
};
|
2912
3983
|
}
|
2913
3984
|
}
|
@@ -3012,37 +4083,21 @@ function getStateFromCookie(stateCookie) {
|
|
3012
4083
|
};
|
3013
4084
|
}
|
3014
4085
|
}
|
3015
|
-
async function createJsonError(fetchRes) {
|
3016
|
-
var _fetchRes$headers$get;
|
3017
|
-
if ((_fetchRes$headers$get = fetchRes.headers.get("Content-Type")) !== null && _fetchRes$headers$get !== void 0 && _fetchRes$headers$get.includes("application/json")) {
|
3018
|
-
return {
|
3019
|
-
status: fetchRes.status,
|
3020
|
-
json: await fetchRes.json()
|
3021
|
-
};
|
3022
|
-
}
|
3023
|
-
console.error("Unexpected failure (did not get a json) - Val down?", fetchRes.status, await fetchRes.text());
|
3024
|
-
return {
|
3025
|
-
status: fetchRes.status,
|
3026
|
-
json: {
|
3027
|
-
message: "Unexpected failure (did not get a json) - Val down?"
|
3028
|
-
}
|
3029
|
-
};
|
3030
|
-
}
|
3031
4086
|
function createStateCookie(state) {
|
3032
4087
|
return Buffer.from(JSON.stringify(state), "utf8").toString("base64");
|
3033
4088
|
}
|
3034
|
-
const ValAppJwtPayload = z
|
3035
|
-
sub: z
|
3036
|
-
exp: z
|
3037
|
-
project: z
|
3038
|
-
org: z
|
4089
|
+
const ValAppJwtPayload = z.object({
|
4090
|
+
sub: z.string(),
|
4091
|
+
exp: z.number(),
|
4092
|
+
project: z.string(),
|
4093
|
+
org: z.string()
|
3039
4094
|
});
|
3040
|
-
const IntegratedServerJwtPayload = z
|
3041
|
-
sub: z
|
3042
|
-
exp: z
|
3043
|
-
token: z
|
3044
|
-
org: z
|
3045
|
-
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()
|
3046
4101
|
});
|
3047
4102
|
async function withAuth(secret, cookies, errorMessageType, handler) {
|
3048
4103
|
const cookie = cookies[VAL_SESSION_COOKIE$1];
|
@@ -3099,42 +4154,147 @@ function getAuthHeaders(token, type) {
|
|
3099
4154
|
Authorization: `Bearer ${token}`
|
3100
4155
|
};
|
3101
4156
|
}
|
3102
|
-
|
3103
|
-
|
3104
|
-
|
3105
|
-
|
3106
|
-
|
3107
|
-
|
3108
|
-
|
3109
|
-
|
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;
|
3110
4173
|
}
|
3111
|
-
|
3112
|
-
paramsString += `${key}=${encodeURIComponent(param)}`;
|
3113
|
-
}
|
3114
|
-
if (paramIdx < Object.keys(params).length - 1) {
|
3115
|
-
paramsString += "&";
|
4174
|
+
controller.close();
|
3116
4175
|
}
|
3117
|
-
|
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
|
+
};
|
3118
4187
|
}
|
3119
|
-
|
4188
|
+
if (overrideHost) {
|
4189
|
+
return overrideHost + "?redirect_to=" + encodeURIComponent(query.redirect_to);
|
4190
|
+
}
|
4191
|
+
return query.redirect_to;
|
3120
4192
|
}
|
3121
4193
|
|
3122
|
-
|
3123
|
-
|
3124
|
-
|
3125
|
-
|
3126
|
-
|
3127
|
-
|
3128
|
-
|
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;
|
3129
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);
|
3130
4287
|
}
|
3131
4288
|
async function initHandlerOptions(route, opts) {
|
3132
4289
|
const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
|
3133
4290
|
const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
|
3134
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";
|
3135
4296
|
if (isProxyMode) {
|
3136
4297
|
var _opts$versions, _opts$versions2;
|
3137
|
-
const valBuildUrl = opts.valBuildUrl || process.env.VAL_BUILD_URL || "https://app.val.build";
|
3138
4298
|
if (!maybeApiKey || !maybeValSecret) {
|
3139
4299
|
throw new Error("VAL_API_KEY and VAL_SECRET env vars must both be set in proxy mode");
|
3140
4300
|
}
|
@@ -3147,9 +4307,8 @@ async function initHandlerOptions(route, opts) {
|
|
3147
4307
|
if (!maybeGitBranch) {
|
3148
4308
|
throw new Error("VAL_GIT_BRANCH env var must be set in proxy mode");
|
3149
4309
|
}
|
3150
|
-
|
3151
|
-
|
3152
|
-
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.");
|
3153
4312
|
}
|
3154
4313
|
const coreVersion = (_opts$versions = opts.versions) === null || _opts$versions === void 0 ? void 0 : _opts$versions.core;
|
3155
4314
|
if (!coreVersion) {
|
@@ -3160,43 +4319,40 @@ async function initHandlerOptions(route, opts) {
|
|
3160
4319
|
throw new Error("Could not determine version of @valbuild/next");
|
3161
4320
|
}
|
3162
4321
|
return {
|
3163
|
-
mode: "
|
4322
|
+
mode: "http",
|
3164
4323
|
route,
|
3165
4324
|
apiKey: maybeApiKey,
|
3166
4325
|
valSecret: maybeValSecret,
|
3167
|
-
|
4326
|
+
commit: maybeGitCommit,
|
4327
|
+
branch: maybeGitBranch,
|
4328
|
+
root: opts.root,
|
4329
|
+
project: maybeValProject,
|
4330
|
+
valEnableRedirectUrl,
|
4331
|
+
valDisableRedirectUrl,
|
3168
4332
|
valContentUrl,
|
3169
|
-
|
3170
|
-
core: coreVersion,
|
3171
|
-
next: nextVersion
|
3172
|
-
},
|
3173
|
-
git: {
|
3174
|
-
commit: maybeGitCommit,
|
3175
|
-
branch: maybeGitBranch
|
3176
|
-
},
|
3177
|
-
remote: maybeValRemote,
|
3178
|
-
valEnableRedirectUrl: opts.valEnableRedirectUrl || process.env.VAL_ENABLE_REDIRECT_URL,
|
3179
|
-
valDisableRedirectUrl: opts.valDisableRedirectUrl || process.env.VAL_DISABLE_REDIRECT_URL
|
4333
|
+
valBuildUrl
|
3180
4334
|
};
|
3181
4335
|
} else {
|
3182
4336
|
const cwd = process.cwd();
|
3183
|
-
const
|
3184
|
-
const git = await safeReadGit(cwd);
|
4337
|
+
const valBuildUrl = opts.valBuildUrl || process.env.VAL_BUILD_URL || "https://app.val.build";
|
3185
4338
|
return {
|
3186
|
-
mode: "
|
3187
|
-
|
3188
|
-
|
3189
|
-
valDisableRedirectUrl
|
3190
|
-
|
3191
|
-
|
3192
|
-
|
3193
|
-
|
4339
|
+
mode: "fs",
|
4340
|
+
cwd,
|
4341
|
+
route,
|
4342
|
+
valDisableRedirectUrl,
|
4343
|
+
valEnableRedirectUrl,
|
4344
|
+
valBuildUrl,
|
4345
|
+
apiKey: maybeApiKey,
|
4346
|
+
valSecret: maybeValSecret,
|
4347
|
+
project: maybeValProject
|
3194
4348
|
};
|
3195
4349
|
}
|
3196
4350
|
}
|
4351
|
+
|
4352
|
+
// TODO: remove
|
3197
4353
|
async function safeReadGit(cwd) {
|
3198
4354
|
async function findGitHead(currentDir, depth) {
|
3199
|
-
const gitHeadPath =
|
4355
|
+
const gitHeadPath = fsPath.join(currentDir, ".git", "HEAD");
|
3200
4356
|
if (depth > 1000) {
|
3201
4357
|
console.error(`Reached max depth while scanning for .git folder. Current working dir: ${cwd}.`);
|
3202
4358
|
return {
|
@@ -3220,7 +4376,7 @@ async function safeReadGit(cwd) {
|
|
3220
4376
|
};
|
3221
4377
|
}
|
3222
4378
|
} catch (error) {
|
3223
|
-
const parentDir =
|
4379
|
+
const parentDir = fsPath.dirname(currentDir);
|
3224
4380
|
|
3225
4381
|
// We've reached the root directory
|
3226
4382
|
if (parentDir === currentDir) {
|
@@ -3244,7 +4400,7 @@ async function safeReadGit(cwd) {
|
|
3244
4400
|
}
|
3245
4401
|
async function readCommit(gitDir, branchName) {
|
3246
4402
|
try {
|
3247
|
-
return (await promises.readFile(
|
4403
|
+
return (await promises.readFile(fsPath.join(gitDir, ".git", "refs", "heads", branchName), "utf-8")).trim();
|
3248
4404
|
} catch (err) {
|
3249
4405
|
return undefined;
|
3250
4406
|
}
|
@@ -3261,10 +4417,6 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
3261
4417
|
return async req => {
|
3262
4418
|
var _req$method;
|
3263
4419
|
const valServer = await valServerPromise;
|
3264
|
-
const requestHeaders = {
|
3265
|
-
host: req.headers.get("host"),
|
3266
|
-
"x-forwarded-proto": req.headers.get("x-forwarded-proto")
|
3267
|
-
};
|
3268
4420
|
const url = new URL(req.url);
|
3269
4421
|
if (!url.pathname.startsWith(route)) {
|
3270
4422
|
const error = {
|
@@ -3325,35 +4477,40 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
3325
4477
|
return convert(await valServer.disable({
|
3326
4478
|
redirect_to: url.searchParams.get("redirect_to") || undefined
|
3327
4479
|
}));
|
3328
|
-
} else if (method === "POST" && path === "/
|
3329
|
-
const body = await req.json();
|
3330
|
-
return convert(await valServer.postCommit(body, getCookies(req, [VAL_SESSION_COOKIE]), requestHeaders));
|
3331
|
-
} else if (method === "POST" && path === "/validate") {
|
4480
|
+
} else if (method === "POST" && path === "/save") {
|
3332
4481
|
const body = await req.json();
|
3333
|
-
return convert(await valServer.
|
3334
|
-
|
3335
|
-
|
3336
|
-
|
3337
|
-
|
3338
|
-
|
3339
|
-
|
3340
|
-
|
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]))));
|
3341
4501
|
} else if (method === "GET" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
3342
4502
|
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.getPatches({
|
3343
|
-
|
4503
|
+
authors: url.searchParams.getAll("author")
|
3344
4504
|
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
3345
|
-
} else if (method === "POST" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
3346
|
-
const body = await req.json();
|
3347
|
-
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.postPatches(body, getCookies(req, [VAL_SESSION_COOKIE]))));
|
3348
4505
|
} else if (method === "DELETE" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
3349
4506
|
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.deletePatches({
|
3350
4507
|
id: url.searchParams.getAll("id")
|
3351
4508
|
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
3352
|
-
} else if (path.startsWith(FILES_PATH_PREFIX)) {
|
4509
|
+
} else if (method === "GET" && path.startsWith(FILES_PATH_PREFIX)) {
|
3353
4510
|
const treePath = path.slice(FILES_PATH_PREFIX.length);
|
3354
4511
|
return convert(await valServer.getFiles(treePath, {
|
3355
|
-
|
3356
|
-
}
|
4512
|
+
patch_id: url.searchParams.get("patch_id") || undefined
|
4513
|
+
}));
|
3357
4514
|
} else {
|
3358
4515
|
return convert({
|
3359
4516
|
status: 404,
|
@@ -3405,10 +4562,10 @@ class ValFSHost {
|
|
3405
4562
|
return this.currentDirectory;
|
3406
4563
|
}
|
3407
4564
|
getCanonicalFileName(fileName) {
|
3408
|
-
if (
|
3409
|
-
return
|
4565
|
+
if (fsPath__default.isAbsolute(fileName)) {
|
4566
|
+
return fsPath__default.normalize(fileName);
|
3410
4567
|
}
|
3411
|
-
return
|
4568
|
+
return fsPath__default.resolve(this.getCurrentDirectory(), fileName);
|
3412
4569
|
}
|
3413
4570
|
fileExists(fileName) {
|
3414
4571
|
return this.valFS.fileExists(fileName);
|
@@ -3421,6 +4578,14 @@ class ValFSHost {
|
|
3421
4578
|
}
|
3422
4579
|
}
|
3423
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
|
+
|
3424
4589
|
// TODO: find a better name? transformFixesToPatch?
|
3425
4590
|
async function createFixPatch(config, apply, sourcePath, validationError) {
|
3426
4591
|
async function getImageMetadata() {
|
@@ -3429,7 +4594,7 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
3429
4594
|
// TODO:
|
3430
4595
|
throw Error("Cannot fix image without a file reference");
|
3431
4596
|
}
|
3432
|
-
const filename =
|
4597
|
+
const filename = fsPath__default.join(config.projectRoot, fileRef);
|
3433
4598
|
const buffer = fs.readFileSync(filename);
|
3434
4599
|
return extractImageMetadata(filename, buffer);
|
3435
4600
|
}
|
@@ -3439,7 +4604,7 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
3439
4604
|
// TODO:
|
3440
4605
|
throw Error("Cannot fix file without a file reference");
|
3441
4606
|
}
|
3442
|
-
const filename =
|
4607
|
+
const filename = fsPath__default.join(config.projectRoot, fileRef);
|
3443
4608
|
const buffer = fs.readFileSync(filename);
|
3444
4609
|
return extractFileMetadata(fileRef, buffer);
|
3445
4610
|
}
|
@@ -3605,4 +4770,4 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
3605
4770
|
};
|
3606
4771
|
}
|
3607
4772
|
|
3608
|
-
export {
|
4773
|
+
export { Patch, PatchJSON, Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createFixPatch, createService, createValApiRouter, createValServer, decodeJwt, encodeJwt, formatSyntaxErrorTree, getCompilerOptions, getExpire, patchSourceFile, safeReadGit };
|