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