@valbuild/server 0.33.0 → 0.35.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/LocalValServer.d.ts +26 -16
- package/dist/declarations/src/SerializedModuleContent.d.ts +1 -1
- package/dist/declarations/src/Service.d.ts +1 -1
- package/dist/declarations/src/ValServer.d.ts +45 -14
- package/dist/declarations/src/{hosting.d.ts → createValApiRouter.d.ts} +4 -3
- package/dist/declarations/src/index.d.ts +1 -2
- package/dist/declarations/src/patchValFile.d.ts +1 -2
- package/dist/valbuild-server.cjs.dev.js +660 -412
- package/dist/valbuild-server.cjs.prod.js +660 -412
- package/dist/valbuild-server.esm.js +658 -409
- package/package.json +7 -6
- package/dist/declarations/src/createRequestHandler.d.ts +0 -3
@@ -7,14 +7,13 @@ 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 crypto = require('crypto');
|
10
11
|
var path = require('path');
|
11
12
|
var fs = require('fs');
|
12
13
|
var sucrase = require('sucrase');
|
13
|
-
var express = require('express');
|
14
|
-
var server = require('@valbuild/ui/server');
|
15
14
|
var z = require('zod');
|
16
|
-
var
|
17
|
-
var
|
15
|
+
var internal = require('@valbuild/shared/internal');
|
16
|
+
var server = require('@valbuild/ui/server');
|
18
17
|
var sizeOf = require('image-size');
|
19
18
|
|
20
19
|
function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
|
@@ -38,11 +37,10 @@ function _interopNamespace(e) {
|
|
38
37
|
}
|
39
38
|
|
40
39
|
var ts__default = /*#__PURE__*/_interopDefault(ts);
|
40
|
+
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
41
41
|
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
42
42
|
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
43
|
-
var express__default = /*#__PURE__*/_interopDefault(express);
|
44
43
|
var z__default = /*#__PURE__*/_interopDefault(z);
|
45
|
-
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
46
44
|
var sizeOf__default = /*#__PURE__*/_interopDefault(sizeOf);
|
47
45
|
|
48
46
|
class ValSyntaxError {
|
@@ -496,7 +494,7 @@ function getPointerFromPath(node, path) {
|
|
496
494
|
return childNode;
|
497
495
|
}
|
498
496
|
if (childNode.value === undefined) {
|
499
|
-
return fp.result.err(new patch.PatchError(
|
497
|
+
return fp.result.err(new patch.PatchError(`Path refers to non-existing object/array: ${path.join("/")}`));
|
500
498
|
}
|
501
499
|
targetNode = childNode.value;
|
502
500
|
}
|
@@ -618,6 +616,64 @@ class TSOps {
|
|
618
616
|
}
|
619
617
|
}
|
620
618
|
|
619
|
+
const ops = new TSOps(document => {
|
620
|
+
return fp.pipe(analyzeValModule(document), fp.result.map(({
|
621
|
+
source
|
622
|
+
}) => source));
|
623
|
+
});
|
624
|
+
|
625
|
+
// TODO: rename to patchValFiles since we may write multiple files
|
626
|
+
const patchValFile = async (id, valConfigPath, patch$1, sourceFileHandler, runtime) => {
|
627
|
+
const timeId = crypto.randomUUID();
|
628
|
+
console.time("patchValFile" + timeId);
|
629
|
+
const filePath = sourceFileHandler.resolveSourceModulePath(valConfigPath, `.${id}.val`);
|
630
|
+
const sourceFile = sourceFileHandler.getSourceFile(filePath);
|
631
|
+
if (!sourceFile) {
|
632
|
+
throw Error(`Source file ${filePath} not found`);
|
633
|
+
}
|
634
|
+
const derefRes = core.derefPatch(patch$1, sourceFile, ops);
|
635
|
+
if (fp.result.isErr(derefRes)) {
|
636
|
+
throw derefRes.error;
|
637
|
+
}
|
638
|
+
const dereferencedPatch = derefRes.value.dereferencedPatch; // TODO: add ref changes to remote replace/add, ...
|
639
|
+
const newSourceFile = patchSourceFile(sourceFile, dereferencedPatch);
|
640
|
+
if (fp.result.isErr(newSourceFile)) {
|
641
|
+
if (newSourceFile.error instanceof patch.PatchError) {
|
642
|
+
throw newSourceFile.error;
|
643
|
+
} else {
|
644
|
+
throw new Error(`${filePath}\n${flatMapErrors(newSourceFile.error, error => formatSyntaxError(error, sourceFile)).join("\n")}`);
|
645
|
+
}
|
646
|
+
}
|
647
|
+
for (const [filePath, content] of Object.entries(derefRes.value.fileUpdates)) {
|
648
|
+
// Evaluate if we want to make these writes (more) atomic with a temp file and a move.
|
649
|
+
// This can potentially fill mid-way if there is not enough space on disk for example...
|
650
|
+
// However, that might be add add bit more complexity in our host and virtual file systems?
|
651
|
+
// Example:
|
652
|
+
// const tempFilePath = sourceFileHandler.writeTempFile(
|
653
|
+
// Buffer.from(content, "base64").toString("binary")
|
654
|
+
// );
|
655
|
+
// sourceFileHandler.moveFile(tempFilePath, "." + filePath);
|
656
|
+
// TODO: ensure that directory exists
|
657
|
+
if (content.startsWith("data:/image/svg+xml")) {
|
658
|
+
sourceFileHandler.writeFile("." + filePath, convertDataUrlToBase64(content).toString("utf8"), "utf8");
|
659
|
+
} else {
|
660
|
+
sourceFileHandler.writeFile("." + filePath, convertDataUrlToBase64(content).toString("binary"), "binary");
|
661
|
+
}
|
662
|
+
}
|
663
|
+
sourceFileHandler.writeSourceFile(newSourceFile.value);
|
664
|
+
console.timeEnd("patchValFile" + timeId);
|
665
|
+
};
|
666
|
+
function convertDataUrlToBase64(dataUrl) {
|
667
|
+
const base64 = dataUrl.slice(dataUrl.indexOf(",") + 1);
|
668
|
+
return Buffer.from(base64, "base64");
|
669
|
+
}
|
670
|
+
const patchSourceFile = (sourceFile, patch$1) => {
|
671
|
+
if (typeof sourceFile === "string") {
|
672
|
+
return patch.applyPatch(ts__default["default"].createSourceFile("<val>", sourceFile, ts__default["default"].ScriptTarget.ES2015), ops, patch$1);
|
673
|
+
}
|
674
|
+
return patch.applyPatch(sourceFile, ops, patch$1);
|
675
|
+
};
|
676
|
+
|
621
677
|
const readValFile = async (id, valConfigPath, runtime) => {
|
622
678
|
const context = runtime.newContext();
|
623
679
|
try {
|
@@ -695,62 +751,6 @@ globalThis.valModule = {
|
|
695
751
|
}
|
696
752
|
};
|
697
753
|
|
698
|
-
const ops = new TSOps(document => {
|
699
|
-
return fp.pipe(analyzeValModule(document), fp.result.map(({
|
700
|
-
source
|
701
|
-
}) => source));
|
702
|
-
});
|
703
|
-
|
704
|
-
// TODO: rename to patchValFiles since we may write multiple files
|
705
|
-
const patchValFile = async (id, valConfigPath, patch$1, sourceFileHandler, runtime) => {
|
706
|
-
const filePath = sourceFileHandler.resolveSourceModulePath(valConfigPath, `.${id}.val`);
|
707
|
-
const sourceFile = sourceFileHandler.getSourceFile(filePath);
|
708
|
-
if (!sourceFile) {
|
709
|
-
throw Error(`Source file ${filePath} not found`);
|
710
|
-
}
|
711
|
-
const derefRes = core.derefPatch(patch$1, sourceFile, ops);
|
712
|
-
if (fp.result.isErr(derefRes)) {
|
713
|
-
throw derefRes.error;
|
714
|
-
}
|
715
|
-
const dereferencedPatch = derefRes.value.dereferencedPatch; // TODO: add ref changes to remote replace/add, ...
|
716
|
-
const newSourceFile = patchSourceFile(sourceFile, dereferencedPatch);
|
717
|
-
if (fp.result.isErr(newSourceFile)) {
|
718
|
-
if (newSourceFile.error instanceof patch.PatchError) {
|
719
|
-
throw newSourceFile.error;
|
720
|
-
} else {
|
721
|
-
throw new Error(`${filePath}\n${flatMapErrors(newSourceFile.error, error => formatSyntaxError(error, sourceFile)).join("\n")}`);
|
722
|
-
}
|
723
|
-
}
|
724
|
-
for (const [filePath, content] of Object.entries(derefRes.value.fileUpdates)) {
|
725
|
-
// Evaluate if we want to make these writes (more) atomic with a temp file and a move.
|
726
|
-
// This can potentially fill mid-way if there is not enough space on disk for example...
|
727
|
-
// However, that might be add add bit more complexity in our host and virtual file systems?
|
728
|
-
// Example:
|
729
|
-
// const tempFilePath = sourceFileHandler.writeTempFile(
|
730
|
-
// Buffer.from(content, "base64").toString("binary")
|
731
|
-
// );
|
732
|
-
// sourceFileHandler.moveFile(tempFilePath, "." + filePath);
|
733
|
-
// TODO: ensure that directory exists
|
734
|
-
if (content.startsWith("data:/image/svg+xml")) {
|
735
|
-
sourceFileHandler.writeFile("." + filePath, convertDataUrlToBase64(content).toString("utf8"), "utf8");
|
736
|
-
} else {
|
737
|
-
sourceFileHandler.writeFile("." + filePath, convertDataUrlToBase64(content).toString("binary"), "binary");
|
738
|
-
}
|
739
|
-
}
|
740
|
-
sourceFileHandler.writeSourceFile(newSourceFile.value);
|
741
|
-
return readValFile(id, valConfigPath, runtime);
|
742
|
-
};
|
743
|
-
function convertDataUrlToBase64(dataUrl) {
|
744
|
-
const base64 = dataUrl.slice(dataUrl.indexOf(",") + 1);
|
745
|
-
return Buffer.from(base64, "base64");
|
746
|
-
}
|
747
|
-
const patchSourceFile = (sourceFile, patch$1) => {
|
748
|
-
if (typeof sourceFile === "string") {
|
749
|
-
return patch.applyPatch(ts__default["default"].createSourceFile("<val>", sourceFile, ts__default["default"].ScriptTarget.ES2015), ops, patch$1);
|
750
|
-
}
|
751
|
-
return patch.applyPatch(sourceFile, ops, patch$1);
|
752
|
-
};
|
753
|
-
|
754
754
|
const getCompilerOptions = (rootDir, parseConfigHost) => {
|
755
755
|
const tsConfigPath = path__namespace["default"].resolve(rootDir, "tsconfig.json");
|
756
756
|
const jsConfigPath = path__namespace["default"].resolve(rootDir, "jsconfig.json");
|
@@ -797,7 +797,9 @@ class ValSourceFileHandler {
|
|
797
797
|
}
|
798
798
|
}
|
799
799
|
writeSourceFile(sourceFile) {
|
800
|
-
return this.writeFile(sourceFile.fileName,
|
800
|
+
return this.writeFile(sourceFile.fileName,
|
801
|
+
// https://github.com/microsoft/TypeScript/issues/36174
|
802
|
+
unescape(sourceFile.text.replace(/\\u/g, "%u")), "utf8");
|
801
803
|
}
|
802
804
|
writeFile(filePath, content, encoding) {
|
803
805
|
this.host.writeFile(filePath, content, encoding);
|
@@ -815,7 +817,7 @@ class ValSourceFileHandler {
|
|
815
817
|
const JsFileLookupMapping = [
|
816
818
|
// NOTE: first one matching will be used
|
817
819
|
[".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js", ".esm.js", ".mjs.js"]]];
|
818
|
-
const MAX_CACHE_SIZE =
|
820
|
+
const MAX_CACHE_SIZE = 10 * 1024 * 1024; // 10 mb
|
819
821
|
const MAX_OBJECT_KEY_SIZE = 2 ** 27; // https://stackoverflow.com/questions/13367391/is-there-a-limit-on-length-of-the-key-string-in-js-object
|
820
822
|
|
821
823
|
class ValModuleLoader {
|
@@ -959,6 +961,11 @@ async function newValQuickJSRuntime(quickJSModule, moduleLoader, {
|
|
959
961
|
value: "export default new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'react' in this file`) } } } )"
|
960
962
|
};
|
961
963
|
}
|
964
|
+
if (modulePath === "./ValNextProvider") {
|
965
|
+
return {
|
966
|
+
value: "export const ValNextProvider = new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'ValProvider' in this file`) } } } )"
|
967
|
+
};
|
968
|
+
}
|
962
969
|
return {
|
963
970
|
value: moduleLoader.getModule(modulePath)
|
964
971
|
};
|
@@ -994,6 +1001,11 @@ async function newValQuickJSRuntime(quickJSModule, moduleLoader, {
|
|
994
1001
|
value: requestedName
|
995
1002
|
};
|
996
1003
|
}
|
1004
|
+
if (requestedName === "./ValNextProvider") {
|
1005
|
+
return {
|
1006
|
+
value: requestedName
|
1007
|
+
};
|
1008
|
+
}
|
997
1009
|
const modulePath = moduleLoader.resolveModulePath(baseModuleName, requestedName);
|
998
1010
|
return {
|
999
1011
|
value: modulePath
|
@@ -1052,25 +1064,6 @@ class Service {
|
|
1052
1064
|
}
|
1053
1065
|
}
|
1054
1066
|
|
1055
|
-
function createRequestHandler(valServer) {
|
1056
|
-
const router = express.Router();
|
1057
|
-
router.use("/static", server.createRequestHandler());
|
1058
|
-
router.get("/session", valServer.session.bind(valServer));
|
1059
|
-
router.get("/authorize", valServer.authorize.bind(valServer));
|
1060
|
-
router.get("/callback", valServer.callback.bind(valServer));
|
1061
|
-
router.get("/logout", valServer.logout.bind(valServer));
|
1062
|
-
router.post("/patches/*", express__default["default"].json({
|
1063
|
-
type: "application/json",
|
1064
|
-
limit: "10mb"
|
1065
|
-
}), valServer.postPatches.bind(valServer)).get("/patches/*", valServer.getPatches.bind(valServer));
|
1066
|
-
router.post("/commit", valServer.commit.bind(valServer));
|
1067
|
-
router.get("/enable", valServer.enable.bind(valServer));
|
1068
|
-
router.get("/disable", valServer.disable.bind(valServer));
|
1069
|
-
router.get("/tree/*", valServer.getTree.bind(valServer));
|
1070
|
-
router.get("/files/*", valServer.getFiles.bind(valServer));
|
1071
|
-
return router;
|
1072
|
-
}
|
1073
|
-
|
1074
1067
|
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)]));
|
1075
1068
|
|
1076
1069
|
/**
|
@@ -1113,6 +1106,182 @@ const OperationJSONT = z__default["default"].discriminatedUnion("op", [z__defaul
|
|
1113
1106
|
}).strict()]);
|
1114
1107
|
const PatchJSON = z__default["default"].array(OperationJSONT);
|
1115
1108
|
|
1109
|
+
const ENABLE_COOKIE_VALUE = {
|
1110
|
+
value: "true",
|
1111
|
+
options: {
|
1112
|
+
httpOnly: false,
|
1113
|
+
sameSite: "lax"
|
1114
|
+
}
|
1115
|
+
};
|
1116
|
+
function getRedirectUrl(query, overrideHost) {
|
1117
|
+
if (typeof query.redirect_to !== "string") {
|
1118
|
+
return {
|
1119
|
+
status: 400,
|
1120
|
+
json: {
|
1121
|
+
message: "Missing redirect_to query param"
|
1122
|
+
}
|
1123
|
+
};
|
1124
|
+
}
|
1125
|
+
if (overrideHost) {
|
1126
|
+
return overrideHost + "?redirect_to=" + encodeURIComponent(query.redirect_to);
|
1127
|
+
}
|
1128
|
+
return query.redirect_to;
|
1129
|
+
}
|
1130
|
+
|
1131
|
+
class LocalValServer {
|
1132
|
+
constructor(options, callbacks) {
|
1133
|
+
this.options = options;
|
1134
|
+
this.callbacks = callbacks;
|
1135
|
+
}
|
1136
|
+
async session() {
|
1137
|
+
return {
|
1138
|
+
status: 200,
|
1139
|
+
json: {
|
1140
|
+
mode: "local",
|
1141
|
+
enabled: await this.callbacks.isEnabled()
|
1142
|
+
}
|
1143
|
+
};
|
1144
|
+
}
|
1145
|
+
async getTree(treePath,
|
1146
|
+
// TODO: use the params: patch, schema, source
|
1147
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
1148
|
+
query) {
|
1149
|
+
const rootDir = process.cwd();
|
1150
|
+
const moduleIds = [];
|
1151
|
+
// iterate over all .val files in the root directory
|
1152
|
+
const walk = async dir => {
|
1153
|
+
const files = await fs.promises.readdir(dir);
|
1154
|
+
for (const file of files) {
|
1155
|
+
if ((await fs.promises.stat(path__namespace["default"].join(dir, file))).isDirectory()) {
|
1156
|
+
if (file === "node_modules") continue;
|
1157
|
+
await walk(path__namespace["default"].join(dir, file));
|
1158
|
+
} else {
|
1159
|
+
const isValFile = file.endsWith(".val.js") || file.endsWith(".val.ts");
|
1160
|
+
if (!isValFile) {
|
1161
|
+
continue;
|
1162
|
+
}
|
1163
|
+
if (treePath && !path__namespace["default"].join(dir, file).replace(rootDir, "").startsWith(treePath)) {
|
1164
|
+
continue;
|
1165
|
+
}
|
1166
|
+
moduleIds.push(path__namespace["default"].join(dir, file).replace(rootDir, "").replace(".val.js", "").replace(".val.ts", ""));
|
1167
|
+
}
|
1168
|
+
}
|
1169
|
+
};
|
1170
|
+
const serializedModuleContent = await walk(rootDir).then(async () => {
|
1171
|
+
return Promise.all(moduleIds.map(async moduleId => {
|
1172
|
+
return await this.options.service.get(moduleId, "");
|
1173
|
+
}));
|
1174
|
+
});
|
1175
|
+
|
1176
|
+
//
|
1177
|
+
const modules = Object.fromEntries(serializedModuleContent.map(serializedModuleContent => {
|
1178
|
+
const module = {
|
1179
|
+
schema: serializedModuleContent.schema,
|
1180
|
+
source: serializedModuleContent.source,
|
1181
|
+
errors: serializedModuleContent.errors
|
1182
|
+
};
|
1183
|
+
return [serializedModuleContent.path, module];
|
1184
|
+
}));
|
1185
|
+
const apiTreeResponse = {
|
1186
|
+
modules,
|
1187
|
+
git: this.options.git
|
1188
|
+
};
|
1189
|
+
return {
|
1190
|
+
status: 200,
|
1191
|
+
json: apiTreeResponse
|
1192
|
+
};
|
1193
|
+
}
|
1194
|
+
async enable(query) {
|
1195
|
+
const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
|
1196
|
+
if (typeof redirectToRes !== "string") {
|
1197
|
+
return redirectToRes;
|
1198
|
+
}
|
1199
|
+
await this.callbacks.onEnable(true);
|
1200
|
+
return {
|
1201
|
+
cookies: {
|
1202
|
+
[internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
|
1203
|
+
},
|
1204
|
+
status: 302,
|
1205
|
+
redirectTo: redirectToRes
|
1206
|
+
};
|
1207
|
+
}
|
1208
|
+
async disable(query) {
|
1209
|
+
const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
|
1210
|
+
if (typeof redirectToRes !== "string") {
|
1211
|
+
return redirectToRes;
|
1212
|
+
}
|
1213
|
+
await this.callbacks.onDisable(true);
|
1214
|
+
return {
|
1215
|
+
cookies: {
|
1216
|
+
[internal.VAL_ENABLE_COOKIE_NAME]: {
|
1217
|
+
value: null
|
1218
|
+
}
|
1219
|
+
},
|
1220
|
+
status: 302,
|
1221
|
+
redirectTo: redirectToRes
|
1222
|
+
};
|
1223
|
+
}
|
1224
|
+
async postPatches(body) {
|
1225
|
+
// First validate that the body has the right structure
|
1226
|
+
const patchJSON = z.z.record(PatchJSON).safeParse(body);
|
1227
|
+
if (!patchJSON.success) {
|
1228
|
+
return {
|
1229
|
+
status: 404,
|
1230
|
+
json: {
|
1231
|
+
message: `Invalid patch: ${patchJSON.error.message}`,
|
1232
|
+
details: patchJSON.error.issues
|
1233
|
+
}
|
1234
|
+
};
|
1235
|
+
}
|
1236
|
+
const id = crypto.randomUUID();
|
1237
|
+
console.time("patching:" + id);
|
1238
|
+
for (const moduleId in patchJSON.data) {
|
1239
|
+
// Then parse/validate
|
1240
|
+
// TODO: validate all and then fail instead:
|
1241
|
+
const patch$1 = patch.parsePatch(patchJSON.data[moduleId]);
|
1242
|
+
if (fp.result.isErr(patch$1)) {
|
1243
|
+
console.error("Unexpected error parsing patch", patch$1.error);
|
1244
|
+
throw new Error("Unexpected error parsing patch");
|
1245
|
+
}
|
1246
|
+
await this.options.service.patch(moduleId, patch$1.value);
|
1247
|
+
}
|
1248
|
+
console.timeEnd("patching:" + id);
|
1249
|
+
return {
|
1250
|
+
status: 200,
|
1251
|
+
json: {} // no patch ids created
|
1252
|
+
};
|
1253
|
+
}
|
1254
|
+
|
1255
|
+
badRequest() {
|
1256
|
+
return {
|
1257
|
+
status: 400,
|
1258
|
+
json: {
|
1259
|
+
message: "Local server does not handle this request"
|
1260
|
+
}
|
1261
|
+
};
|
1262
|
+
}
|
1263
|
+
|
1264
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
1265
|
+
async postCommit() {
|
1266
|
+
return this.badRequest();
|
1267
|
+
}
|
1268
|
+
async authorize() {
|
1269
|
+
return this.badRequest();
|
1270
|
+
}
|
1271
|
+
async callback() {
|
1272
|
+
return this.badRequest();
|
1273
|
+
}
|
1274
|
+
async logout() {
|
1275
|
+
return this.badRequest();
|
1276
|
+
}
|
1277
|
+
async getFiles() {
|
1278
|
+
return this.badRequest();
|
1279
|
+
}
|
1280
|
+
async getPatches() {
|
1281
|
+
return this.badRequest();
|
1282
|
+
}
|
1283
|
+
}
|
1284
|
+
|
1116
1285
|
function decodeJwt(token, secretKey) {
|
1117
1286
|
const [headerBase64, payloadBase64, signatureBase64, ...rest] = token.split(".");
|
1118
1287
|
if (!headerBase64 || !payloadBase64 || !signatureBase64 || rest.length > 0) {
|
@@ -1172,167 +1341,258 @@ function encodeJwt(payload, sessionKey) {
|
|
1172
1341
|
return `${jwtHeaderBase64}.${payloadBase64}.${crypto__default["default"].createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
|
1173
1342
|
}
|
1174
1343
|
|
1175
|
-
const VAL_SESSION_COOKIE = core.Internal.VAL_SESSION_COOKIE;
|
1176
|
-
const VAL_STATE_COOKIE = core.Internal.VAL_STATE_COOKIE;
|
1177
|
-
const VAL_ENABLED_COOKIE = core.Internal.VAL_ENABLE_COOKIE_NAME;
|
1178
|
-
class BrowserReadableStreamWrapper extends stream.Readable {
|
1179
|
-
constructor(readableStream) {
|
1180
|
-
super();
|
1181
|
-
this.reader = readableStream.getReader();
|
1182
|
-
}
|
1183
|
-
_read() {
|
1184
|
-
this.reader.read().then(({
|
1185
|
-
done,
|
1186
|
-
value
|
1187
|
-
}) => {
|
1188
|
-
if (done) {
|
1189
|
-
this.push(null); // No more data to read
|
1190
|
-
} else {
|
1191
|
-
this.push(Buffer.from(value));
|
1192
|
-
}
|
1193
|
-
}).catch(error => {
|
1194
|
-
this.emit("error", error);
|
1195
|
-
});
|
1196
|
-
}
|
1197
|
-
}
|
1198
1344
|
class ProxyValServer {
|
1199
|
-
constructor(options) {
|
1345
|
+
constructor(options, callbacks) {
|
1200
1346
|
this.options = options;
|
1347
|
+
this.callbacks = callbacks;
|
1201
1348
|
}
|
1202
|
-
async getFiles(
|
1203
|
-
return this.withAuth(
|
1204
|
-
const url = new URL(`/v1/files/${this.options.valName}/${
|
1205
|
-
if (typeof
|
1206
|
-
url.searchParams.append("sha256",
|
1349
|
+
async getFiles(treePath, query, cookies) {
|
1350
|
+
return this.withAuth(cookies, "getFiles", async data => {
|
1351
|
+
const url = new URL(`/v1/files/${this.options.valName}/${treePath}`, this.options.valContentUrl);
|
1352
|
+
if (typeof query.sha256 === "string") {
|
1353
|
+
url.searchParams.append("sha256", query.sha256);
|
1207
1354
|
} else {
|
1208
1355
|
console.warn("Missing sha256 query param");
|
1209
1356
|
}
|
1210
1357
|
const fetchRes = await fetch(url, {
|
1211
1358
|
headers: this.getAuthHeaders(data.token)
|
1212
1359
|
});
|
1213
|
-
|
1214
|
-
if (contentType !== null) {
|
1215
|
-
res.setHeader("Content-Type", contentType);
|
1216
|
-
}
|
1217
|
-
const contentLength = fetchRes.headers.get("content-length");
|
1218
|
-
if (contentLength !== null) {
|
1219
|
-
res.setHeader("Content-Length", contentLength);
|
1220
|
-
}
|
1221
|
-
if (fetchRes.ok) {
|
1360
|
+
if (fetchRes.status === 200) {
|
1222
1361
|
if (fetchRes.body) {
|
1223
|
-
|
1362
|
+
return {
|
1363
|
+
status: fetchRes.status,
|
1364
|
+
headers: {
|
1365
|
+
"Content-Type": fetchRes.headers.get("Content-Type") || "",
|
1366
|
+
"Content-Length": fetchRes.headers.get("Content-Length") || "0"
|
1367
|
+
},
|
1368
|
+
body: fetchRes.body
|
1369
|
+
};
|
1224
1370
|
} else {
|
1225
|
-
|
1226
|
-
|
1371
|
+
return {
|
1372
|
+
status: 500,
|
1373
|
+
body: {
|
1374
|
+
message: "No body in response"
|
1375
|
+
}
|
1376
|
+
};
|
1227
1377
|
}
|
1228
1378
|
} else {
|
1229
|
-
|
1379
|
+
return {
|
1380
|
+
status: fetchRes.status,
|
1381
|
+
body: {
|
1382
|
+
message: "Failed to get files"
|
1383
|
+
}
|
1384
|
+
};
|
1230
1385
|
}
|
1231
1386
|
});
|
1232
1387
|
}
|
1233
|
-
async authorize(
|
1234
|
-
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
1238
|
-
|
1239
|
-
|
1388
|
+
async authorize(query) {
|
1389
|
+
if (typeof query.redirect_to !== "string") {
|
1390
|
+
return {
|
1391
|
+
status: 400,
|
1392
|
+
json: {
|
1393
|
+
message: "Missing redirect_to query param"
|
1394
|
+
}
|
1395
|
+
};
|
1240
1396
|
}
|
1241
1397
|
const token = crypto__default["default"].randomUUID();
|
1242
|
-
const redirectUrl = new URL(redirect_to);
|
1398
|
+
const redirectUrl = new URL(query.redirect_to);
|
1243
1399
|
const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
|
1244
|
-
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
1400
|
+
return {
|
1401
|
+
cookies: {
|
1402
|
+
[internal.VAL_STATE_COOKIE]: {
|
1403
|
+
value: createStateCookie({
|
1404
|
+
redirect_to: query.redirect_to,
|
1405
|
+
token
|
1406
|
+
}),
|
1407
|
+
options: {
|
1408
|
+
httpOnly: true,
|
1409
|
+
sameSite: "lax",
|
1410
|
+
expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
|
1411
|
+
}
|
1412
|
+
}
|
1413
|
+
},
|
1414
|
+
|
1415
|
+
status: 302,
|
1416
|
+
redirectTo: appAuthorizeUrl
|
1417
|
+
};
|
1252
1418
|
}
|
1253
|
-
async enable(
|
1254
|
-
|
1419
|
+
async enable(query) {
|
1420
|
+
const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
|
1421
|
+
if (typeof redirectToRes !== "string") {
|
1422
|
+
return redirectToRes;
|
1423
|
+
}
|
1424
|
+
await this.callbacks.onEnable(true);
|
1425
|
+
return {
|
1426
|
+
cookies: {
|
1427
|
+
[internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
|
1428
|
+
},
|
1429
|
+
status: 302,
|
1430
|
+
redirectTo: redirectToRes
|
1431
|
+
};
|
1255
1432
|
}
|
1256
|
-
async disable(
|
1257
|
-
|
1433
|
+
async disable(query) {
|
1434
|
+
const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
|
1435
|
+
if (typeof redirectToRes !== "string") {
|
1436
|
+
return redirectToRes;
|
1437
|
+
}
|
1438
|
+
await this.callbacks.onDisable(true);
|
1439
|
+
return {
|
1440
|
+
cookies: {
|
1441
|
+
[internal.VAL_ENABLE_COOKIE_NAME]: {
|
1442
|
+
value: null
|
1443
|
+
}
|
1444
|
+
},
|
1445
|
+
status: 302,
|
1446
|
+
redirectTo: redirectToRes
|
1447
|
+
};
|
1258
1448
|
}
|
1259
|
-
async callback(
|
1449
|
+
async callback(query, cookies) {
|
1260
1450
|
const {
|
1261
1451
|
success: callbackReqSuccess,
|
1262
1452
|
error: callbackReqError
|
1263
|
-
} = verifyCallbackReq(
|
1264
|
-
res.clearCookie(VAL_STATE_COOKIE); // we don't need this anymore
|
1265
|
-
|
1453
|
+
} = verifyCallbackReq(cookies[internal.VAL_STATE_COOKIE], query);
|
1266
1454
|
if (callbackReqError !== null) {
|
1267
|
-
|
1268
|
-
|
1455
|
+
return {
|
1456
|
+
status: 302,
|
1457
|
+
cookies: {
|
1458
|
+
[internal.VAL_STATE_COOKIE]: {
|
1459
|
+
value: null
|
1460
|
+
}
|
1461
|
+
},
|
1462
|
+
redirectTo: this.getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`)
|
1463
|
+
};
|
1269
1464
|
}
|
1270
1465
|
const data = await this.consumeCode(callbackReqSuccess.code);
|
1271
1466
|
if (data === null) {
|
1272
|
-
|
1273
|
-
|
1467
|
+
return {
|
1468
|
+
status: 302,
|
1469
|
+
cookies: {
|
1470
|
+
[internal.VAL_STATE_COOKIE]: {
|
1471
|
+
value: null
|
1472
|
+
}
|
1473
|
+
},
|
1474
|
+
redirectTo: this.getAppErrorUrl("Failed to exchange code for user")
|
1475
|
+
};
|
1274
1476
|
}
|
1275
1477
|
const exp = getExpire();
|
1276
1478
|
const cookie = encodeJwt({
|
1277
1479
|
...data,
|
1278
1480
|
exp // this is the client side exp
|
1279
1481
|
}, this.options.valSecret);
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1289
|
-
|
1290
|
-
|
1291
|
-
|
1482
|
+
return {
|
1483
|
+
status: 302,
|
1484
|
+
cookies: {
|
1485
|
+
[internal.VAL_STATE_COOKIE]: {
|
1486
|
+
value: null
|
1487
|
+
},
|
1488
|
+
[internal.VAL_SESSION_COOKIE]: {
|
1489
|
+
value: cookie,
|
1490
|
+
options: {
|
1491
|
+
httpOnly: true,
|
1492
|
+
sameSite: "strict",
|
1493
|
+
path: "/",
|
1494
|
+
secure: true,
|
1495
|
+
expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
|
1496
|
+
}
|
1497
|
+
}
|
1498
|
+
},
|
1499
|
+
|
1500
|
+
redirectTo: callbackReqSuccess.redirect_uri || "/"
|
1501
|
+
};
|
1502
|
+
}
|
1503
|
+
async logout() {
|
1504
|
+
return {
|
1505
|
+
status: 200,
|
1506
|
+
cookies: {
|
1507
|
+
[internal.VAL_SESSION_COOKIE]: {
|
1508
|
+
value: null
|
1509
|
+
},
|
1510
|
+
[internal.VAL_STATE_COOKIE]: {
|
1511
|
+
value: null
|
1512
|
+
}
|
1513
|
+
}
|
1514
|
+
};
|
1515
|
+
}
|
1516
|
+
async withAuth(cookies, errorMessageType, handler) {
|
1517
|
+
const cookie = cookies[internal.VAL_SESSION_COOKIE];
|
1292
1518
|
if (typeof cookie === "string") {
|
1293
|
-
const
|
1519
|
+
const decodedToken = decodeJwt(cookie, this.options.valSecret);
|
1520
|
+
if (!decodedToken) {
|
1521
|
+
return {
|
1522
|
+
status: 401,
|
1523
|
+
json: {
|
1524
|
+
message: "Invalid JWT token"
|
1525
|
+
}
|
1526
|
+
};
|
1527
|
+
}
|
1528
|
+
const verification = IntegratedServerJwtPayload.safeParse(decodedToken);
|
1294
1529
|
if (!verification.success) {
|
1295
|
-
|
1296
|
-
|
1530
|
+
return {
|
1531
|
+
status: 401,
|
1532
|
+
json: {
|
1533
|
+
message: "Could not parse JWT",
|
1534
|
+
details: verification.error
|
1535
|
+
}
|
1536
|
+
};
|
1297
1537
|
}
|
1298
1538
|
return handler(verification.data).catch(err => {
|
1299
|
-
console.error(`Failed while processing: ${
|
1300
|
-
|
1301
|
-
|
1539
|
+
console.error(`Failed while processing: ${errorMessageType}`, err);
|
1540
|
+
return {
|
1541
|
+
status: 500,
|
1542
|
+
body: {
|
1543
|
+
message: err.message
|
1544
|
+
}
|
1545
|
+
};
|
1302
1546
|
});
|
1303
1547
|
} else {
|
1304
|
-
|
1548
|
+
return {
|
1549
|
+
status: 401,
|
1550
|
+
json: {
|
1551
|
+
message: "No token"
|
1552
|
+
}
|
1553
|
+
};
|
1305
1554
|
}
|
1306
1555
|
}
|
1307
|
-
async session(
|
1308
|
-
return this.withAuth(
|
1556
|
+
async session(cookies) {
|
1557
|
+
return this.withAuth(cookies, "session", async data => {
|
1309
1558
|
const url = new URL(`/api/val/${this.options.valName}/auth/session`, this.options.valBuildUrl);
|
1310
1559
|
const fetchRes = await fetch(url, {
|
1311
1560
|
headers: this.getAuthHeaders(data.token, "application/json")
|
1312
1561
|
});
|
1313
|
-
if (fetchRes.
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
1317
|
-
|
1562
|
+
if (fetchRes.status === 200) {
|
1563
|
+
return {
|
1564
|
+
status: fetchRes.status,
|
1565
|
+
json: {
|
1566
|
+
mode: "proxy",
|
1567
|
+
enabled: await this.callbacks.isEnabled(),
|
1568
|
+
...(await fetchRes.json())
|
1569
|
+
}
|
1570
|
+
};
|
1318
1571
|
} else {
|
1319
|
-
|
1572
|
+
return {
|
1573
|
+
status: fetchRes.status,
|
1574
|
+
body: {
|
1575
|
+
message: "Failed to get session"
|
1576
|
+
}
|
1577
|
+
};
|
1320
1578
|
}
|
1321
1579
|
});
|
1322
1580
|
}
|
1323
|
-
async getTree(
|
1324
|
-
return this.withAuth(
|
1581
|
+
async getTree(treePath, query, cookies) {
|
1582
|
+
return this.withAuth(cookies, "getTree", async data => {
|
1325
1583
|
const {
|
1326
1584
|
patch,
|
1327
1585
|
schema,
|
1328
1586
|
source
|
1329
|
-
} =
|
1587
|
+
} = query;
|
1330
1588
|
const commit = this.options.gitCommit;
|
1331
1589
|
if (!commit) {
|
1332
|
-
|
1333
|
-
|
1334
|
-
|
1335
|
-
|
1590
|
+
return {
|
1591
|
+
status: 400,
|
1592
|
+
body: {
|
1593
|
+
message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
|
1594
|
+
}
|
1595
|
+
};
|
1336
1596
|
}
|
1337
1597
|
const params = new URLSearchParams({
|
1338
1598
|
patch: (patch === "true").toString(),
|
@@ -1340,63 +1600,99 @@ class ProxyValServer {
|
|
1340
1600
|
source: (source === "true").toString(),
|
1341
1601
|
commit
|
1342
1602
|
});
|
1343
|
-
const url = new URL(`/v1/tree/${this.options.valName}/heads/${this.options.gitBranch}/${
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1603
|
+
const url = new URL(`/v1/tree/${this.options.valName}/heads/${this.options.gitBranch}/${treePath}/?${params}`, this.options.valContentUrl);
|
1604
|
+
try {
|
1605
|
+
const fetchRes = await fetch(url, {
|
1606
|
+
headers: this.getAuthHeaders(data.token, "application/json")
|
1607
|
+
});
|
1608
|
+
if (fetchRes.status === 200) {
|
1609
|
+
return {
|
1610
|
+
status: fetchRes.status,
|
1611
|
+
json: await fetchRes.json()
|
1612
|
+
};
|
1613
|
+
} else {
|
1614
|
+
try {
|
1615
|
+
var _fetchRes$headers$get;
|
1616
|
+
if ((_fetchRes$headers$get = fetchRes.headers.get("Content-Type")) !== null && _fetchRes$headers$get !== void 0 && _fetchRes$headers$get.includes("application/json")) {
|
1617
|
+
const json = await fetchRes.json();
|
1618
|
+
return {
|
1619
|
+
status: fetchRes.status,
|
1620
|
+
json
|
1621
|
+
};
|
1622
|
+
}
|
1623
|
+
} catch (err) {
|
1624
|
+
console.error(err);
|
1625
|
+
}
|
1626
|
+
return {
|
1627
|
+
status: fetchRes.status,
|
1628
|
+
json: {
|
1629
|
+
message: "Unknown failure while accessing Val"
|
1630
|
+
}
|
1631
|
+
};
|
1632
|
+
}
|
1633
|
+
} catch (err) {
|
1634
|
+
return {
|
1635
|
+
status: 500,
|
1636
|
+
body: {
|
1637
|
+
message: "Failed to fetch: check network connection"
|
1638
|
+
}
|
1639
|
+
};
|
1640
|
+
}
|
1351
1641
|
});
|
1352
1642
|
}
|
1353
|
-
async getPatches(
|
1354
|
-
const patchIds =
|
1643
|
+
async getPatches(query, cookies) {
|
1644
|
+
const patchIds = query.id || [];
|
1355
1645
|
const params = patchIds.length > 0 ? `?${patchIds.map(id => `id=${encodeURIComponent(id)}`).join("&")}` : "";
|
1356
|
-
|
1646
|
+
return this.withAuth(cookies, "getPatches", async ({
|
1357
1647
|
token
|
1358
1648
|
}) => {
|
1359
|
-
const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.gitBranch}
|
1360
|
-
console.log(url);
|
1649
|
+
const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.gitBranch}/~${params}`, this.options.valContentUrl);
|
1361
1650
|
// Proxy patch to val.build
|
1362
1651
|
const fetchRes = await fetch(url, {
|
1363
1652
|
method: "GET",
|
1364
1653
|
headers: this.getAuthHeaders(token, "application/json")
|
1365
1654
|
});
|
1366
|
-
if (fetchRes.
|
1367
|
-
|
1368
|
-
|
1655
|
+
if (fetchRes.status === 200) {
|
1656
|
+
return {
|
1657
|
+
status: fetchRes.status,
|
1658
|
+
json: await fetchRes.json()
|
1659
|
+
};
|
1369
1660
|
} else {
|
1370
|
-
|
1661
|
+
return {
|
1662
|
+
status: fetchRes.status,
|
1663
|
+
body: {
|
1664
|
+
message: "Failed to get patches"
|
1665
|
+
}
|
1666
|
+
};
|
1371
1667
|
}
|
1372
|
-
}).catch(e => {
|
1373
|
-
res.status(500).send({
|
1374
|
-
error: {
|
1375
|
-
message: e === null || e === void 0 ? void 0 : e.message,
|
1376
|
-
status: 500
|
1377
|
-
}
|
1378
|
-
});
|
1379
1668
|
});
|
1380
1669
|
}
|
1381
|
-
async postPatches(
|
1670
|
+
async postPatches(body, cookies) {
|
1382
1671
|
const commit = this.options.gitCommit;
|
1383
1672
|
if (!commit) {
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1673
|
+
return {
|
1674
|
+
status: 401,
|
1675
|
+
json: {
|
1676
|
+
message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
|
1677
|
+
}
|
1678
|
+
};
|
1388
1679
|
}
|
1389
1680
|
const params = new URLSearchParams({
|
1390
1681
|
commit
|
1391
1682
|
});
|
1392
|
-
|
1683
|
+
return this.withAuth(cookies, "postPatches", async ({
|
1393
1684
|
token
|
1394
1685
|
}) => {
|
1395
1686
|
// First validate that the body has the right structure
|
1396
|
-
const patchJSON = z.z.record(PatchJSON).safeParse(
|
1687
|
+
const patchJSON = z.z.record(PatchJSON).safeParse(body);
|
1397
1688
|
if (!patchJSON.success) {
|
1398
|
-
|
1399
|
-
|
1689
|
+
return {
|
1690
|
+
status: 400,
|
1691
|
+
body: {
|
1692
|
+
message: "Invalid patch",
|
1693
|
+
details: patchJSON.error.issues
|
1694
|
+
}
|
1695
|
+
};
|
1400
1696
|
}
|
1401
1697
|
// Then parse/validate
|
1402
1698
|
// TODO:
|
@@ -1406,29 +1702,27 @@ class ProxyValServer {
|
|
1406
1702
|
// res.status(401).json(patch.error);
|
1407
1703
|
// return;
|
1408
1704
|
// }
|
1409
|
-
const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.gitBranch}
|
1705
|
+
const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.gitBranch}/~?${params}`, this.options.valContentUrl);
|
1410
1706
|
// Proxy patch to val.build
|
1411
1707
|
const fetchRes = await fetch(url, {
|
1412
1708
|
method: "POST",
|
1413
1709
|
headers: this.getAuthHeaders(token, "application/json"),
|
1414
1710
|
body: JSON.stringify(patch)
|
1415
1711
|
});
|
1416
|
-
if (fetchRes.
|
1417
|
-
|
1712
|
+
if (fetchRes.status === 200) {
|
1713
|
+
return {
|
1714
|
+
status: fetchRes.status,
|
1715
|
+
json: await fetchRes.json()
|
1716
|
+
};
|
1418
1717
|
} else {
|
1419
|
-
|
1718
|
+
return {
|
1719
|
+
status: fetchRes.status
|
1720
|
+
};
|
1420
1721
|
}
|
1421
|
-
}).catch(e => {
|
1422
|
-
res.status(500).send({
|
1423
|
-
error: {
|
1424
|
-
message: e === null || e === void 0 ? void 0 : e.message,
|
1425
|
-
status: 500
|
1426
|
-
}
|
1427
|
-
});
|
1428
1722
|
});
|
1429
1723
|
}
|
1430
|
-
async
|
1431
|
-
|
1724
|
+
async postCommit(cookies) {
|
1725
|
+
return this.withAuth(cookies, "postCommit", async ({
|
1432
1726
|
token
|
1433
1727
|
}) => {
|
1434
1728
|
const url = new URL(`/api/val/commit/${encodeURIComponent(this.options.gitBranch)}`, this.options.valBuildUrl);
|
@@ -1436,10 +1730,15 @@ class ProxyValServer {
|
|
1436
1730
|
method: "POST",
|
1437
1731
|
headers: this.getAuthHeaders(token)
|
1438
1732
|
});
|
1439
|
-
if (fetchRes.
|
1440
|
-
|
1733
|
+
if (fetchRes.status === 200) {
|
1734
|
+
return {
|
1735
|
+
status: fetchRes.status,
|
1736
|
+
json: await fetchRes.json()
|
1737
|
+
};
|
1441
1738
|
} else {
|
1442
|
-
|
1739
|
+
return {
|
1740
|
+
status: fetchRes.status
|
1741
|
+
};
|
1443
1742
|
}
|
1444
1743
|
});
|
1445
1744
|
}
|
@@ -1592,40 +1891,6 @@ function getStateFromCookie(stateCookie) {
|
|
1592
1891
|
};
|
1593
1892
|
}
|
1594
1893
|
}
|
1595
|
-
async function enable(req, res, redirectUrl) {
|
1596
|
-
const {
|
1597
|
-
redirect_to
|
1598
|
-
} = req.query;
|
1599
|
-
if (typeof redirect_to === "string" || typeof redirect_to === "undefined") {
|
1600
|
-
let redirectUrlToUse = redirect_to || "/";
|
1601
|
-
if (redirectUrl) {
|
1602
|
-
redirectUrlToUse = redirectUrl + "?redirect_to=" + encodeURIComponent(redirectUrlToUse);
|
1603
|
-
}
|
1604
|
-
res.cookie(VAL_ENABLED_COOKIE, "true", {
|
1605
|
-
httpOnly: false,
|
1606
|
-
sameSite: "lax"
|
1607
|
-
}).redirect(redirectUrlToUse);
|
1608
|
-
} else {
|
1609
|
-
res.sendStatus(400);
|
1610
|
-
}
|
1611
|
-
}
|
1612
|
-
async function disable(req, res, redirectUrl) {
|
1613
|
-
const {
|
1614
|
-
redirect_to
|
1615
|
-
} = req.query;
|
1616
|
-
if (typeof redirect_to === "string" || typeof redirect_to === "undefined") {
|
1617
|
-
let redirectUrlToUse = redirect_to || "/";
|
1618
|
-
if (redirectUrl) {
|
1619
|
-
redirectUrlToUse = redirectUrl + "?redirect_to=" + encodeURIComponent(redirectUrlToUse);
|
1620
|
-
}
|
1621
|
-
res.cookie(VAL_ENABLED_COOKIE, "false", {
|
1622
|
-
httpOnly: false,
|
1623
|
-
sameSite: "lax"
|
1624
|
-
}).redirect(redirectUrlToUse);
|
1625
|
-
} else {
|
1626
|
-
res.sendStatus(400);
|
1627
|
-
}
|
1628
|
-
}
|
1629
1894
|
function createStateCookie(state) {
|
1630
1895
|
return Buffer.from(JSON.stringify(state), "utf8").toString("base64");
|
1631
1896
|
}
|
@@ -1643,137 +1908,13 @@ const IntegratedServerJwtPayload = z.z.object({
|
|
1643
1908
|
project: z.z.string()
|
1644
1909
|
});
|
1645
1910
|
|
1646
|
-
|
1647
|
-
constructor(options) {
|
1648
|
-
this.options = options;
|
1649
|
-
}
|
1650
|
-
async session(_req, res) {
|
1651
|
-
res.json({
|
1652
|
-
mode: "local"
|
1653
|
-
});
|
1654
|
-
}
|
1655
|
-
async getTree(req, res) {
|
1656
|
-
try {
|
1657
|
-
// TODO: use the params: patch, schema, source
|
1658
|
-
const treePath = req.params["0"].replace("~", "");
|
1659
|
-
const rootDir = process.cwd();
|
1660
|
-
const moduleIds = [];
|
1661
|
-
// iterate over all .val files in the root directory
|
1662
|
-
const walk = async dir => {
|
1663
|
-
const files = await fs.promises.readdir(dir);
|
1664
|
-
for (const file of files) {
|
1665
|
-
if ((await fs.promises.stat(path__namespace["default"].join(dir, file))).isDirectory()) {
|
1666
|
-
if (file === "node_modules") continue;
|
1667
|
-
await walk(path__namespace["default"].join(dir, file));
|
1668
|
-
} else {
|
1669
|
-
const isValFile = file.endsWith(".val.js") || file.endsWith(".val.ts");
|
1670
|
-
if (!isValFile) {
|
1671
|
-
continue;
|
1672
|
-
}
|
1673
|
-
if (treePath && !path__namespace["default"].join(dir, file).replace(rootDir, "").startsWith(treePath)) {
|
1674
|
-
continue;
|
1675
|
-
}
|
1676
|
-
moduleIds.push(path__namespace["default"].join(dir, file).replace(rootDir, "").replace(".val.js", "").replace(".val.ts", ""));
|
1677
|
-
}
|
1678
|
-
}
|
1679
|
-
};
|
1680
|
-
const serializedModuleContent = await walk(rootDir).then(async () => {
|
1681
|
-
return Promise.all(moduleIds.map(async moduleId => {
|
1682
|
-
return await this.options.service.get(moduleId, "");
|
1683
|
-
}));
|
1684
|
-
});
|
1685
|
-
|
1686
|
-
//
|
1687
|
-
const modules = Object.fromEntries(serializedModuleContent.map(serializedModuleContent => {
|
1688
|
-
const module = {
|
1689
|
-
schema: serializedModuleContent.schema,
|
1690
|
-
source: serializedModuleContent.source,
|
1691
|
-
errors: serializedModuleContent.errors
|
1692
|
-
};
|
1693
|
-
return [serializedModuleContent.path, module];
|
1694
|
-
}));
|
1695
|
-
const apiTreeResponse = {
|
1696
|
-
modules,
|
1697
|
-
git: this.options.git
|
1698
|
-
};
|
1699
|
-
res.send(JSON.stringify(apiTreeResponse));
|
1700
|
-
} catch (err) {
|
1701
|
-
console.error(err);
|
1702
|
-
res.sendStatus(500);
|
1703
|
-
}
|
1704
|
-
}
|
1705
|
-
async enable(req, res) {
|
1706
|
-
return enable(req, res);
|
1707
|
-
}
|
1708
|
-
async disable(req, res) {
|
1709
|
-
return disable(req, res);
|
1710
|
-
}
|
1711
|
-
async postPatches(req, res) {
|
1712
|
-
// First validate that the body has the right structure
|
1713
|
-
const patchJSON = z.z.record(PatchJSON).safeParse(req.body);
|
1714
|
-
if (!patchJSON.success) {
|
1715
|
-
res.status(401).json(patchJSON.error.issues);
|
1716
|
-
return;
|
1717
|
-
}
|
1718
|
-
try {
|
1719
|
-
for (const moduleId in patchJSON.data) {
|
1720
|
-
// Then parse/validate
|
1721
|
-
// TODO: validate all and then fail instead:
|
1722
|
-
const patch$1 = patch.parsePatch(patchJSON.data[moduleId]);
|
1723
|
-
if (fp.result.isErr(patch$1)) {
|
1724
|
-
res.status(401).json(patch$1.error);
|
1725
|
-
return;
|
1726
|
-
}
|
1727
|
-
await this.options.service.patch(moduleId, patch$1.value);
|
1728
|
-
}
|
1729
|
-
res.json({});
|
1730
|
-
} catch (err) {
|
1731
|
-
if (err instanceof patch.PatchError) {
|
1732
|
-
res.status(400).send({
|
1733
|
-
message: err.message
|
1734
|
-
});
|
1735
|
-
} else {
|
1736
|
-
console.error(err);
|
1737
|
-
res.status(500).send({
|
1738
|
-
message: err instanceof Error ? err.message : "Unknown error"
|
1739
|
-
});
|
1740
|
-
}
|
1741
|
-
}
|
1742
|
-
}
|
1743
|
-
async badRequest(req, res) {
|
1744
|
-
console.debug("Local server does handle this request", req.url);
|
1745
|
-
res.sendStatus(400);
|
1746
|
-
}
|
1747
|
-
commit(req, res) {
|
1748
|
-
return this.badRequest(req, res);
|
1749
|
-
}
|
1750
|
-
authorize(req, res) {
|
1751
|
-
return this.badRequest(req, res);
|
1752
|
-
}
|
1753
|
-
callback(req, res) {
|
1754
|
-
return this.badRequest(req, res);
|
1755
|
-
}
|
1756
|
-
logout(req, res) {
|
1757
|
-
return this.badRequest(req, res);
|
1758
|
-
}
|
1759
|
-
getFiles(req, res) {
|
1760
|
-
return this.badRequest(req, res);
|
1761
|
-
}
|
1762
|
-
getPatches(req, res) {
|
1763
|
-
return this.badRequest(req, res);
|
1764
|
-
}
|
1765
|
-
}
|
1766
|
-
|
1767
|
-
async function _createRequestListener(route, opts) {
|
1911
|
+
async function createValServer(route, opts, callbacks) {
|
1768
1912
|
const serverOpts = await initHandlerOptions(route, opts);
|
1769
|
-
let valServer;
|
1770
1913
|
if (serverOpts.mode === "proxy") {
|
1771
|
-
|
1914
|
+
return new ProxyValServer(serverOpts, callbacks);
|
1772
1915
|
} else {
|
1773
|
-
|
1916
|
+
return new LocalValServer(serverOpts, callbacks);
|
1774
1917
|
}
|
1775
|
-
const reqHandler = createRequestHandler(valServer);
|
1776
|
-
return express__default["default"]().use(route, reqHandler);
|
1777
1918
|
}
|
1778
1919
|
async function initHandlerOptions(route, opts) {
|
1779
1920
|
const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
|
@@ -1817,6 +1958,8 @@ async function initHandlerOptions(route, opts) {
|
|
1817
1958
|
return {
|
1818
1959
|
mode: "local",
|
1819
1960
|
service,
|
1961
|
+
valEnableRedirectUrl: opts.valEnableRedirectUrl || process.env.VAL_ENABLE_REDIRECT_URL,
|
1962
|
+
valDisableRedirectUrl: opts.valDisableRedirectUrl || process.env.VAL_DISABLE_REDIRECT_URL,
|
1820
1963
|
git: {
|
1821
1964
|
commit: process.env.VAL_GIT_COMMIT || git.commit,
|
1822
1965
|
branch: process.env.VAL_GIT_BRANCH || git.branch
|
@@ -1879,22 +2022,127 @@ async function readCommit(gitDir, branchName) {
|
|
1879
2022
|
return undefined;
|
1880
2023
|
}
|
1881
2024
|
}
|
1882
|
-
|
1883
|
-
|
1884
|
-
|
1885
|
-
|
1886
|
-
|
1887
|
-
|
1888
|
-
|
1889
|
-
|
1890
|
-
|
1891
|
-
|
1892
|
-
|
1893
|
-
|
2025
|
+
const {
|
2026
|
+
VAL_SESSION_COOKIE,
|
2027
|
+
VAL_STATE_COOKIE
|
2028
|
+
} = core.Internal;
|
2029
|
+
const TREE_PATH_PREFIX = "/tree/~";
|
2030
|
+
const PATCHES_PATH_PREFIX = "/patches/~";
|
2031
|
+
const FILES_PATH_PREFIX = "/files";
|
2032
|
+
function createValApiRouter(route, valServerPromise, convert) {
|
2033
|
+
const uiRequestHandler = server.createUIRequestHandler();
|
2034
|
+
return async req => {
|
2035
|
+
var _req$method;
|
2036
|
+
const valServer = await valServerPromise;
|
2037
|
+
req.headers.get("content-type");
|
2038
|
+
req.headers.get("Cookie");
|
2039
|
+
const url = new URL(req.url);
|
2040
|
+
if (!url.pathname.startsWith(route)) {
|
2041
|
+
const error = {
|
2042
|
+
message: "Val: routes are not configured correctly",
|
2043
|
+
details: `Check you api routes. Expected pathname to start with "${route}", but it was: "${url.pathname}"`
|
2044
|
+
};
|
2045
|
+
console.error(error);
|
2046
|
+
return convert({
|
2047
|
+
status: 500,
|
2048
|
+
json: error
|
2049
|
+
});
|
2050
|
+
}
|
2051
|
+
const method = (_req$method = req.method) === null || _req$method === void 0 ? void 0 : _req$method.toUpperCase();
|
2052
|
+
function withTreePath(path, prefix) {
|
2053
|
+
return async useTreePath => {
|
2054
|
+
const pathIndex = path.indexOf("~");
|
2055
|
+
if (path.startsWith(prefix) && pathIndex !== -1) {
|
2056
|
+
return useTreePath(path.slice(pathIndex + 1));
|
2057
|
+
} else {
|
2058
|
+
if (prefix.indexOf("/~") === -1) {
|
2059
|
+
return convert({
|
2060
|
+
status: 500,
|
2061
|
+
json: {
|
2062
|
+
message: `Route is incorrectly formed: ${prefix}!`
|
2063
|
+
}
|
2064
|
+
});
|
2065
|
+
}
|
2066
|
+
return convert({
|
2067
|
+
status: 404,
|
2068
|
+
json: {
|
2069
|
+
message: `Malformed ${prefix} path! Expected: '${prefix}'`
|
2070
|
+
}
|
2071
|
+
});
|
2072
|
+
}
|
2073
|
+
};
|
2074
|
+
}
|
2075
|
+
const path = url.pathname.slice(route.length);
|
2076
|
+
if (path.startsWith("/static")) {
|
2077
|
+
return convert(await uiRequestHandler(path.slice("/static".length)));
|
2078
|
+
} else if (path === "/session") {
|
2079
|
+
return convert(await valServer.session(getCookies(req, [VAL_SESSION_COOKIE])));
|
2080
|
+
} else if (path === "/authorize") {
|
2081
|
+
return convert(await valServer.authorize({
|
2082
|
+
redirect_to: url.searchParams.get("redirect_to") || undefined
|
2083
|
+
}));
|
2084
|
+
} else if (path === "/callback") {
|
2085
|
+
return convert(await valServer.callback({
|
2086
|
+
code: url.searchParams.get("code") || undefined,
|
2087
|
+
state: url.searchParams.get("state") || undefined
|
2088
|
+
}, getCookies(req, [VAL_STATE_COOKIE])));
|
2089
|
+
} else if (path === "/logout") {
|
2090
|
+
return convert(await valServer.logout());
|
2091
|
+
} else if (path === "/enable") {
|
2092
|
+
return convert(await valServer.enable({
|
2093
|
+
redirect_to: url.searchParams.get("redirect_to") || undefined
|
2094
|
+
}));
|
2095
|
+
} else if (path === "/disable") {
|
2096
|
+
return convert(await valServer.disable({
|
2097
|
+
redirect_to: url.searchParams.get("redirect_to") || undefined
|
2098
|
+
}));
|
2099
|
+
} else if (method === "POST" && path === "/commit") {
|
2100
|
+
return convert(await valServer.postCommit(getCookies(req, [VAL_SESSION_COOKIE])));
|
2101
|
+
} else if (method === "GET" && path.startsWith(TREE_PATH_PREFIX)) {
|
2102
|
+
return withTreePath(path, TREE_PATH_PREFIX)(async treePath => convert(await valServer.getTree(treePath, {
|
2103
|
+
patch: url.searchParams.get("patch") || undefined,
|
2104
|
+
schema: url.searchParams.get("schema") || undefined,
|
2105
|
+
source: url.searchParams.get("source") || undefined
|
2106
|
+
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
2107
|
+
} else if (method === "GET" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
2108
|
+
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.getPatches({
|
2109
|
+
id: url.searchParams.getAll("id")
|
2110
|
+
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
2111
|
+
} else if (method === "POST" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
2112
|
+
const body = await req.json();
|
2113
|
+
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.postPatches(body, getCookies(req, [VAL_SESSION_COOKIE]))));
|
2114
|
+
} else if (path.startsWith(FILES_PATH_PREFIX)) {
|
2115
|
+
const treePath = path.slice(FILES_PATH_PREFIX.length);
|
2116
|
+
return convert(await valServer.getFiles(treePath, {
|
2117
|
+
sha256: url.searchParams.get("sha256") || undefined
|
2118
|
+
}, getCookies(req, [VAL_SESSION_COOKIE])));
|
2119
|
+
} else {
|
2120
|
+
return convert({
|
2121
|
+
status: 404,
|
2122
|
+
json: {
|
2123
|
+
message: "Not Found",
|
2124
|
+
details: {
|
2125
|
+
method,
|
2126
|
+
path
|
2127
|
+
}
|
2128
|
+
}
|
2129
|
+
});
|
1894
2130
|
}
|
1895
2131
|
};
|
1896
2132
|
}
|
1897
2133
|
|
2134
|
+
// TODO: is this naive implementation is too naive?
|
2135
|
+
function getCookies(req, names) {
|
2136
|
+
var _req$headers$get;
|
2137
|
+
return ((_req$headers$get = req.headers.get("Cookie")) === null || _req$headers$get === void 0 ? void 0 : _req$headers$get.split("; ").reduce((acc, cookie) => {
|
2138
|
+
const [name, value] = cookie.split("=");
|
2139
|
+
if (names.includes(name.trim())) {
|
2140
|
+
acc[name.trim()] = decodeURIComponent(value.trim());
|
2141
|
+
}
|
2142
|
+
return acc;
|
2143
|
+
}, {})) || {};
|
2144
|
+
}
|
2145
|
+
|
1898
2146
|
/**
|
1899
2147
|
* An implementation of methods in the various ts.*Host interfaces
|
1900
2148
|
* that uses ValFS to resolve modules and read/write files.
|
@@ -2042,9 +2290,9 @@ exports.ValFSHost = ValFSHost;
|
|
2042
2290
|
exports.ValModuleLoader = ValModuleLoader;
|
2043
2291
|
exports.ValSourceFileHandler = ValSourceFileHandler;
|
2044
2292
|
exports.createFixPatch = createFixPatch;
|
2045
|
-
exports.createRequestHandler = createRequestHandler;
|
2046
|
-
exports.createRequestListener = createRequestListener;
|
2047
2293
|
exports.createService = createService;
|
2294
|
+
exports.createValApiRouter = createValApiRouter;
|
2295
|
+
exports.createValServer = createValServer;
|
2048
2296
|
exports.decodeJwt = decodeJwt;
|
2049
2297
|
exports.encodeJwt = encodeJwt;
|
2050
2298
|
exports.formatSyntaxErrorTree = formatSyntaxErrorTree;
|