@valbuild/server 0.60.27 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/declarations/src/SerializedModuleContent.d.ts +2 -2
- package/dist/declarations/src/Service.d.ts +3 -3
- package/dist/declarations/src/ValServer.d.ts +93 -133
- package/dist/declarations/src/createValApiRouter.d.ts +7 -8
- package/dist/declarations/src/index.d.ts +0 -1
- package/dist/valbuild-server.cjs.dev.js +2621 -1426
- package/dist/valbuild-server.cjs.prod.js +2621 -1426
- package/dist/valbuild-server.esm.js +2622 -1426
- package/package.json +7 -10
- package/dist/declarations/src/LocalValServer.d.ts +0 -63
@@ -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,1218 +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) {
|
1361
|
+
this.valModules = valModules;
|
1419
1362
|
this.options = options;
|
1420
|
-
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));
|
1421
1377
|
}
|
1422
1378
|
|
1423
|
-
|
1424
|
-
async
|
1425
|
-
|
1426
|
-
|
1427
|
-
|
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;
|
1428
1453
|
}
|
1429
|
-
await this.callbacks.onEnable(true);
|
1430
1454
|
return {
|
1431
|
-
|
1432
|
-
|
1433
|
-
|
1434
|
-
|
1435
|
-
|
1455
|
+
baseSha: this.baseSha,
|
1456
|
+
schemaSha: this.schemaSha,
|
1457
|
+
sources: this.sources,
|
1458
|
+
schemas: this.schemas,
|
1459
|
+
moduleErrors: this.modulesErrors
|
1436
1460
|
};
|
1437
1461
|
}
|
1438
|
-
async
|
1439
|
-
const
|
1440
|
-
|
1441
|
-
|
1442
|
-
}
|
1443
|
-
await this.
|
1444
|
-
return {
|
1445
|
-
cookies: {
|
1446
|
-
[internal.VAL_ENABLE_COOKIE_NAME]: {
|
1447
|
-
value: "false"
|
1448
|
-
}
|
1449
|
-
},
|
1450
|
-
status: 302,
|
1451
|
-
redirectTo: redirectToRes
|
1452
|
-
};
|
1462
|
+
async init() {
|
1463
|
+
const {
|
1464
|
+
baseSha,
|
1465
|
+
schemaSha
|
1466
|
+
} = await this.initTree();
|
1467
|
+
await this.onInit(baseSha, schemaSha);
|
1453
1468
|
}
|
1454
|
-
async
|
1455
|
-
|
1456
|
-
query, cookies, requestHeaders) {
|
1457
|
-
const ensureRes = await debugTiming("ensureInitialized", () => this.ensureInitialized("getTree", cookies));
|
1458
|
-
if (fp.result.isErr(ensureRes)) {
|
1459
|
-
return ensureRes.error;
|
1460
|
-
}
|
1461
|
-
const applyPatches = query.patch === "true";
|
1462
|
-
const includeSource = query.source === "true";
|
1463
|
-
const includeSchema = query.schema === "true";
|
1464
|
-
const moduleIds = await debugTiming("getAllModules", () => this.getAllModules(treePath));
|
1465
|
-
let {
|
1466
|
-
patchIdsByModuleId,
|
1467
|
-
patchesById,
|
1468
|
-
fileUpdates
|
1469
|
-
} = {
|
1470
|
-
patchIdsByModuleId: {},
|
1471
|
-
patchesById: {},
|
1472
|
-
fileUpdates: {}
|
1473
|
-
};
|
1474
|
-
if (applyPatches) {
|
1475
|
-
const res = await debugTiming("readPatches", () => this.readPatches(cookies));
|
1476
|
-
if (fp.result.isErr(res)) {
|
1477
|
-
return res.error;
|
1478
|
-
}
|
1479
|
-
patchIdsByModuleId = res.value.patchIdsByModuleId;
|
1480
|
-
patchesById = res.value.patchesById;
|
1481
|
-
fileUpdates = res.value.fileUpdates;
|
1482
|
-
}
|
1483
|
-
const validate = false;
|
1484
|
-
const possiblyPatchedContent = await debugTiming("applyAllPatchesThenValidate", () => Promise.all(moduleIds.map(async moduleId => {
|
1485
|
-
return this.applyAllPatchesThenValidate(moduleId, patchIdsByModuleId, patchesById, fileUpdates, cookies, requestHeaders, applyPatches, validate, includeSource, includeSchema);
|
1486
|
-
})));
|
1487
|
-
const modules = Object.fromEntries(possiblyPatchedContent.map(serializedModuleContent => {
|
1488
|
-
const module = {
|
1489
|
-
schema: serializedModuleContent.schema,
|
1490
|
-
source: serializedModuleContent.source,
|
1491
|
-
errors: serializedModuleContent.errors
|
1492
|
-
};
|
1493
|
-
return [serializedModuleContent.path, module];
|
1494
|
-
}));
|
1495
|
-
const apiTreeResponse = {
|
1496
|
-
modules,
|
1497
|
-
git: this.options.git
|
1498
|
-
};
|
1499
|
-
return {
|
1500
|
-
status: 200,
|
1501
|
-
json: apiTreeResponse
|
1502
|
-
};
|
1469
|
+
async getBaseSources() {
|
1470
|
+
return this.initTree().then(result => result.sources);
|
1503
1471
|
}
|
1504
|
-
async
|
1505
|
-
|
1506
|
-
if (fp.result.isErr(ensureRes)) {
|
1507
|
-
return ensureRes.error;
|
1508
|
-
}
|
1509
|
-
return this.validateThenMaybeCommit(rawBody, false, cookies, requestHeaders);
|
1472
|
+
async getSchemas() {
|
1473
|
+
return this.initTree().then(result => result.schemas);
|
1510
1474
|
}
|
1511
|
-
async
|
1512
|
-
|
1513
|
-
|
1514
|
-
|
1515
|
-
|
1516
|
-
|
1517
|
-
|
1518
|
-
|
1519
|
-
return {
|
1520
|
-
status: 400,
|
1521
|
-
json: {
|
1522
|
-
...res.json
|
1523
|
-
}
|
1524
|
-
};
|
1525
|
-
}
|
1526
|
-
return {
|
1527
|
-
status: 200,
|
1528
|
-
json: {
|
1529
|
-
...res.json,
|
1530
|
-
git: this.options.git
|
1531
|
-
}
|
1532
|
-
};
|
1533
|
-
}
|
1534
|
-
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);
|
1535
1483
|
}
|
1536
1484
|
|
1537
|
-
|
1538
|
-
|
1539
|
-
const
|
1540
|
-
|
1541
|
-
|
1542
|
-
|
1543
|
-
|
1544
|
-
|
1545
|
-
}
|
1546
|
-
|
1547
|
-
|
1548
|
-
|
1549
|
-
|
1550
|
-
}
|
1551
|
-
if (!maybeSource || !schema) {
|
1552
|
-
return serializedModuleContent;
|
1553
|
-
}
|
1554
|
-
let source = maybeSource;
|
1555
|
-
await debugTiming("applyPatches:" + moduleId, async () => {
|
1556
|
-
for (const patchId of patchIdsByModuleId[moduleId] ?? []) {
|
1557
|
-
const patch$1 = patchesById[patchId];
|
1558
|
-
if (!patch$1) {
|
1559
|
-
continue;
|
1560
|
-
}
|
1561
|
-
const patchRes = patch.applyPatch(source, ops, patch$1.filter(core.Internal.notFileOp));
|
1562
|
-
if (fp.result.isOk(patchRes)) {
|
1563
|
-
source = patchRes.value;
|
1564
|
-
} else {
|
1565
|
-
console.error("Val: got an unexpected error while applying patch. Is there a mismatch in Val versions? Perhaps Val is misconfigured?", {
|
1566
|
-
patchId,
|
1567
|
-
moduleId,
|
1568
|
-
patch: JSON.stringify(patch$1, null, 2),
|
1569
|
-
error: patchRes.error
|
1570
|
-
});
|
1571
|
-
return {
|
1572
|
-
path: moduleId,
|
1573
|
-
schema,
|
1574
|
-
source,
|
1575
|
-
errors: {
|
1576
|
-
fatal: [{
|
1577
|
-
message: "Unexpected error applying patch",
|
1578
|
-
type: "invalid-patch"
|
1579
|
-
}]
|
1580
|
-
}
|
1581
|
-
};
|
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;
|
1582
1498
|
}
|
1583
1499
|
}
|
1584
|
-
|
1585
|
-
|
1586
|
-
const validationErrors = await debugTiming("validate:" + moduleId, async () => core.deserializeSchema(schema).validate(moduleId, source));
|
1587
|
-
if (validationErrors) {
|
1588
|
-
const revalidated = await debugTiming("revalidate image/file:" + moduleId, async () => this.revalidateImageAndFileValidation(validationErrors, fileUpdates, cookies, requestHeaders));
|
1589
|
-
return {
|
1590
|
-
path: moduleId,
|
1591
|
-
schema,
|
1592
|
-
source,
|
1593
|
-
errors: revalidated && {
|
1594
|
-
validation: revalidated
|
1595
|
-
}
|
1596
|
-
};
|
1500
|
+
if (!patchesByModule[path]) {
|
1501
|
+
patchesByModule[path] = [];
|
1597
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));
|
1598
1510
|
}
|
1599
1511
|
return {
|
1600
|
-
|
1601
|
-
|
1602
|
-
source,
|
1603
|
-
errors: false
|
1512
|
+
patchesByModule,
|
1513
|
+
fileLastUpdatedByPatchId
|
1604
1514
|
};
|
1605
1515
|
}
|
1606
|
-
|
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
|
-
continue;
|
1667
|
-
}
|
1668
|
-
if (error.fixes.some(fix => fix === "image:replace-metadata")) {
|
1669
|
-
expectedMetadata = await extractImageMetadata(filePath, fileBuffer);
|
1670
|
-
} else {
|
1671
|
-
expectedMetadata = await extractFileMetadata(filePath, fileBuffer);
|
1672
|
-
}
|
1673
|
-
}
|
1674
|
-
if (!expectedMetadata) {
|
1675
|
-
revalidatedValidationErrors[errorSourcePath].push({
|
1676
|
-
message: `Could not read file metadata. Is the reference to the file: ${fileRef} correct?`
|
1677
|
-
});
|
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
|
+
}];
|
1678
1575
|
} else {
|
1679
|
-
|
1680
|
-
const revalidatedError = validateMetadata(actualMetadata, expectedMetadata);
|
1681
|
-
if (!revalidatedError) {
|
1682
|
-
// no errors anymore:
|
1683
|
-
continue;
|
1684
|
-
}
|
1685
|
-
const errorMsgs = (revalidatedError.globalErrors || []).concat(Object.values(revalidatedError.erroneousMetadata || {})).concat(Object.values(revalidatedError.missingMetadata || []).map(missingKey => {
|
1686
|
-
var _expectedMetadata;
|
1687
|
-
return `Required key: '${missingKey}' is not defined. Should be: '${JSON.stringify((_expectedMetadata = expectedMetadata) === null || _expectedMetadata === void 0 ? void 0 : _expectedMetadata[missingKey])}'`;
|
1688
|
-
}));
|
1689
|
-
revalidatedValidationErrors[errorSourcePath].push(...errorMsgs.map(message => ({
|
1690
|
-
message
|
1691
|
-
})));
|
1576
|
+
applicableOps.push(op);
|
1692
1577
|
}
|
1693
|
-
} else {
|
1694
|
-
revalidatedValidationErrors[errorSourcePath].push(error);
|
1695
1578
|
}
|
1696
|
-
|
1697
|
-
|
1698
|
-
|
1699
|
-
|
1700
|
-
}
|
1701
|
-
const hasErrors = Object.values(revalidatedValidationErrors).some(errors => errors.length > 0);
|
1702
|
-
if (hasErrors) {
|
1703
|
-
return revalidatedValidationErrors;
|
1704
|
-
}
|
1705
|
-
return hasErrors;
|
1706
|
-
}
|
1707
|
-
sortPatchIds(patchesByModule) {
|
1708
|
-
return Object.values(patchesByModule).flatMap(modulePatches => modulePatches).sort((a, b) => {
|
1709
|
-
return a.created_at.localeCompare(b.created_at);
|
1710
|
-
}).map(patchData => patchData.patch_id);
|
1711
|
-
}
|
1712
|
-
|
1713
|
-
// can be overridden if FS cannot read from static assets / public folder (because of bundlers or what not)
|
1714
|
-
async readStaticBinaryFile(filePath) {
|
1715
|
-
return fs__default["default"].promises.readFile(filePath);
|
1716
|
-
}
|
1717
|
-
async readPatches(cookies) {
|
1718
|
-
const res = await this.getPatches({},
|
1719
|
-
// {} means no ids, so get all patches
|
1720
|
-
cookies);
|
1721
|
-
if (res.status === 400 || res.status === 401 || res.status === 403 || res.status === 404 || res.status === 500 || res.status === 501) {
|
1722
|
-
return fp.result.err(res);
|
1723
|
-
} else if (res.status === 200 || res.status === 201) {
|
1724
|
-
const patchesByModule = res.json;
|
1725
|
-
const patches = [];
|
1726
|
-
const patchIdsByModuleId = {};
|
1727
|
-
const patchesById = {};
|
1728
|
-
for (const [moduleIdS, modulePatchData] of Object.entries(patchesByModule)) {
|
1729
|
-
const moduleId = moduleIdS;
|
1730
|
-
patchIdsByModuleId[moduleId] = modulePatchData.map(patch => patch.patch_id);
|
1731
|
-
for (const patchData of modulePatchData) {
|
1732
|
-
patches.push([patchData.patch_id, moduleId, patchData.patch]);
|
1733
|
-
patchesById[patchData.patch_id] = patchData.patch;
|
1734
|
-
}
|
1735
|
-
}
|
1736
|
-
const fileUpdates = {};
|
1737
|
-
const sortedPatchIds = this.sortPatchIds(patchesByModule);
|
1738
|
-
for (const sortedPatchId of sortedPatchIds) {
|
1739
|
-
const patchId = sortedPatchId;
|
1740
|
-
for (const op of patchesById[patchId] || []) {
|
1741
|
-
if (op.op === "file") {
|
1742
|
-
const parsedFileOp = z.z.object({
|
1743
|
-
sha256: z.z.string(),
|
1744
|
-
mimeType: z.z.string()
|
1745
|
-
}).safeParse(op.value);
|
1746
|
-
if (!parsedFileOp.success) {
|
1747
|
-
return fp.result.err({
|
1748
|
-
status: 500,
|
1749
|
-
json: {
|
1750
|
-
message: "Unexpected error: file op value must be transformed into object",
|
1751
|
-
details: {
|
1752
|
-
value: "First 200 chars: " + JSON.stringify(op.value).slice(0, 200),
|
1753
|
-
patchId
|
1754
|
-
}
|
1755
|
-
}
|
1756
|
-
});
|
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] = [];
|
1757
1583
|
}
|
1758
|
-
|
1759
|
-
|
1760
|
-
|
1584
|
+
errors[path].push({
|
1585
|
+
patchId: patchId,
|
1586
|
+
error: patchRes.error
|
1587
|
+
});
|
1588
|
+
} else {
|
1589
|
+
patchedSources[path] = patchRes.value;
|
1761
1590
|
}
|
1762
1591
|
}
|
1763
1592
|
}
|
1764
|
-
return fp.result.ok({
|
1765
|
-
patches,
|
1766
|
-
patchIdsByModuleId,
|
1767
|
-
patchesById,
|
1768
|
-
fileUpdates
|
1769
|
-
});
|
1770
|
-
} else {
|
1771
|
-
return fp.result.err({
|
1772
|
-
status: 500,
|
1773
|
-
json: {
|
1774
|
-
message: "Unknown error"
|
1775
|
-
}
|
1776
|
-
});
|
1777
1593
|
}
|
1594
|
+
return {
|
1595
|
+
sources: patchedSources,
|
1596
|
+
errors
|
1597
|
+
};
|
1778
1598
|
}
|
1779
|
-
|
1780
|
-
|
1781
|
-
|
1782
|
-
}
|
1783
|
-
|
1784
|
-
|
1785
|
-
|
1786
|
-
|
1787
|
-
|
1788
|
-
|
1789
|
-
|
1790
|
-
|
1791
|
-
|
1792
|
-
|
1793
|
-
|
1794
|
-
|
1795
|
-
|
1796
|
-
const {
|
1797
|
-
patchIdsByModuleId,
|
1798
|
-
patchesById,
|
1799
|
-
patches,
|
1800
|
-
fileUpdates
|
1801
|
-
} = res.value;
|
1802
|
-
const validationErrorsByModuleId = {};
|
1803
|
-
for (const moduleIdStr of await this.getAllModules("/")) {
|
1804
|
-
const moduleId = moduleIdStr;
|
1805
|
-
const serializedModuleContent = await this.applyAllPatchesThenValidate(moduleId, filterPatchesByModuleIdRes.data.patches ||
|
1806
|
-
// TODO: refine to ModuleId and PatchId when parsing
|
1807
|
-
patchIdsByModuleId, patchesById, fileUpdates, cookies, requestHeaders, true, true, true, commit);
|
1808
|
-
if (serializedModuleContent.errors) {
|
1809
|
-
validationErrorsByModuleId[moduleId] = serializedModuleContent;
|
1810
|
-
}
|
1811
|
-
}
|
1812
|
-
if (Object.keys(validationErrorsByModuleId).length > 0) {
|
1813
|
-
const modules = {};
|
1814
|
-
for (const [patchId, moduleId] of patches) {
|
1815
|
-
if (!modules[moduleId]) {
|
1816
|
-
modules[moduleId] = {
|
1817
|
-
patches: {
|
1818
|
-
applied: []
|
1819
|
-
}
|
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: {}
|
1820
1616
|
};
|
1821
1617
|
}
|
1822
|
-
|
1823
|
-
|
1824
|
-
|
1825
|
-
|
1618
|
+
errors[path] = {
|
1619
|
+
...errors[path],
|
1620
|
+
invalidSource: {
|
1621
|
+
message: `Module at path: '${path}' does not exist`
|
1826
1622
|
}
|
1827
|
-
|
1828
|
-
|
1829
|
-
modules[moduleId].patches.applied.push(patchId);
|
1830
|
-
}
|
1623
|
+
};
|
1624
|
+
continue;
|
1831
1625
|
}
|
1832
|
-
|
1833
|
-
|
1834
|
-
|
1835
|
-
|
1836
|
-
|
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
|
+
}
|
1837
1652
|
}
|
1838
|
-
};
|
1839
|
-
}
|
1840
|
-
let modules;
|
1841
|
-
if (commit) {
|
1842
|
-
const commitRes = await this.execCommit(patches, cookies);
|
1843
|
-
if (commitRes.status !== 200) {
|
1844
|
-
return commitRes;
|
1845
1653
|
}
|
1846
|
-
modules = commitRes.json;
|
1847
|
-
} else {
|
1848
|
-
modules = await this.getPatchedModules(patches);
|
1849
1654
|
}
|
1850
1655
|
return {
|
1851
|
-
|
1852
|
-
|
1853
|
-
|
1854
|
-
|
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
|
+
};
|
1855
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;
|
1856
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);
|
1857
1797
|
}
|
1858
|
-
|
1859
|
-
|
1860
|
-
|
1861
|
-
|
1862
|
-
|
1863
|
-
|
1864
|
-
|
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
|
+
});
|
1865
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
|
1866
1873
|
};
|
1867
1874
|
}
|
1868
|
-
|
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
|
+
}
|
1869
1900
|
}
|
1870
|
-
|
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;
|
1871
1929
|
}
|
1872
1930
|
|
1873
|
-
|
1874
|
-
|
1875
|
-
|
1876
|
-
|
1877
|
-
|
1878
|
-
|
1879
|
-
|
1880
|
-
|
1881
|
-
|
1882
|
-
|
1883
|
-
|
1884
|
-
|
1885
|
-
|
1886
|
-
}
|
1887
|
-
|
1888
|
-
|
1889
|
-
|
1890
|
-
|
1891
|
-
|
1892
|
-
|
1893
|
-
|
1894
|
-
|
1895
|
-
|
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
|
+
}
|
1896
1996
|
}
|
1897
|
-
controller.close();
|
1898
1997
|
}
|
1899
|
-
|
1900
|
-
|
1901
|
-
|
1902
|
-
|
1903
|
-
|
1904
|
-
|
1905
|
-
|
1906
|
-
|
1907
|
-
|
1908
|
-
|
1909
|
-
|
1910
|
-
|
1911
|
-
|
1912
|
-
|
1913
|
-
|
1914
|
-
|
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
|
+
};
|
1915
2073
|
}
|
2074
|
+
}));
|
2075
|
+
return {
|
2076
|
+
patchId,
|
2077
|
+
files: saveFileRes,
|
2078
|
+
createdAt: new Date().toISOString()
|
1916
2079
|
};
|
1917
2080
|
}
|
1918
|
-
|
1919
|
-
|
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;
|
1920
2088
|
}
|
1921
|
-
return
|
2089
|
+
return false;
|
1922
2090
|
}
|
1923
|
-
|
1924
|
-
|
1925
|
-
|
1926
|
-
const base64Index = content.indexOf(";base64,");
|
1927
|
-
if (dataIndex > -1 || base64Index > -1) {
|
1928
|
-
const mimeType = content.slice(dataIndex + base64DataAttr.length, base64Index);
|
1929
|
-
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;
|
1930
2094
|
}
|
1931
|
-
return
|
2095
|
+
return false;
|
1932
2096
|
}
|
1933
|
-
function
|
1934
|
-
|
1935
|
-
|
1936
|
-
|
1937
|
-
|
1938
|
-
base64Data = dataUrl.slice(base64Index + ";base64,".length);
|
1939
|
-
}
|
1940
|
-
} else {
|
1941
|
-
const dataUrlEncodingHeader = `${base64DataAttr}${contentType};base64,`;
|
1942
|
-
if (dataUrl.slice(0, dataUrlEncodingHeader.length) === dataUrlEncodingHeader) {
|
1943
|
-
base64Data = dataUrl.slice(dataUrlEncodingHeader.length);
|
1944
|
-
}
|
2097
|
+
function getFieldsForType(type) {
|
2098
|
+
if (type === "file") {
|
2099
|
+
return ["sha256", "mimeType"];
|
2100
|
+
} else if (type === "image") {
|
2101
|
+
return ["sha256", "mimeType", "height", "width"];
|
1945
2102
|
}
|
1946
|
-
|
1947
|
-
|
1948
|
-
|
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
|
+
};
|
2122
|
+
}
|
2123
|
+
availableMetadata = {
|
2124
|
+
sha256: sha256,
|
2125
|
+
mimeType,
|
2126
|
+
height,
|
2127
|
+
width
|
2128
|
+
};
|
2129
|
+
} else {
|
2130
|
+
availableMetadata = {
|
2131
|
+
sha256: sha256,
|
2132
|
+
mimeType
|
2133
|
+
};
|
1949
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
|
+
});
|
2145
|
+
}
|
2146
|
+
}
|
2147
|
+
if (errors.length > 0) {
|
2148
|
+
return {
|
2149
|
+
errors
|
2150
|
+
};
|
2151
|
+
}
|
2152
|
+
return {
|
2153
|
+
metadata
|
2154
|
+
};
|
1950
2155
|
}
|
1951
|
-
|
1952
|
-
|
1953
|
-
|
1954
|
-
|
1955
|
-
|
1956
|
-
|
1957
|
-
|
1958
|
-
|
1959
|
-
bin: "application/octet-stream",
|
1960
|
-
bmp: "image/bmp",
|
1961
|
-
bz: "application/x-bzip",
|
1962
|
-
bz2: "application/x-bzip2",
|
1963
|
-
cda: "application/x-cdf",
|
1964
|
-
csh: "application/x-csh",
|
1965
|
-
css: "text/css",
|
1966
|
-
csv: "text/csv",
|
1967
|
-
doc: "application/msword",
|
1968
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
1969
|
-
eot: "application/vnd.ms-fontobject",
|
1970
|
-
epub: "application/epub+zip",
|
1971
|
-
gz: "application/gzip",
|
1972
|
-
gif: "image/gif",
|
1973
|
-
htm: "text/html",
|
1974
|
-
html: "text/html",
|
1975
|
-
ico: "image/vnd.microsoft.icon",
|
1976
|
-
ics: "text/calendar",
|
1977
|
-
jar: "application/java-archive",
|
1978
|
-
jpeg: "image/jpeg",
|
1979
|
-
jpg: "image/jpeg",
|
1980
|
-
js: "text/javascript",
|
1981
|
-
json: "application/json",
|
1982
|
-
jsonld: "application/ld+json",
|
1983
|
-
mid: "audio/midi",
|
1984
|
-
midi: "audio/midi",
|
1985
|
-
mjs: "text/javascript",
|
1986
|
-
mp3: "audio/mpeg",
|
1987
|
-
mp4: "video/mp4",
|
1988
|
-
mpeg: "video/mpeg",
|
1989
|
-
mpkg: "application/vnd.apple.installer+xml",
|
1990
|
-
odp: "application/vnd.oasis.opendocument.presentation",
|
1991
|
-
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
1992
|
-
odt: "application/vnd.oasis.opendocument.text",
|
1993
|
-
oga: "audio/ogg",
|
1994
|
-
ogv: "video/ogg",
|
1995
|
-
ogx: "application/ogg",
|
1996
|
-
opus: "audio/opus",
|
1997
|
-
otf: "font/otf",
|
1998
|
-
png: "image/png",
|
1999
|
-
pdf: "application/pdf",
|
2000
|
-
php: "application/x-httpd-php",
|
2001
|
-
ppt: "application/vnd.ms-powerpoint",
|
2002
|
-
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
2003
|
-
rar: "application/vnd.rar",
|
2004
|
-
rtf: "application/rtf",
|
2005
|
-
sh: "application/x-sh",
|
2006
|
-
svg: "image/svg+xml",
|
2007
|
-
tar: "application/x-tar",
|
2008
|
-
tif: "image/tiff",
|
2009
|
-
tiff: "image/tiff",
|
2010
|
-
ts: "video/mp2t",
|
2011
|
-
ttf: "font/ttf",
|
2012
|
-
txt: "text/plain",
|
2013
|
-
vsd: "application/vnd.visio",
|
2014
|
-
wav: "audio/wav",
|
2015
|
-
weba: "audio/webm",
|
2016
|
-
webm: "video/webm",
|
2017
|
-
webp: "image/webp",
|
2018
|
-
woff: "font/woff",
|
2019
|
-
woff2: "font/woff2",
|
2020
|
-
xhtml: "application/xhtml+xml",
|
2021
|
-
xls: "application/vnd.ms-excel",
|
2022
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
2023
|
-
xml: "application/xml",
|
2024
|
-
xul: "application/vnd.mozilla.xul+xml",
|
2025
|
-
zip: "application/zip",
|
2026
|
-
"3gp": "video/3gpp; audio/3gpp if it doesn't contain video",
|
2027
|
-
"3g2": "video/3gpp2; audio/3gpp2 if it doesn't contain video",
|
2028
|
-
"7z": "application/x-7z-compressed"
|
2029
|
-
};
|
2030
|
-
function guessMimeTypeFromPath(filePath) {
|
2031
|
-
const fileExt = filePath.split(".").pop();
|
2032
|
-
if (fileExt) {
|
2033
|
-
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;
|
2034
2164
|
}
|
2035
2165
|
return null;
|
2036
2166
|
}
|
2037
|
-
function
|
2038
|
-
|
2039
|
-
|
2040
|
-
|
2041
|
-
|
2042
|
-
|
2043
|
-
|
2044
|
-
|
2045
|
-
|
2046
|
-
} else {
|
2047
|
-
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
|
+
);
|
2048
2176
|
}
|
2049
2177
|
}
|
2050
2178
|
|
2051
|
-
const
|
2052
|
-
|
2053
|
-
|
2054
|
-
|
2055
|
-
|
2056
|
-
|
2057
|
-
|
2058
|
-
|
2059
|
-
|
2060
|
-
|
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
|
2061
2265
|
}
|
2062
|
-
async
|
2063
|
-
|
2064
|
-
|
2065
|
-
|
2066
|
-
|
2067
|
-
|
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(", "));
|
2068
2278
|
}
|
2069
|
-
|
2070
|
-
|
2071
|
-
async deletePatches(query) {
|
2072
|
-
const deletedPatches = [];
|
2073
|
-
for (const patchId of query.id ?? []) {
|
2074
|
-
const rawPatchFileContent = this.host.readFile(this.getPatchFilePath(patchId));
|
2075
|
-
if (!rawPatchFileContent) {
|
2076
|
-
console.warn("Val: Patch not found", patchId);
|
2279
|
+
const patchId = patchIdNum.toString();
|
2280
|
+
if (includes && !includes.includes(patchId)) {
|
2077
2281
|
continue;
|
2078
2282
|
}
|
2079
|
-
const
|
2080
|
-
|
2081
|
-
|
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))) {
|
2082
2322
|
continue;
|
2083
2323
|
}
|
2084
|
-
|
2085
|
-
|
2086
|
-
|
2087
|
-
|
2088
|
-
|
2089
|
-
|
2090
|
-
|
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;
|
2091
2333
|
}
|
2092
|
-
|
2093
|
-
|
2334
|
+
}
|
2335
|
+
if (errors && Object.keys(errors).length > 0) {
|
2336
|
+
return {
|
2337
|
+
patches,
|
2338
|
+
errors
|
2339
|
+
};
|
2094
2340
|
}
|
2095
2341
|
return {
|
2096
|
-
|
2097
|
-
json: deletedPatches
|
2342
|
+
patches
|
2098
2343
|
};
|
2099
2344
|
}
|
2100
|
-
|
2101
|
-
|
2102
|
-
|
2345
|
+
|
2346
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2347
|
+
parseJsonFile(filePath, parser) {
|
2348
|
+
if (!this.host.fileExists(filePath)) {
|
2103
2349
|
return {
|
2104
|
-
|
2105
|
-
|
2106
|
-
|
2107
|
-
details: patches.error.issues
|
2350
|
+
error: {
|
2351
|
+
message: `File not found: ${filePath}`,
|
2352
|
+
filePath
|
2108
2353
|
}
|
2109
2354
|
};
|
2110
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
|
2416
|
+
}
|
2417
|
+
};
|
2418
|
+
}
|
2419
|
+
}
|
2420
|
+
async saveSourceFilePatch(path, patch, authorId) {
|
2111
2421
|
let fileId = Date.now();
|
2112
|
-
|
2113
|
-
|
2114
|
-
|
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
|
+
};
|
2115
2452
|
}
|
2116
|
-
|
2117
|
-
|
2118
|
-
const
|
2119
|
-
|
2120
|
-
|
2121
|
-
|
2122
|
-
|
2123
|
-
|
2124
|
-
|
2125
|
-
|
2126
|
-
|
2127
|
-
|
2128
|
-
|
2129
|
-
|
2130
|
-
|
2131
|
-
|
2132
|
-
|
2133
|
-
|
2134
|
-
|
2135
|
-
|
2136
|
-
|
2137
|
-
|
2138
|
-
|
2139
|
-
|
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
|
2140
2479
|
}
|
2141
|
-
|
2142
|
-
|
2143
|
-
|
2144
|
-
|
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)
|
2145
2498
|
}
|
2146
|
-
|
2147
|
-
|
2148
|
-
|
2149
|
-
|
2150
|
-
|
2151
|
-
|
2152
|
-
|
2153
|
-
|
2154
|
-
|
2155
|
-
|
2156
|
-
|
2157
|
-
|
2158
|
-
|
2159
|
-
|
2160
|
-
|
2161
|
-
|
2162
|
-
|
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"
|
2163
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
|
+
});
|
2164
2547
|
}
|
2165
2548
|
}
|
2166
|
-
|
2549
|
+
if (fieldErrors.length > 0) {
|
2550
|
+
return {
|
2551
|
+
errors: fieldErrors
|
2552
|
+
};
|
2553
|
+
}
|
2167
2554
|
return {
|
2168
|
-
|
2169
|
-
|
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}`
|
2170
2842
|
};
|
2843
|
+
this.root = (options === null || options === void 0 ? void 0 : options.root) ?? "";
|
2171
2844
|
}
|
2172
|
-
async
|
2173
|
-
|
2845
|
+
async onInit() {
|
2846
|
+
// TODO: unused for now. Implement or remove
|
2174
2847
|
}
|
2175
|
-
async
|
2176
|
-
|
2177
|
-
|
2178
|
-
|
2179
|
-
|
2180
|
-
|
2181
|
-
|
2182
|
-
|
2183
|
-
|
2184
|
-
|
2185
|
-
|
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
|
+
};
|
2186
2886
|
}
|
2187
|
-
const metadata = JSON.parse(metadataFileContent);
|
2188
2887
|
return {
|
2189
|
-
|
2190
|
-
|
2191
|
-
|
2192
|
-
|
2193
|
-
"Cache-Control": "public, max-age=31536000, immutable"
|
2194
|
-
},
|
2195
|
-
body: bufferToReadableStream(fileContent)
|
2888
|
+
patches,
|
2889
|
+
error: {
|
2890
|
+
message: `Could not parse get patches response. Error: ${zodValidationError.fromError(parsed.error)}`
|
2891
|
+
}
|
2196
2892
|
};
|
2197
2893
|
}
|
2198
|
-
}
|
2199
|
-
const buffer = await this.readStaticBinaryFile(path__namespace["default"].join(this.cwd, filePath));
|
2200
|
-
const mimeType = guessMimeTypeFromPath(filePath) || "application/octet-stream";
|
2201
|
-
if (!buffer) {
|
2202
2894
|
return {
|
2203
|
-
|
2204
|
-
|
2205
|
-
message: "
|
2895
|
+
patches,
|
2896
|
+
error: {
|
2897
|
+
message: "Could not get patches. HTTP error: " + res.status + " " + res.statusText
|
2206
2898
|
}
|
2207
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(",")));
|
2208
2907
|
}
|
2209
|
-
|
2210
|
-
|
2211
|
-
|
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
|
+
}
|
2212
2936
|
return {
|
2213
|
-
|
2214
|
-
|
2215
|
-
|
2216
|
-
|
2217
|
-
"Cache-Control": "public, max-age=31536000, immutable"
|
2218
|
-
},
|
2219
|
-
body: bufferToReadableStream(buffer)
|
2937
|
+
patches,
|
2938
|
+
error: {
|
2939
|
+
message: `Could not parse search patches response. Error: ${zodValidationError.fromError(parsed.error)}`
|
2940
|
+
}
|
2220
2941
|
};
|
2221
2942
|
}
|
2222
|
-
|
2223
|
-
|
2224
|
-
|
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",
|
2225
2954
|
headers: {
|
2226
|
-
|
2227
|
-
"Content-
|
2955
|
+
...this.authHeaders,
|
2956
|
+
"Content-Type": "application/json"
|
2228
2957
|
},
|
2229
|
-
body:
|
2230
|
-
|
2231
|
-
|
2232
|
-
|
2233
|
-
|
2234
|
-
|
2235
|
-
|
2236
|
-
|
2237
|
-
|
2238
|
-
|
2239
|
-
|
2240
|
-
|
2241
|
-
}
|
2242
|
-
const res = {};
|
2243
|
-
const sortedPatchIds = files.map(file => parseInt(path__namespace["default"].basename(file), 10)).sort();
|
2244
|
-
for (const patchIdStr of sortedPatchIds) {
|
2245
|
-
const patchId = patchIdStr.toString();
|
2246
|
-
if (query.id && query.id.length > 0 && !query.id.includes(patchId)) {
|
2247
|
-
continue;
|
2248
|
-
}
|
2249
|
-
try {
|
2250
|
-
const currentParsedPatches = z.z.record(Patch).safeParse(JSON.parse(this.host.readFile(path__namespace["default"].join(this.patchesRootPath, LocalValServer.PATCHES_DIR, `${patchId}`)) || ""));
|
2251
|
-
if (!currentParsedPatches.success) {
|
2252
|
-
const msg = "Unexpected error reading patch. Patch did not parse correctly. Is there a mismatch in Val versions? Perhaps Val is misconfigured?";
|
2253
|
-
console.error(`Val: ${msg}`, {
|
2254
|
-
patchId,
|
2255
|
-
error: currentParsedPatches.error
|
2256
|
-
});
|
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) {
|
2257
2970
|
return {
|
2258
|
-
|
2259
|
-
json: {
|
2260
|
-
message: msg,
|
2261
|
-
details: {
|
2262
|
-
patchId,
|
2263
|
-
error: currentParsedPatches.error
|
2264
|
-
}
|
2265
|
-
}
|
2971
|
+
patchId: parsed.data.patchId
|
2266
2972
|
};
|
2267
2973
|
}
|
2268
|
-
const createdAt = patchId;
|
2269
|
-
for (const moduleIdStr in currentParsedPatches.data) {
|
2270
|
-
const moduleId = moduleIdStr;
|
2271
|
-
if (!res[moduleId]) {
|
2272
|
-
res[moduleId] = [];
|
2273
|
-
}
|
2274
|
-
res[moduleId].push({
|
2275
|
-
patch: currentParsedPatches.data[moduleId],
|
2276
|
-
patch_id: patchId,
|
2277
|
-
created_at: new Date(Number(createdAt)).toISOString()
|
2278
|
-
});
|
2279
|
-
}
|
2280
|
-
} catch (err) {
|
2281
|
-
const msg = `Unexpected error while reading patch file. The cache may be corrupted or Val may be misconfigured. Try deleting the cache directory.`;
|
2282
|
-
console.error(`Val: ${msg}`, {
|
2283
|
-
patchId,
|
2284
|
-
error: err,
|
2285
|
-
dir: this.patchesRootPath
|
2286
|
-
});
|
2287
2974
|
return {
|
2288
|
-
|
2289
|
-
|
2290
|
-
message: msg,
|
2291
|
-
details: {
|
2292
|
-
patchId,
|
2293
|
-
error: err === null || err === void 0 ? void 0 : err.toString()
|
2294
|
-
}
|
2975
|
+
error: {
|
2976
|
+
message: `Could not parse save patch response. Error: ${zodValidationError.fromError(parsed.error)}`
|
2295
2977
|
}
|
2296
2978
|
};
|
2297
2979
|
}
|
2298
|
-
|
2299
|
-
|
2300
|
-
|
2301
|
-
|
2302
|
-
|
2303
|
-
|
2304
|
-
|
2305
|
-
|
2306
|
-
|
2307
|
-
|
2308
|
-
|
2309
|
-
|
2310
|
-
getPatchFilePath(patchId) {
|
2311
|
-
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
|
+
});
|
2312
2992
|
}
|
2313
|
-
|
2314
|
-
return {
|
2315
|
-
|
2316
|
-
|
2317
|
-
|
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
|
+
};
|
2318
3020
|
}
|
2319
|
-
|
2320
|
-
|
2321
|
-
|
2322
|
-
|
2323
|
-
|
2324
|
-
|
2325
|
-
|
2326
|
-
|
2327
|
-
|
2328
|
-
|
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
|
+
};
|
2329
3032
|
});
|
2330
3033
|
}
|
2331
|
-
async
|
2332
|
-
const
|
2333
|
-
|
2334
|
-
|
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;
|
3058
|
+
}
|
3059
|
+
return {
|
3060
|
+
error: {
|
3061
|
+
message: `Could not parse file response. Error: ${zodValidationError.fromError(parsedFileResponse.error)}`
|
3062
|
+
}
|
3063
|
+
};
|
2335
3064
|
}
|
2336
|
-
return
|
2337
|
-
|
2338
|
-
|
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
|
+
};
|
3076
|
+
});
|
2339
3077
|
}
|
2340
|
-
async
|
2341
|
-
|
2342
|
-
|
2343
|
-
|
2344
|
-
|
2345
|
-
|
2346
|
-
|
2347
|
-
|
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
|
+
};
|
2348
3095
|
}
|
2349
3096
|
return {
|
2350
|
-
|
2351
|
-
json: await this.getPatchedModules(patches)
|
3097
|
+
data: Buffer.from(file.value, "base64").toString("utf-8")
|
2352
3098
|
};
|
2353
3099
|
}
|
2354
|
-
|
2355
|
-
|
2356
|
-
|
2357
|
-
|
2358
|
-
|
2359
|
-
|
2360
|
-
|
2361
|
-
|
2362
|
-
|
2363
|
-
async logout() {
|
2364
|
-
return this.badRequest();
|
2365
|
-
}
|
2366
|
-
}
|
2367
|
-
|
2368
|
-
function decodeJwt(token, secretKey) {
|
2369
|
-
const [headerBase64, payloadBase64, signatureBase64, ...rest] = token.split(".");
|
2370
|
-
if (!headerBase64 || !payloadBase64 || !signatureBase64 || rest.length > 0) {
|
2371
|
-
console.debug("Invalid JWT: format is not exactly {header}.{payload}.{signature}", token);
|
2372
|
-
return null;
|
2373
|
-
}
|
2374
|
-
try {
|
2375
|
-
const parsedHeader = JSON.parse(Buffer.from(headerBase64, "base64").toString("utf8"));
|
2376
|
-
const headerVerification = JwtHeaderSchema.safeParse(parsedHeader);
|
2377
|
-
if (!headerVerification.success) {
|
2378
|
-
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) {
|
2379
3109
|
return null;
|
2380
3110
|
}
|
2381
|
-
|
2382
|
-
|
3111
|
+
const file = filesRes.files.find(f => f.filePath === filePath);
|
3112
|
+
if (!file) {
|
2383
3113
|
return null;
|
2384
3114
|
}
|
2385
|
-
|
2386
|
-
|
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) {
|
2387
3124
|
return null;
|
2388
3125
|
}
|
2389
|
-
|
2390
|
-
|
2391
|
-
return null;
|
2392
|
-
}
|
2393
|
-
if (secretKey) {
|
2394
|
-
const signature = crypto__default["default"].createHmac("sha256", secretKey).update(`${headerBase64}.${payloadBase64}`).digest("base64");
|
2395
|
-
if (signature !== signatureBase64) {
|
2396
|
-
console.debug("Invalid JWT: invalid signature");
|
3126
|
+
const file = filesRes.files.find(f => f.filePath === filePath);
|
3127
|
+
if (!file) {
|
2397
3128
|
return null;
|
2398
3129
|
}
|
3130
|
+
return Buffer.from(file.value, "base64");
|
2399
3131
|
}
|
2400
|
-
|
2401
|
-
const
|
2402
|
-
|
2403
|
-
|
2404
|
-
|
2405
|
-
|
2406
|
-
|
2407
|
-
|
2408
|
-
|
2409
|
-
|
2410
|
-
|
2411
|
-
const
|
2412
|
-
|
2413
|
-
|
2414
|
-
|
2415
|
-
|
2416
|
-
|
2417
|
-
|
2418
|
-
|
2419
|
-
|
2420
|
-
|
2421
|
-
|
2422
|
-
|
2423
|
-
|
2424
|
-
}
|
2425
|
-
|
2426
|
-
|
2427
|
-
|
2428
|
-
|
2429
|
-
|
2430
|
-
|
2431
|
-
|
2432
|
-
|
2433
|
-
|
2434
|
-
|
2435
|
-
|
2436
|
-
|
2437
|
-
/** Remote FS dependent methods: */
|
2438
|
-
|
2439
|
-
async getModule(moduleId,
|
2440
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
2441
|
-
_options) {
|
2442
|
-
if (this.moduleCache) {
|
2443
|
-
return this.moduleCache[moduleId];
|
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"
|
3140
|
+
}
|
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
|
+
};
|
3149
|
+
}
|
3150
|
+
return {
|
3151
|
+
errors: [{
|
3152
|
+
message: `Could not parse metadata response. Error: ${zodValidationError.fromError(parsed.error)}`,
|
3153
|
+
filePath
|
3154
|
+
}]
|
3155
|
+
};
|
3156
|
+
}
|
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
|
+
};
|
2444
3169
|
}
|
2445
|
-
throw new Error("Module cache not initialized");
|
2446
3170
|
}
|
2447
|
-
async
|
2448
|
-
|
2449
|
-
|
2450
|
-
|
2451
|
-
|
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
|
+
};
|
2452
3188
|
}
|
2453
|
-
|
2454
|
-
return
|
2455
|
-
|
2456
|
-
|
2457
|
-
|
2458
|
-
|
2459
|
-
|
2460
|
-
|
2461
|
-
|
2462
|
-
|
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;
|
2463
3206
|
}
|
2464
|
-
|
2465
|
-
|
2466
|
-
|
2467
|
-
|
2468
|
-
|
2469
|
-
|
2470
|
-
|
2471
|
-
|
2472
|
-
|
2473
|
-
|
2474
|
-
const patchIds = patches.map(([patchId]) => patchId);
|
2475
|
-
const fetchRes = await fetch(url, {
|
2476
|
-
method: "POST",
|
2477
|
-
headers: getAuthHeaders(token, "application/json"),
|
2478
|
-
body: JSON.stringify({
|
2479
|
-
patchIds
|
2480
|
-
})
|
2481
|
-
});
|
2482
|
-
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
|
+
}
|
2483
3217
|
return {
|
2484
|
-
|
2485
|
-
|
3218
|
+
error: {
|
3219
|
+
message: `Could not parse delete patches response. Error: ${zodValidationError.fromError(parsed.error)}`
|
3220
|
+
}
|
2486
3221
|
};
|
2487
|
-
} else {
|
2488
|
-
return createJsonError(fetchRes);
|
2489
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
|
+
};
|
2490
3234
|
});
|
2491
3235
|
}
|
2492
|
-
async
|
2493
|
-
const params = createParams({
|
2494
|
-
root: this.apiOptions.root,
|
2495
|
-
commit,
|
2496
|
-
ext: ["ts", "js", "json"],
|
2497
|
-
package: ["@valbuild/core@" + this.options.versions.core, "@valbuild/next@" + this.options.versions.next],
|
2498
|
-
include: ["**/*.val.{js,ts},package.json,tsconfig.json,jsconfig.json"]
|
2499
|
-
});
|
2500
|
-
const url = new URL(`/v1/eval/${this.options.remote}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
|
3236
|
+
async commit(prepared, message, committer, newBranch) {
|
2501
3237
|
try {
|
2502
|
-
const
|
2503
|
-
|
2504
|
-
|
2505
|
-
|
2506
|
-
|
2507
|
-
|
2508
|
-
|
2509
|
-
|
2510
|
-
|
2511
|
-
|
2512
|
-
|
2513
|
-
|
2514
|
-
|
2515
|
-
|
2516
|
-
|
2517
|
-
|
2518
|
-
|
2519
|
-
|
2520
|
-
|
2521
|
-
|
2522
|
-
|
2523
|
-
|
2524
|
-
|
2525
|
-
details: "Invalid response: missing modules"
|
2526
|
-
};
|
2527
|
-
}
|
2528
|
-
if (error) {
|
2529
|
-
console.error("Could not initialize remote modules", error);
|
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
|
+
})
|
3257
|
+
});
|
3258
|
+
if (res.ok) {
|
3259
|
+
const parsed = CommitResponse.safeParse(await res.json());
|
3260
|
+
if (parsed.success) {
|
2530
3261
|
return {
|
2531
|
-
|
2532
|
-
|
2533
|
-
|
2534
|
-
...error
|
2535
|
-
}
|
3262
|
+
updatedFiles: parsed.data.updatedFiles,
|
3263
|
+
commit: parsed.data.commit,
|
3264
|
+
branch: parsed.data.branch
|
2536
3265
|
};
|
2537
3266
|
}
|
2538
|
-
this.moduleCache = json.modules;
|
2539
3267
|
return {
|
2540
|
-
|
3268
|
+
error: {
|
3269
|
+
message: `Could not parse commit response. Error: ${zodValidationError.fromError(parsed.error)}`
|
3270
|
+
}
|
2541
3271
|
};
|
2542
|
-
} else {
|
2543
|
-
return createJsonError(fetchRes);
|
2544
3272
|
}
|
3273
|
+
return {
|
3274
|
+
error: {
|
3275
|
+
message: "Could not commit. HTTP error: " + res.status + " " + res.statusText
|
3276
|
+
}
|
3277
|
+
};
|
2545
3278
|
} catch (err) {
|
2546
3279
|
return {
|
2547
|
-
|
2548
|
-
|
2549
|
-
message: "Failed to fetch: check network connection"
|
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"}`
|
2550
3282
|
}
|
2551
3283
|
};
|
2552
3284
|
}
|
2553
3285
|
}
|
2554
|
-
|
2555
|
-
|
2556
|
-
|
2557
|
-
|
2558
|
-
|
2559
|
-
|
2560
|
-
|
2561
|
-
|
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
|
2562
3302
|
});
|
2563
|
-
}
|
2564
|
-
const res = await withAuth(this.options.valSecret, cookies, errorMessageType, async data => {
|
2565
|
-
if (!this.moduleCache) {
|
2566
|
-
return this.init(commit, data.token);
|
2567
|
-
} else {
|
2568
|
-
return {
|
2569
|
-
status: 200
|
2570
|
-
};
|
2571
|
-
}
|
2572
|
-
});
|
2573
|
-
if (res.status === 200) {
|
2574
|
-
return fp.result.ok(undefined);
|
2575
3303
|
} else {
|
2576
|
-
|
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));
|
2577
3306
|
}
|
2578
3307
|
}
|
2579
|
-
/* Auth endpoints */
|
2580
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
|
+
}
|
2581
3340
|
async authorize(query) {
|
2582
3341
|
if (typeof query.redirect_to !== "string") {
|
2583
3342
|
return {
|
@@ -2587,7 +3346,7 @@ class ProxyValServer extends ValServer {
|
|
2587
3346
|
}
|
2588
3347
|
};
|
2589
3348
|
}
|
2590
|
-
const token =
|
3349
|
+
const token = crypto.randomUUID();
|
2591
3350
|
const redirectUrl = new URL(query.redirect_to);
|
2592
3351
|
const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
|
2593
3352
|
return {
|
@@ -2609,6 +3368,28 @@ class ProxyValServer extends ValServer {
|
|
2609
3368
|
};
|
2610
3369
|
}
|
2611
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
|
+
}
|
2612
3393
|
const {
|
2613
3394
|
success: callbackReqSuccess,
|
2614
3395
|
error: callbackReqError
|
@@ -2637,10 +3418,22 @@ class ProxyValServer extends ValServer {
|
|
2637
3418
|
};
|
2638
3419
|
}
|
2639
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
|
+
}
|
2640
3433
|
const cookie = encodeJwt({
|
2641
3434
|
...data,
|
2642
3435
|
exp // this is the client side exp
|
2643
|
-
},
|
3436
|
+
}, valSecret);
|
2644
3437
|
return {
|
2645
3438
|
status: 302,
|
2646
3439
|
cookies: {
|
@@ -2662,7 +3455,7 @@ class ProxyValServer extends ValServer {
|
|
2662
3455
|
redirectTo: callbackReqSuccess.redirect_uri || "/"
|
2663
3456
|
};
|
2664
3457
|
}
|
2665
|
-
async
|
3458
|
+
async log() {
|
2666
3459
|
return {
|
2667
3460
|
status: 200,
|
2668
3461
|
cookies: {
|
@@ -2676,8 +3469,41 @@ class ProxyValServer extends ValServer {
|
|
2676
3469
|
};
|
2677
3470
|
}
|
2678
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
|
+
}
|
2679
3497
|
return withAuth(this.options.valSecret, cookies, "session", async data => {
|
2680
|
-
|
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);
|
2681
3507
|
const fetchRes = await fetch(url, {
|
2682
3508
|
headers: getAuthHeaders(data.token, "application/json")
|
2683
3509
|
});
|
@@ -2686,7 +3512,7 @@ class ProxyValServer extends ValServer {
|
|
2686
3512
|
status: fetchRes.status,
|
2687
3513
|
json: {
|
2688
3514
|
mode: "proxy",
|
2689
|
-
|
3515
|
+
d: await this.callbacks.isEnabled(),
|
2690
3516
|
...(await fetchRes.json())
|
2691
3517
|
}
|
2692
3518
|
};
|
@@ -2702,8 +3528,17 @@ class ProxyValServer extends ValServer {
|
|
2702
3528
|
});
|
2703
3529
|
}
|
2704
3530
|
async consumeCode(code) {
|
2705
|
-
|
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);
|
2706
3538
|
url.searchParams.set("code", encodeURIComponent(code));
|
3539
|
+
if (!this.options.apiKey) {
|
3540
|
+
return null;
|
3541
|
+
}
|
2707
3542
|
return fetch(url, {
|
2708
3543
|
method: "POST",
|
2709
3544
|
headers: getAuthHeaders(this.options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
|
@@ -2727,186 +3562,453 @@ class ProxyValServer extends ValServer {
|
|
2727
3562
|
return null;
|
2728
3563
|
});
|
2729
3564
|
}
|
2730
|
-
getAuthorizeUrl(
|
2731
|
-
|
2732
|
-
|
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`));
|
2733
3574
|
url.searchParams.set("state", token);
|
2734
3575
|
return url.toString();
|
2735
3576
|
}
|
2736
3577
|
getAppErrorUrl(error) {
|
2737
|
-
|
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);
|
2738
3585
|
url.searchParams.set("error", encodeURIComponent(error));
|
2739
3586
|
return url.toString();
|
2740
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
|
+
}
|
2741
3655
|
|
2742
|
-
|
2743
|
-
async
|
2744
|
-
|
2745
|
-
|
2746
|
-
|
2747
|
-
|
2748
|
-
|
2749
|
-
|
2750
|
-
|
2751
|
-
|
2752
|
-
|
2753
|
-
|
2754
|
-
|
2755
|
-
|
2756
|
-
|
2757
|
-
|
2758
|
-
|
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
|
2759
3693
|
});
|
2760
|
-
|
2761
|
-
|
2762
|
-
|
2763
|
-
|
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
|
2764
3867
|
};
|
2765
|
-
} else {
|
2766
|
-
return createJsonError(fetchRes);
|
2767
3868
|
}
|
2768
|
-
|
2769
|
-
|
2770
|
-
|
2771
|
-
|
2772
|
-
|
2773
|
-
|
2774
|
-
|
2775
|
-
|
2776
|
-
|
2777
|
-
|
2778
|
-
|
2779
|
-
|
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
|
2780
3887
|
}
|
2781
3888
|
};
|
2782
3889
|
}
|
2783
|
-
|
2784
|
-
|
2785
|
-
|
2786
|
-
|
2787
|
-
|
2788
|
-
|
2789
|
-
|
2790
|
-
|
2791
|
-
|
2792
|
-
|
2793
|
-
|
2794
|
-
|
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
|
2795
3915
|
};
|
2796
|
-
} else {
|
2797
|
-
return createJsonError(fetchRes);
|
2798
3916
|
}
|
2799
|
-
}
|
3917
|
+
}
|
3918
|
+
return {
|
3919
|
+
status: 200,
|
3920
|
+
json: {
|
3921
|
+
schemaSha,
|
3922
|
+
modules
|
3923
|
+
}
|
3924
|
+
};
|
2800
3925
|
}
|
2801
|
-
async
|
2802
|
-
const
|
2803
|
-
if (
|
3926
|
+
async postSave(body, cookies) {
|
3927
|
+
const auth = this.getAuth(cookies);
|
3928
|
+
if (auth.error) {
|
2804
3929
|
return {
|
2805
3930
|
status: 401,
|
2806
3931
|
json: {
|
2807
|
-
message:
|
3932
|
+
message: auth.error
|
2808
3933
|
}
|
2809
3934
|
};
|
2810
3935
|
}
|
2811
|
-
const
|
2812
|
-
|
3936
|
+
const PostSaveBody = z.z.object({
|
3937
|
+
patchIds: z.z.array(z.z.string().refine(id => true // TODO:
|
3938
|
+
))
|
2813
3939
|
});
|
2814
|
-
|
2815
|
-
|
2816
|
-
|
2817
|
-
|
2818
|
-
|
2819
|
-
|
2820
|
-
|
2821
|
-
|
2822
|
-
|
2823
|
-
|
2824
|
-
|
2825
|
-
|
2826
|
-
|
2827
|
-
|
2828
|
-
|
2829
|
-
|
2830
|
-
|
2831
|
-
|
2832
|
-
|
2833
|
-
|
2834
|
-
|
2835
|
-
|
2836
|
-
|
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);
|
2837
3968
|
return {
|
2838
|
-
status:
|
2839
|
-
json:
|
3969
|
+
status: 200,
|
3970
|
+
json: {} // TODO:
|
2840
3971
|
};
|
2841
|
-
} else {
|
2842
|
-
return createJsonError(fetchRes);
|
2843
|
-
}
|
2844
|
-
});
|
2845
|
-
}
|
2846
|
-
async getMetadata(filePath, sha256) {
|
2847
|
-
const url = new URL(`/v1/metadata/${this.options.remote}${filePath}?commit=${this.options.git.commit}${sha256 ? `&sha256=${sha256}` : ""}`, this.options.valContentUrl);
|
2848
|
-
const fetchRes = await fetch(url, {
|
2849
|
-
headers: {
|
2850
|
-
Authorization: `Bearer ${this.options.apiKey}`
|
2851
|
-
}
|
2852
|
-
});
|
2853
|
-
if (fetchRes.status === 200) {
|
2854
|
-
const json = await fetchRes.json();
|
2855
|
-
if (json.type === "file") {
|
2856
|
-
return json;
|
2857
|
-
} else if (json.type === "image") {
|
2858
|
-
return json;
|
2859
3972
|
}
|
3973
|
+
return {
|
3974
|
+
status: 401,
|
3975
|
+
json: {
|
3976
|
+
message: "Unauthorized"
|
3977
|
+
}
|
3978
|
+
};
|
3979
|
+
} else {
|
3980
|
+
throw new Error("Invalid server ops");
|
2860
3981
|
}
|
2861
|
-
return undefined;
|
2862
3982
|
}
|
2863
|
-
|
2864
|
-
|
2865
|
-
|
2866
|
-
|
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);
|
2867
4000
|
}
|
2868
|
-
|
2869
|
-
|
2870
|
-
|
2871
|
-
|
2872
|
-
|
2873
|
-
if (fetchRes.status === 200) {
|
2874
|
-
// TODO: does this stream data?
|
2875
|
-
if (fetchRes.body) {
|
2876
|
-
return {
|
2877
|
-
status: fetchRes.status,
|
2878
|
-
headers: {
|
2879
|
-
"Content-Type": fetchRes.headers.get("Content-Type") || "",
|
2880
|
-
"Content-Length": fetchRes.headers.get("Content-Length") || "0",
|
2881
|
-
"Cache-Control": fetchRes.headers.get("Cache-Control") || ""
|
2882
|
-
},
|
2883
|
-
body: fetchRes.body
|
2884
|
-
};
|
2885
|
-
} else {
|
2886
|
-
return {
|
2887
|
-
status: 500,
|
2888
|
-
json: {
|
2889
|
-
message: "No body in response"
|
2890
|
-
}
|
2891
|
-
};
|
2892
|
-
}
|
4001
|
+
if (fileBuffer) {
|
4002
|
+
return {
|
4003
|
+
status: 200,
|
4004
|
+
body: bufferToReadableStream(fileBuffer)
|
4005
|
+
};
|
2893
4006
|
} else {
|
2894
|
-
if (!(reqHeaders.host && reqHeaders["x-forwarded-proto"])) {
|
2895
|
-
return {
|
2896
|
-
status: 500,
|
2897
|
-
json: {
|
2898
|
-
message: "Missing host or x-forwarded-proto header"
|
2899
|
-
}
|
2900
|
-
};
|
2901
|
-
}
|
2902
|
-
const host = `${reqHeaders["x-forwarded-proto"]}://${reqHeaders["host"]}`;
|
2903
|
-
const staticPublicUrl = new URL(filePath.slice("/public".length), host).toString();
|
2904
|
-
const fetchRes = await fetch(staticPublicUrl);
|
2905
4007
|
return {
|
2906
|
-
status:
|
2907
|
-
|
2908
|
-
|
2909
|
-
|
4008
|
+
status: 404,
|
4009
|
+
json: {
|
4010
|
+
message: "File not found"
|
4011
|
+
}
|
2910
4012
|
};
|
2911
4013
|
}
|
2912
4014
|
}
|
@@ -3011,22 +4113,6 @@ function getStateFromCookie(stateCookie) {
|
|
3011
4113
|
};
|
3012
4114
|
}
|
3013
4115
|
}
|
3014
|
-
async function createJsonError(fetchRes) {
|
3015
|
-
var _fetchRes$headers$get;
|
3016
|
-
if ((_fetchRes$headers$get = fetchRes.headers.get("Content-Type")) !== null && _fetchRes$headers$get !== void 0 && _fetchRes$headers$get.includes("application/json")) {
|
3017
|
-
return {
|
3018
|
-
status: fetchRes.status,
|
3019
|
-
json: await fetchRes.json()
|
3020
|
-
};
|
3021
|
-
}
|
3022
|
-
console.error("Unexpected failure (did not get a json) - Val down?", fetchRes.status, await fetchRes.text());
|
3023
|
-
return {
|
3024
|
-
status: fetchRes.status,
|
3025
|
-
json: {
|
3026
|
-
message: "Unexpected failure (did not get a json) - Val down?"
|
3027
|
-
}
|
3028
|
-
};
|
3029
|
-
}
|
3030
4116
|
function createStateCookie(state) {
|
3031
4117
|
return Buffer.from(JSON.stringify(state), "utf8").toString("base64");
|
3032
4118
|
}
|
@@ -3098,42 +4184,147 @@ function getAuthHeaders(token, type) {
|
|
3098
4184
|
Authorization: `Bearer ${token}`
|
3099
4185
|
};
|
3100
4186
|
}
|
3101
|
-
|
3102
|
-
|
3103
|
-
|
3104
|
-
|
3105
|
-
|
3106
|
-
|
3107
|
-
|
3108
|
-
|
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;
|
3109
4203
|
}
|
3110
|
-
|
3111
|
-
paramsString += `${key}=${encodeURIComponent(param)}`;
|
3112
|
-
}
|
3113
|
-
if (paramIdx < Object.keys(params).length - 1) {
|
3114
|
-
paramsString += "&";
|
4204
|
+
controller.close();
|
3115
4205
|
}
|
3116
|
-
|
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
|
+
};
|
3117
4217
|
}
|
3118
|
-
|
4218
|
+
if (overrideHost) {
|
4219
|
+
return overrideHost + "?redirect_to=" + encodeURIComponent(query.redirect_to);
|
4220
|
+
}
|
4221
|
+
return query.redirect_to;
|
3119
4222
|
}
|
3120
4223
|
|
3121
|
-
|
3122
|
-
|
3123
|
-
|
3124
|
-
|
3125
|
-
|
3126
|
-
|
3127
|
-
|
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;
|
3128
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);
|
3129
4317
|
}
|
3130
4318
|
async function initHandlerOptions(route, opts) {
|
3131
4319
|
const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
|
3132
4320
|
const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
|
3133
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";
|
3134
4326
|
if (isProxyMode) {
|
3135
4327
|
var _opts$versions, _opts$versions2;
|
3136
|
-
const valBuildUrl = opts.valBuildUrl || process.env.VAL_BUILD_URL || "https://app.val.build";
|
3137
4328
|
if (!maybeApiKey || !maybeValSecret) {
|
3138
4329
|
throw new Error("VAL_API_KEY and VAL_SECRET env vars must both be set in proxy mode");
|
3139
4330
|
}
|
@@ -3146,9 +4337,8 @@ async function initHandlerOptions(route, opts) {
|
|
3146
4337
|
if (!maybeGitBranch) {
|
3147
4338
|
throw new Error("VAL_GIT_BRANCH env var must be set in proxy mode");
|
3148
4339
|
}
|
3149
|
-
|
3150
|
-
|
3151
|
-
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.");
|
3152
4342
|
}
|
3153
4343
|
const coreVersion = (_opts$versions = opts.versions) === null || _opts$versions === void 0 ? void 0 : _opts$versions.core;
|
3154
4344
|
if (!coreVersion) {
|
@@ -3159,43 +4349,40 @@ async function initHandlerOptions(route, opts) {
|
|
3159
4349
|
throw new Error("Could not determine version of @valbuild/next");
|
3160
4350
|
}
|
3161
4351
|
return {
|
3162
|
-
mode: "
|
4352
|
+
mode: "http",
|
3163
4353
|
route,
|
3164
4354
|
apiKey: maybeApiKey,
|
3165
4355
|
valSecret: maybeValSecret,
|
3166
|
-
|
4356
|
+
commit: maybeGitCommit,
|
4357
|
+
branch: maybeGitBranch,
|
4358
|
+
root: opts.root,
|
4359
|
+
project: maybeValProject,
|
4360
|
+
valEnableRedirectUrl,
|
4361
|
+
valDisableRedirectUrl,
|
3167
4362
|
valContentUrl,
|
3168
|
-
|
3169
|
-
core: coreVersion,
|
3170
|
-
next: nextVersion
|
3171
|
-
},
|
3172
|
-
git: {
|
3173
|
-
commit: maybeGitCommit,
|
3174
|
-
branch: maybeGitBranch
|
3175
|
-
},
|
3176
|
-
remote: maybeValRemote,
|
3177
|
-
valEnableRedirectUrl: opts.valEnableRedirectUrl || process.env.VAL_ENABLE_REDIRECT_URL,
|
3178
|
-
valDisableRedirectUrl: opts.valDisableRedirectUrl || process.env.VAL_DISABLE_REDIRECT_URL
|
4363
|
+
valBuildUrl
|
3179
4364
|
};
|
3180
4365
|
} else {
|
3181
4366
|
const cwd = process.cwd();
|
3182
|
-
const
|
3183
|
-
const git = await safeReadGit(cwd);
|
4367
|
+
const valBuildUrl = opts.valBuildUrl || process.env.VAL_BUILD_URL || "https://app.val.build";
|
3184
4368
|
return {
|
3185
|
-
mode: "
|
3186
|
-
|
3187
|
-
|
3188
|
-
valDisableRedirectUrl
|
3189
|
-
|
3190
|
-
|
3191
|
-
|
3192
|
-
|
4369
|
+
mode: "fs",
|
4370
|
+
cwd,
|
4371
|
+
route,
|
4372
|
+
valDisableRedirectUrl,
|
4373
|
+
valEnableRedirectUrl,
|
4374
|
+
valBuildUrl,
|
4375
|
+
apiKey: maybeApiKey,
|
4376
|
+
valSecret: maybeValSecret,
|
4377
|
+
project: maybeValProject
|
3193
4378
|
};
|
3194
4379
|
}
|
3195
4380
|
}
|
4381
|
+
|
4382
|
+
// TODO: remove
|
3196
4383
|
async function safeReadGit(cwd) {
|
3197
4384
|
async function findGitHead(currentDir, depth) {
|
3198
|
-
const gitHeadPath =
|
4385
|
+
const gitHeadPath = fsPath__namespace.join(currentDir, ".git", "HEAD");
|
3199
4386
|
if (depth > 1000) {
|
3200
4387
|
console.error(`Reached max depth while scanning for .git folder. Current working dir: ${cwd}.`);
|
3201
4388
|
return {
|
@@ -3219,7 +4406,7 @@ async function safeReadGit(cwd) {
|
|
3219
4406
|
};
|
3220
4407
|
}
|
3221
4408
|
} catch (error) {
|
3222
|
-
const parentDir =
|
4409
|
+
const parentDir = fsPath__namespace.dirname(currentDir);
|
3223
4410
|
|
3224
4411
|
// We've reached the root directory
|
3225
4412
|
if (parentDir === currentDir) {
|
@@ -3243,7 +4430,7 @@ async function safeReadGit(cwd) {
|
|
3243
4430
|
}
|
3244
4431
|
async function readCommit(gitDir, branchName) {
|
3245
4432
|
try {
|
3246
|
-
return (await fs.promises.readFile(
|
4433
|
+
return (await fs.promises.readFile(fsPath__namespace.join(gitDir, ".git", "refs", "heads", branchName), "utf-8")).trim();
|
3247
4434
|
} catch (err) {
|
3248
4435
|
return undefined;
|
3249
4436
|
}
|
@@ -3260,10 +4447,6 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
3260
4447
|
return async req => {
|
3261
4448
|
var _req$method;
|
3262
4449
|
const valServer = await valServerPromise;
|
3263
|
-
const requestHeaders = {
|
3264
|
-
host: req.headers.get("host"),
|
3265
|
-
"x-forwarded-proto": req.headers.get("x-forwarded-proto")
|
3266
|
-
};
|
3267
4450
|
const url = new URL(req.url);
|
3268
4451
|
if (!url.pathname.startsWith(route)) {
|
3269
4452
|
const error = {
|
@@ -3324,35 +4507,40 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
3324
4507
|
return convert(await valServer.disable({
|
3325
4508
|
redirect_to: url.searchParams.get("redirect_to") || undefined
|
3326
4509
|
}));
|
3327
|
-
} else if (method === "POST" && path === "/
|
3328
|
-
const body = await req.json();
|
3329
|
-
return convert(await valServer.postCommit(body, getCookies(req, [VAL_SESSION_COOKIE]), requestHeaders));
|
3330
|
-
} else if (method === "POST" && path === "/validate") {
|
4510
|
+
} else if (method === "POST" && path === "/save") {
|
3331
4511
|
const body = await req.json();
|
3332
|
-
return convert(await valServer.
|
3333
|
-
|
3334
|
-
|
3335
|
-
|
3336
|
-
|
3337
|
-
|
3338
|
-
|
3339
|
-
|
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]))));
|
3340
4531
|
} else if (method === "GET" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
3341
4532
|
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.getPatches({
|
3342
|
-
|
4533
|
+
authors: url.searchParams.getAll("author")
|
3343
4534
|
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
3344
|
-
} else if (method === "POST" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
3345
|
-
const body = await req.json();
|
3346
|
-
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.postPatches(body, getCookies(req, [VAL_SESSION_COOKIE]))));
|
3347
4535
|
} else if (method === "DELETE" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
3348
4536
|
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.deletePatches({
|
3349
4537
|
id: url.searchParams.getAll("id")
|
3350
4538
|
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
3351
|
-
} else if (path.startsWith(FILES_PATH_PREFIX)) {
|
4539
|
+
} else if (method === "GET" && path.startsWith(FILES_PATH_PREFIX)) {
|
3352
4540
|
const treePath = path.slice(FILES_PATH_PREFIX.length);
|
3353
4541
|
return convert(await valServer.getFiles(treePath, {
|
3354
|
-
|
3355
|
-
}
|
4542
|
+
patch_id: url.searchParams.get("patch_id") || undefined
|
4543
|
+
}));
|
3356
4544
|
} else {
|
3357
4545
|
return convert({
|
3358
4546
|
status: 404,
|
@@ -3404,10 +4592,10 @@ class ValFSHost {
|
|
3404
4592
|
return this.currentDirectory;
|
3405
4593
|
}
|
3406
4594
|
getCanonicalFileName(fileName) {
|
3407
|
-
if (
|
3408
|
-
return
|
4595
|
+
if (fsPath__namespace["default"].isAbsolute(fileName)) {
|
4596
|
+
return fsPath__namespace["default"].normalize(fileName);
|
3409
4597
|
}
|
3410
|
-
return
|
4598
|
+
return fsPath__namespace["default"].resolve(this.getCurrentDirectory(), fileName);
|
3411
4599
|
}
|
3412
4600
|
fileExists(fileName) {
|
3413
4601
|
return this.valFS.fileExists(fileName);
|
@@ -3420,6 +4608,14 @@ class ValFSHost {
|
|
3420
4608
|
}
|
3421
4609
|
}
|
3422
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
|
+
|
3423
4619
|
// TODO: find a better name? transformFixesToPatch?
|
3424
4620
|
async function createFixPatch(config, apply, sourcePath, validationError) {
|
3425
4621
|
async function getImageMetadata() {
|
@@ -3428,7 +4624,7 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
3428
4624
|
// TODO:
|
3429
4625
|
throw Error("Cannot fix image without a file reference");
|
3430
4626
|
}
|
3431
|
-
const filename =
|
4627
|
+
const filename = fsPath__namespace["default"].join(config.projectRoot, fileRef);
|
3432
4628
|
const buffer = fs__default["default"].readFileSync(filename);
|
3433
4629
|
return extractImageMetadata(filename, buffer);
|
3434
4630
|
}
|
@@ -3438,7 +4634,7 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
3438
4634
|
// TODO:
|
3439
4635
|
throw Error("Cannot fix file without a file reference");
|
3440
4636
|
}
|
3441
|
-
const filename =
|
4637
|
+
const filename = fsPath__namespace["default"].join(config.projectRoot, fileRef);
|
3442
4638
|
const buffer = fs__default["default"].readFileSync(filename);
|
3443
4639
|
return extractFileMetadata(fileRef, buffer);
|
3444
4640
|
}
|
@@ -3604,7 +4800,6 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
3604
4800
|
};
|
3605
4801
|
}
|
3606
4802
|
|
3607
|
-
exports.LocalValServer = LocalValServer;
|
3608
4803
|
exports.Patch = Patch;
|
3609
4804
|
exports.PatchJSON = PatchJSON;
|
3610
4805
|
exports.Service = Service;
|