@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
@@ -3,15 +3,14 @@ import ts from 'typescript';
|
|
3
3
|
import { result, pipe } from '@valbuild/core/fp';
|
4
4
|
import { FILE_REF_PROP, VAL_EXTENSION, derefPatch, Internal, Schema } from '@valbuild/core';
|
5
5
|
import { deepEqual, isNotRoot, PatchError, parseAndValidateArrayIndex, applyPatch, parsePatch, sourceToPatchPath } from '@valbuild/core/patch';
|
6
|
+
import crypto, { randomUUID } from 'crypto';
|
6
7
|
import * as path from 'path';
|
7
8
|
import path__default from 'path';
|
8
9
|
import fs, { promises } from 'fs';
|
9
10
|
import { transform } from 'sucrase';
|
10
|
-
import express, { Router } from 'express';
|
11
|
-
import { createRequestHandler as createRequestHandler$1 } from '@valbuild/ui/server';
|
12
11
|
import z, { z as z$1 } from 'zod';
|
13
|
-
import
|
14
|
-
import {
|
12
|
+
import { VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE as VAL_STATE_COOKIE$1, VAL_SESSION_COOKIE as VAL_SESSION_COOKIE$1 } from '@valbuild/shared/internal';
|
13
|
+
import { createUIRequestHandler } from '@valbuild/ui/server';
|
15
14
|
import sizeOf from 'image-size';
|
16
15
|
|
17
16
|
class ValSyntaxError {
|
@@ -465,7 +464,7 @@ function getPointerFromPath(node, path) {
|
|
465
464
|
return childNode;
|
466
465
|
}
|
467
466
|
if (childNode.value === undefined) {
|
468
|
-
return result.err(new PatchError(
|
467
|
+
return result.err(new PatchError(`Path refers to non-existing object/array: ${path.join("/")}`));
|
469
468
|
}
|
470
469
|
targetNode = childNode.value;
|
471
470
|
}
|
@@ -587,6 +586,64 @@ class TSOps {
|
|
587
586
|
}
|
588
587
|
}
|
589
588
|
|
589
|
+
const ops = new TSOps(document => {
|
590
|
+
return pipe(analyzeValModule(document), result.map(({
|
591
|
+
source
|
592
|
+
}) => source));
|
593
|
+
});
|
594
|
+
|
595
|
+
// TODO: rename to patchValFiles since we may write multiple files
|
596
|
+
const patchValFile = async (id, valConfigPath, patch, sourceFileHandler, runtime) => {
|
597
|
+
const timeId = randomUUID();
|
598
|
+
console.time("patchValFile" + timeId);
|
599
|
+
const filePath = sourceFileHandler.resolveSourceModulePath(valConfigPath, `.${id}.val`);
|
600
|
+
const sourceFile = sourceFileHandler.getSourceFile(filePath);
|
601
|
+
if (!sourceFile) {
|
602
|
+
throw Error(`Source file ${filePath} not found`);
|
603
|
+
}
|
604
|
+
const derefRes = derefPatch(patch, sourceFile, ops);
|
605
|
+
if (result.isErr(derefRes)) {
|
606
|
+
throw derefRes.error;
|
607
|
+
}
|
608
|
+
const dereferencedPatch = derefRes.value.dereferencedPatch; // TODO: add ref changes to remote replace/add, ...
|
609
|
+
const newSourceFile = patchSourceFile(sourceFile, dereferencedPatch);
|
610
|
+
if (result.isErr(newSourceFile)) {
|
611
|
+
if (newSourceFile.error instanceof PatchError) {
|
612
|
+
throw newSourceFile.error;
|
613
|
+
} else {
|
614
|
+
throw new Error(`${filePath}\n${flatMapErrors(newSourceFile.error, error => formatSyntaxError(error, sourceFile)).join("\n")}`);
|
615
|
+
}
|
616
|
+
}
|
617
|
+
for (const [filePath, content] of Object.entries(derefRes.value.fileUpdates)) {
|
618
|
+
// Evaluate if we want to make these writes (more) atomic with a temp file and a move.
|
619
|
+
// This can potentially fill mid-way if there is not enough space on disk for example...
|
620
|
+
// However, that might be add add bit more complexity in our host and virtual file systems?
|
621
|
+
// Example:
|
622
|
+
// const tempFilePath = sourceFileHandler.writeTempFile(
|
623
|
+
// Buffer.from(content, "base64").toString("binary")
|
624
|
+
// );
|
625
|
+
// sourceFileHandler.moveFile(tempFilePath, "." + filePath);
|
626
|
+
// TODO: ensure that directory exists
|
627
|
+
if (content.startsWith("data:/image/svg+xml")) {
|
628
|
+
sourceFileHandler.writeFile("." + filePath, convertDataUrlToBase64(content).toString("utf8"), "utf8");
|
629
|
+
} else {
|
630
|
+
sourceFileHandler.writeFile("." + filePath, convertDataUrlToBase64(content).toString("binary"), "binary");
|
631
|
+
}
|
632
|
+
}
|
633
|
+
sourceFileHandler.writeSourceFile(newSourceFile.value);
|
634
|
+
console.timeEnd("patchValFile" + timeId);
|
635
|
+
};
|
636
|
+
function convertDataUrlToBase64(dataUrl) {
|
637
|
+
const base64 = dataUrl.slice(dataUrl.indexOf(",") + 1);
|
638
|
+
return Buffer.from(base64, "base64");
|
639
|
+
}
|
640
|
+
const patchSourceFile = (sourceFile, patch) => {
|
641
|
+
if (typeof sourceFile === "string") {
|
642
|
+
return applyPatch(ts.createSourceFile("<val>", sourceFile, ts.ScriptTarget.ES2015), ops, patch);
|
643
|
+
}
|
644
|
+
return applyPatch(sourceFile, ops, patch);
|
645
|
+
};
|
646
|
+
|
590
647
|
const readValFile = async (id, valConfigPath, runtime) => {
|
591
648
|
const context = runtime.newContext();
|
592
649
|
try {
|
@@ -664,62 +721,6 @@ globalThis.valModule = {
|
|
664
721
|
}
|
665
722
|
};
|
666
723
|
|
667
|
-
const ops = new TSOps(document => {
|
668
|
-
return pipe(analyzeValModule(document), result.map(({
|
669
|
-
source
|
670
|
-
}) => source));
|
671
|
-
});
|
672
|
-
|
673
|
-
// TODO: rename to patchValFiles since we may write multiple files
|
674
|
-
const patchValFile = async (id, valConfigPath, patch, sourceFileHandler, runtime) => {
|
675
|
-
const filePath = sourceFileHandler.resolveSourceModulePath(valConfigPath, `.${id}.val`);
|
676
|
-
const sourceFile = sourceFileHandler.getSourceFile(filePath);
|
677
|
-
if (!sourceFile) {
|
678
|
-
throw Error(`Source file ${filePath} not found`);
|
679
|
-
}
|
680
|
-
const derefRes = derefPatch(patch, sourceFile, ops);
|
681
|
-
if (result.isErr(derefRes)) {
|
682
|
-
throw derefRes.error;
|
683
|
-
}
|
684
|
-
const dereferencedPatch = derefRes.value.dereferencedPatch; // TODO: add ref changes to remote replace/add, ...
|
685
|
-
const newSourceFile = patchSourceFile(sourceFile, dereferencedPatch);
|
686
|
-
if (result.isErr(newSourceFile)) {
|
687
|
-
if (newSourceFile.error instanceof PatchError) {
|
688
|
-
throw newSourceFile.error;
|
689
|
-
} else {
|
690
|
-
throw new Error(`${filePath}\n${flatMapErrors(newSourceFile.error, error => formatSyntaxError(error, sourceFile)).join("\n")}`);
|
691
|
-
}
|
692
|
-
}
|
693
|
-
for (const [filePath, content] of Object.entries(derefRes.value.fileUpdates)) {
|
694
|
-
// Evaluate if we want to make these writes (more) atomic with a temp file and a move.
|
695
|
-
// This can potentially fill mid-way if there is not enough space on disk for example...
|
696
|
-
// However, that might be add add bit more complexity in our host and virtual file systems?
|
697
|
-
// Example:
|
698
|
-
// const tempFilePath = sourceFileHandler.writeTempFile(
|
699
|
-
// Buffer.from(content, "base64").toString("binary")
|
700
|
-
// );
|
701
|
-
// sourceFileHandler.moveFile(tempFilePath, "." + filePath);
|
702
|
-
// TODO: ensure that directory exists
|
703
|
-
if (content.startsWith("data:/image/svg+xml")) {
|
704
|
-
sourceFileHandler.writeFile("." + filePath, convertDataUrlToBase64(content).toString("utf8"), "utf8");
|
705
|
-
} else {
|
706
|
-
sourceFileHandler.writeFile("." + filePath, convertDataUrlToBase64(content).toString("binary"), "binary");
|
707
|
-
}
|
708
|
-
}
|
709
|
-
sourceFileHandler.writeSourceFile(newSourceFile.value);
|
710
|
-
return readValFile(id, valConfigPath, runtime);
|
711
|
-
};
|
712
|
-
function convertDataUrlToBase64(dataUrl) {
|
713
|
-
const base64 = dataUrl.slice(dataUrl.indexOf(",") + 1);
|
714
|
-
return Buffer.from(base64, "base64");
|
715
|
-
}
|
716
|
-
const patchSourceFile = (sourceFile, patch) => {
|
717
|
-
if (typeof sourceFile === "string") {
|
718
|
-
return applyPatch(ts.createSourceFile("<val>", sourceFile, ts.ScriptTarget.ES2015), ops, patch);
|
719
|
-
}
|
720
|
-
return applyPatch(sourceFile, ops, patch);
|
721
|
-
};
|
722
|
-
|
723
724
|
const getCompilerOptions = (rootDir, parseConfigHost) => {
|
724
725
|
const tsConfigPath = path__default.resolve(rootDir, "tsconfig.json");
|
725
726
|
const jsConfigPath = path__default.resolve(rootDir, "jsconfig.json");
|
@@ -766,7 +767,9 @@ class ValSourceFileHandler {
|
|
766
767
|
}
|
767
768
|
}
|
768
769
|
writeSourceFile(sourceFile) {
|
769
|
-
return this.writeFile(sourceFile.fileName,
|
770
|
+
return this.writeFile(sourceFile.fileName,
|
771
|
+
// https://github.com/microsoft/TypeScript/issues/36174
|
772
|
+
unescape(sourceFile.text.replace(/\\u/g, "%u")), "utf8");
|
770
773
|
}
|
771
774
|
writeFile(filePath, content, encoding) {
|
772
775
|
this.host.writeFile(filePath, content, encoding);
|
@@ -784,7 +787,7 @@ class ValSourceFileHandler {
|
|
784
787
|
const JsFileLookupMapping = [
|
785
788
|
// NOTE: first one matching will be used
|
786
789
|
[".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js", ".esm.js", ".mjs.js"]]];
|
787
|
-
const MAX_CACHE_SIZE =
|
790
|
+
const MAX_CACHE_SIZE = 10 * 1024 * 1024; // 10 mb
|
788
791
|
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
|
789
792
|
|
790
793
|
class ValModuleLoader {
|
@@ -928,6 +931,11 @@ async function newValQuickJSRuntime(quickJSModule, moduleLoader, {
|
|
928
931
|
value: "export default new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'react' in this file`) } } } )"
|
929
932
|
};
|
930
933
|
}
|
934
|
+
if (modulePath === "./ValNextProvider") {
|
935
|
+
return {
|
936
|
+
value: "export const ValNextProvider = new Proxy({}, { get() { return () => { throw new Error(`Cannot import 'ValProvider' in this file`) } } } )"
|
937
|
+
};
|
938
|
+
}
|
931
939
|
return {
|
932
940
|
value: moduleLoader.getModule(modulePath)
|
933
941
|
};
|
@@ -963,6 +971,11 @@ async function newValQuickJSRuntime(quickJSModule, moduleLoader, {
|
|
963
971
|
value: requestedName
|
964
972
|
};
|
965
973
|
}
|
974
|
+
if (requestedName === "./ValNextProvider") {
|
975
|
+
return {
|
976
|
+
value: requestedName
|
977
|
+
};
|
978
|
+
}
|
966
979
|
const modulePath = moduleLoader.resolveModulePath(baseModuleName, requestedName);
|
967
980
|
return {
|
968
981
|
value: modulePath
|
@@ -1021,25 +1034,6 @@ class Service {
|
|
1021
1034
|
}
|
1022
1035
|
}
|
1023
1036
|
|
1024
|
-
function createRequestHandler(valServer) {
|
1025
|
-
const router = Router();
|
1026
|
-
router.use("/static", createRequestHandler$1());
|
1027
|
-
router.get("/session", valServer.session.bind(valServer));
|
1028
|
-
router.get("/authorize", valServer.authorize.bind(valServer));
|
1029
|
-
router.get("/callback", valServer.callback.bind(valServer));
|
1030
|
-
router.get("/logout", valServer.logout.bind(valServer));
|
1031
|
-
router.post("/patches/*", express.json({
|
1032
|
-
type: "application/json",
|
1033
|
-
limit: "10mb"
|
1034
|
-
}), valServer.postPatches.bind(valServer)).get("/patches/*", valServer.getPatches.bind(valServer));
|
1035
|
-
router.post("/commit", valServer.commit.bind(valServer));
|
1036
|
-
router.get("/enable", valServer.enable.bind(valServer));
|
1037
|
-
router.get("/disable", valServer.disable.bind(valServer));
|
1038
|
-
router.get("/tree/*", valServer.getTree.bind(valServer));
|
1039
|
-
router.get("/files/*", valServer.getFiles.bind(valServer));
|
1040
|
-
return router;
|
1041
|
-
}
|
1042
|
-
|
1043
1037
|
const JSONValueT = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(JSONValueT), z.record(JSONValueT)]));
|
1044
1038
|
|
1045
1039
|
/**
|
@@ -1082,6 +1076,182 @@ const OperationJSONT = z.discriminatedUnion("op", [z.object({
|
|
1082
1076
|
}).strict()]);
|
1083
1077
|
const PatchJSON = z.array(OperationJSONT);
|
1084
1078
|
|
1079
|
+
const ENABLE_COOKIE_VALUE = {
|
1080
|
+
value: "true",
|
1081
|
+
options: {
|
1082
|
+
httpOnly: false,
|
1083
|
+
sameSite: "lax"
|
1084
|
+
}
|
1085
|
+
};
|
1086
|
+
function getRedirectUrl(query, overrideHost) {
|
1087
|
+
if (typeof query.redirect_to !== "string") {
|
1088
|
+
return {
|
1089
|
+
status: 400,
|
1090
|
+
json: {
|
1091
|
+
message: "Missing redirect_to query param"
|
1092
|
+
}
|
1093
|
+
};
|
1094
|
+
}
|
1095
|
+
if (overrideHost) {
|
1096
|
+
return overrideHost + "?redirect_to=" + encodeURIComponent(query.redirect_to);
|
1097
|
+
}
|
1098
|
+
return query.redirect_to;
|
1099
|
+
}
|
1100
|
+
|
1101
|
+
class LocalValServer {
|
1102
|
+
constructor(options, callbacks) {
|
1103
|
+
this.options = options;
|
1104
|
+
this.callbacks = callbacks;
|
1105
|
+
}
|
1106
|
+
async session() {
|
1107
|
+
return {
|
1108
|
+
status: 200,
|
1109
|
+
json: {
|
1110
|
+
mode: "local",
|
1111
|
+
enabled: await this.callbacks.isEnabled()
|
1112
|
+
}
|
1113
|
+
};
|
1114
|
+
}
|
1115
|
+
async getTree(treePath,
|
1116
|
+
// TODO: use the params: patch, schema, source
|
1117
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
1118
|
+
query) {
|
1119
|
+
const rootDir = process.cwd();
|
1120
|
+
const moduleIds = [];
|
1121
|
+
// iterate over all .val files in the root directory
|
1122
|
+
const walk = async dir => {
|
1123
|
+
const files = await promises.readdir(dir);
|
1124
|
+
for (const file of files) {
|
1125
|
+
if ((await promises.stat(path__default.join(dir, file))).isDirectory()) {
|
1126
|
+
if (file === "node_modules") continue;
|
1127
|
+
await walk(path__default.join(dir, file));
|
1128
|
+
} else {
|
1129
|
+
const isValFile = file.endsWith(".val.js") || file.endsWith(".val.ts");
|
1130
|
+
if (!isValFile) {
|
1131
|
+
continue;
|
1132
|
+
}
|
1133
|
+
if (treePath && !path__default.join(dir, file).replace(rootDir, "").startsWith(treePath)) {
|
1134
|
+
continue;
|
1135
|
+
}
|
1136
|
+
moduleIds.push(path__default.join(dir, file).replace(rootDir, "").replace(".val.js", "").replace(".val.ts", ""));
|
1137
|
+
}
|
1138
|
+
}
|
1139
|
+
};
|
1140
|
+
const serializedModuleContent = await walk(rootDir).then(async () => {
|
1141
|
+
return Promise.all(moduleIds.map(async moduleId => {
|
1142
|
+
return await this.options.service.get(moduleId, "");
|
1143
|
+
}));
|
1144
|
+
});
|
1145
|
+
|
1146
|
+
//
|
1147
|
+
const modules = Object.fromEntries(serializedModuleContent.map(serializedModuleContent => {
|
1148
|
+
const module = {
|
1149
|
+
schema: serializedModuleContent.schema,
|
1150
|
+
source: serializedModuleContent.source,
|
1151
|
+
errors: serializedModuleContent.errors
|
1152
|
+
};
|
1153
|
+
return [serializedModuleContent.path, module];
|
1154
|
+
}));
|
1155
|
+
const apiTreeResponse = {
|
1156
|
+
modules,
|
1157
|
+
git: this.options.git
|
1158
|
+
};
|
1159
|
+
return {
|
1160
|
+
status: 200,
|
1161
|
+
json: apiTreeResponse
|
1162
|
+
};
|
1163
|
+
}
|
1164
|
+
async enable(query) {
|
1165
|
+
const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
|
1166
|
+
if (typeof redirectToRes !== "string") {
|
1167
|
+
return redirectToRes;
|
1168
|
+
}
|
1169
|
+
await this.callbacks.onEnable(true);
|
1170
|
+
return {
|
1171
|
+
cookies: {
|
1172
|
+
[VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
|
1173
|
+
},
|
1174
|
+
status: 302,
|
1175
|
+
redirectTo: redirectToRes
|
1176
|
+
};
|
1177
|
+
}
|
1178
|
+
async disable(query) {
|
1179
|
+
const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
|
1180
|
+
if (typeof redirectToRes !== "string") {
|
1181
|
+
return redirectToRes;
|
1182
|
+
}
|
1183
|
+
await this.callbacks.onDisable(true);
|
1184
|
+
return {
|
1185
|
+
cookies: {
|
1186
|
+
[VAL_ENABLE_COOKIE_NAME]: {
|
1187
|
+
value: null
|
1188
|
+
}
|
1189
|
+
},
|
1190
|
+
status: 302,
|
1191
|
+
redirectTo: redirectToRes
|
1192
|
+
};
|
1193
|
+
}
|
1194
|
+
async postPatches(body) {
|
1195
|
+
// First validate that the body has the right structure
|
1196
|
+
const patchJSON = z$1.record(PatchJSON).safeParse(body);
|
1197
|
+
if (!patchJSON.success) {
|
1198
|
+
return {
|
1199
|
+
status: 404,
|
1200
|
+
json: {
|
1201
|
+
message: `Invalid patch: ${patchJSON.error.message}`,
|
1202
|
+
details: patchJSON.error.issues
|
1203
|
+
}
|
1204
|
+
};
|
1205
|
+
}
|
1206
|
+
const id = randomUUID();
|
1207
|
+
console.time("patching:" + id);
|
1208
|
+
for (const moduleId in patchJSON.data) {
|
1209
|
+
// Then parse/validate
|
1210
|
+
// TODO: validate all and then fail instead:
|
1211
|
+
const patch = parsePatch(patchJSON.data[moduleId]);
|
1212
|
+
if (result.isErr(patch)) {
|
1213
|
+
console.error("Unexpected error parsing patch", patch.error);
|
1214
|
+
throw new Error("Unexpected error parsing patch");
|
1215
|
+
}
|
1216
|
+
await this.options.service.patch(moduleId, patch.value);
|
1217
|
+
}
|
1218
|
+
console.timeEnd("patching:" + id);
|
1219
|
+
return {
|
1220
|
+
status: 200,
|
1221
|
+
json: {} // no patch ids created
|
1222
|
+
};
|
1223
|
+
}
|
1224
|
+
|
1225
|
+
badRequest() {
|
1226
|
+
return {
|
1227
|
+
status: 400,
|
1228
|
+
json: {
|
1229
|
+
message: "Local server does not handle this request"
|
1230
|
+
}
|
1231
|
+
};
|
1232
|
+
}
|
1233
|
+
|
1234
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
1235
|
+
async postCommit() {
|
1236
|
+
return this.badRequest();
|
1237
|
+
}
|
1238
|
+
async authorize() {
|
1239
|
+
return this.badRequest();
|
1240
|
+
}
|
1241
|
+
async callback() {
|
1242
|
+
return this.badRequest();
|
1243
|
+
}
|
1244
|
+
async logout() {
|
1245
|
+
return this.badRequest();
|
1246
|
+
}
|
1247
|
+
async getFiles() {
|
1248
|
+
return this.badRequest();
|
1249
|
+
}
|
1250
|
+
async getPatches() {
|
1251
|
+
return this.badRequest();
|
1252
|
+
}
|
1253
|
+
}
|
1254
|
+
|
1085
1255
|
function decodeJwt(token, secretKey) {
|
1086
1256
|
const [headerBase64, payloadBase64, signatureBase64, ...rest] = token.split(".");
|
1087
1257
|
if (!headerBase64 || !payloadBase64 || !signatureBase64 || rest.length > 0) {
|
@@ -1141,167 +1311,258 @@ function encodeJwt(payload, sessionKey) {
|
|
1141
1311
|
return `${jwtHeaderBase64}.${payloadBase64}.${crypto.createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
|
1142
1312
|
}
|
1143
1313
|
|
1144
|
-
const VAL_SESSION_COOKIE = Internal.VAL_SESSION_COOKIE;
|
1145
|
-
const VAL_STATE_COOKIE = Internal.VAL_STATE_COOKIE;
|
1146
|
-
const VAL_ENABLED_COOKIE = Internal.VAL_ENABLE_COOKIE_NAME;
|
1147
|
-
class BrowserReadableStreamWrapper extends Readable {
|
1148
|
-
constructor(readableStream) {
|
1149
|
-
super();
|
1150
|
-
this.reader = readableStream.getReader();
|
1151
|
-
}
|
1152
|
-
_read() {
|
1153
|
-
this.reader.read().then(({
|
1154
|
-
done,
|
1155
|
-
value
|
1156
|
-
}) => {
|
1157
|
-
if (done) {
|
1158
|
-
this.push(null); // No more data to read
|
1159
|
-
} else {
|
1160
|
-
this.push(Buffer.from(value));
|
1161
|
-
}
|
1162
|
-
}).catch(error => {
|
1163
|
-
this.emit("error", error);
|
1164
|
-
});
|
1165
|
-
}
|
1166
|
-
}
|
1167
1314
|
class ProxyValServer {
|
1168
|
-
constructor(options) {
|
1315
|
+
constructor(options, callbacks) {
|
1169
1316
|
this.options = options;
|
1317
|
+
this.callbacks = callbacks;
|
1170
1318
|
}
|
1171
|
-
async getFiles(
|
1172
|
-
return this.withAuth(
|
1173
|
-
const url = new URL(`/v1/files/${this.options.valName}/${
|
1174
|
-
if (typeof
|
1175
|
-
url.searchParams.append("sha256",
|
1319
|
+
async getFiles(treePath, query, cookies) {
|
1320
|
+
return this.withAuth(cookies, "getFiles", async data => {
|
1321
|
+
const url = new URL(`/v1/files/${this.options.valName}/${treePath}`, this.options.valContentUrl);
|
1322
|
+
if (typeof query.sha256 === "string") {
|
1323
|
+
url.searchParams.append("sha256", query.sha256);
|
1176
1324
|
} else {
|
1177
1325
|
console.warn("Missing sha256 query param");
|
1178
1326
|
}
|
1179
1327
|
const fetchRes = await fetch(url, {
|
1180
1328
|
headers: this.getAuthHeaders(data.token)
|
1181
1329
|
});
|
1182
|
-
|
1183
|
-
if (contentType !== null) {
|
1184
|
-
res.setHeader("Content-Type", contentType);
|
1185
|
-
}
|
1186
|
-
const contentLength = fetchRes.headers.get("content-length");
|
1187
|
-
if (contentLength !== null) {
|
1188
|
-
res.setHeader("Content-Length", contentLength);
|
1189
|
-
}
|
1190
|
-
if (fetchRes.ok) {
|
1330
|
+
if (fetchRes.status === 200) {
|
1191
1331
|
if (fetchRes.body) {
|
1192
|
-
|
1332
|
+
return {
|
1333
|
+
status: fetchRes.status,
|
1334
|
+
headers: {
|
1335
|
+
"Content-Type": fetchRes.headers.get("Content-Type") || "",
|
1336
|
+
"Content-Length": fetchRes.headers.get("Content-Length") || "0"
|
1337
|
+
},
|
1338
|
+
body: fetchRes.body
|
1339
|
+
};
|
1193
1340
|
} else {
|
1194
|
-
|
1195
|
-
|
1341
|
+
return {
|
1342
|
+
status: 500,
|
1343
|
+
body: {
|
1344
|
+
message: "No body in response"
|
1345
|
+
}
|
1346
|
+
};
|
1196
1347
|
}
|
1197
1348
|
} else {
|
1198
|
-
|
1349
|
+
return {
|
1350
|
+
status: fetchRes.status,
|
1351
|
+
body: {
|
1352
|
+
message: "Failed to get files"
|
1353
|
+
}
|
1354
|
+
};
|
1199
1355
|
}
|
1200
1356
|
});
|
1201
1357
|
}
|
1202
|
-
async authorize(
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
1358
|
+
async authorize(query) {
|
1359
|
+
if (typeof query.redirect_to !== "string") {
|
1360
|
+
return {
|
1361
|
+
status: 400,
|
1362
|
+
json: {
|
1363
|
+
message: "Missing redirect_to query param"
|
1364
|
+
}
|
1365
|
+
};
|
1209
1366
|
}
|
1210
1367
|
const token = crypto.randomUUID();
|
1211
|
-
const redirectUrl = new URL(redirect_to);
|
1368
|
+
const redirectUrl = new URL(query.redirect_to);
|
1212
1369
|
const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1216
|
-
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1370
|
+
return {
|
1371
|
+
cookies: {
|
1372
|
+
[VAL_STATE_COOKIE$1]: {
|
1373
|
+
value: createStateCookie({
|
1374
|
+
redirect_to: query.redirect_to,
|
1375
|
+
token
|
1376
|
+
}),
|
1377
|
+
options: {
|
1378
|
+
httpOnly: true,
|
1379
|
+
sameSite: "lax",
|
1380
|
+
expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
|
1381
|
+
}
|
1382
|
+
}
|
1383
|
+
},
|
1384
|
+
|
1385
|
+
status: 302,
|
1386
|
+
redirectTo: appAuthorizeUrl
|
1387
|
+
};
|
1221
1388
|
}
|
1222
|
-
async enable(
|
1223
|
-
|
1389
|
+
async enable(query) {
|
1390
|
+
const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
|
1391
|
+
if (typeof redirectToRes !== "string") {
|
1392
|
+
return redirectToRes;
|
1393
|
+
}
|
1394
|
+
await this.callbacks.onEnable(true);
|
1395
|
+
return {
|
1396
|
+
cookies: {
|
1397
|
+
[VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
|
1398
|
+
},
|
1399
|
+
status: 302,
|
1400
|
+
redirectTo: redirectToRes
|
1401
|
+
};
|
1224
1402
|
}
|
1225
|
-
async disable(
|
1226
|
-
|
1403
|
+
async disable(query) {
|
1404
|
+
const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
|
1405
|
+
if (typeof redirectToRes !== "string") {
|
1406
|
+
return redirectToRes;
|
1407
|
+
}
|
1408
|
+
await this.callbacks.onDisable(true);
|
1409
|
+
return {
|
1410
|
+
cookies: {
|
1411
|
+
[VAL_ENABLE_COOKIE_NAME]: {
|
1412
|
+
value: null
|
1413
|
+
}
|
1414
|
+
},
|
1415
|
+
status: 302,
|
1416
|
+
redirectTo: redirectToRes
|
1417
|
+
};
|
1227
1418
|
}
|
1228
|
-
async callback(
|
1419
|
+
async callback(query, cookies) {
|
1229
1420
|
const {
|
1230
1421
|
success: callbackReqSuccess,
|
1231
1422
|
error: callbackReqError
|
1232
|
-
} = verifyCallbackReq(
|
1233
|
-
res.clearCookie(VAL_STATE_COOKIE); // we don't need this anymore
|
1234
|
-
|
1423
|
+
} = verifyCallbackReq(cookies[VAL_STATE_COOKIE$1], query);
|
1235
1424
|
if (callbackReqError !== null) {
|
1236
|
-
|
1237
|
-
|
1425
|
+
return {
|
1426
|
+
status: 302,
|
1427
|
+
cookies: {
|
1428
|
+
[VAL_STATE_COOKIE$1]: {
|
1429
|
+
value: null
|
1430
|
+
}
|
1431
|
+
},
|
1432
|
+
redirectTo: this.getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`)
|
1433
|
+
};
|
1238
1434
|
}
|
1239
1435
|
const data = await this.consumeCode(callbackReqSuccess.code);
|
1240
1436
|
if (data === null) {
|
1241
|
-
|
1242
|
-
|
1437
|
+
return {
|
1438
|
+
status: 302,
|
1439
|
+
cookies: {
|
1440
|
+
[VAL_STATE_COOKIE$1]: {
|
1441
|
+
value: null
|
1442
|
+
}
|
1443
|
+
},
|
1444
|
+
redirectTo: this.getAppErrorUrl("Failed to exchange code for user")
|
1445
|
+
};
|
1243
1446
|
}
|
1244
1447
|
const exp = getExpire();
|
1245
1448
|
const cookie = encodeJwt({
|
1246
1449
|
...data,
|
1247
1450
|
exp // this is the client side exp
|
1248
1451
|
}, this.options.valSecret);
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
1252
|
-
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1260
|
-
|
1452
|
+
return {
|
1453
|
+
status: 302,
|
1454
|
+
cookies: {
|
1455
|
+
[VAL_STATE_COOKIE$1]: {
|
1456
|
+
value: null
|
1457
|
+
},
|
1458
|
+
[VAL_SESSION_COOKIE$1]: {
|
1459
|
+
value: cookie,
|
1460
|
+
options: {
|
1461
|
+
httpOnly: true,
|
1462
|
+
sameSite: "strict",
|
1463
|
+
path: "/",
|
1464
|
+
secure: true,
|
1465
|
+
expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
|
1466
|
+
}
|
1467
|
+
}
|
1468
|
+
},
|
1469
|
+
|
1470
|
+
redirectTo: callbackReqSuccess.redirect_uri || "/"
|
1471
|
+
};
|
1472
|
+
}
|
1473
|
+
async logout() {
|
1474
|
+
return {
|
1475
|
+
status: 200,
|
1476
|
+
cookies: {
|
1477
|
+
[VAL_SESSION_COOKIE$1]: {
|
1478
|
+
value: null
|
1479
|
+
},
|
1480
|
+
[VAL_STATE_COOKIE$1]: {
|
1481
|
+
value: null
|
1482
|
+
}
|
1483
|
+
}
|
1484
|
+
};
|
1485
|
+
}
|
1486
|
+
async withAuth(cookies, errorMessageType, handler) {
|
1487
|
+
const cookie = cookies[VAL_SESSION_COOKIE$1];
|
1261
1488
|
if (typeof cookie === "string") {
|
1262
|
-
const
|
1489
|
+
const decodedToken = decodeJwt(cookie, this.options.valSecret);
|
1490
|
+
if (!decodedToken) {
|
1491
|
+
return {
|
1492
|
+
status: 401,
|
1493
|
+
json: {
|
1494
|
+
message: "Invalid JWT token"
|
1495
|
+
}
|
1496
|
+
};
|
1497
|
+
}
|
1498
|
+
const verification = IntegratedServerJwtPayload.safeParse(decodedToken);
|
1263
1499
|
if (!verification.success) {
|
1264
|
-
|
1265
|
-
|
1500
|
+
return {
|
1501
|
+
status: 401,
|
1502
|
+
json: {
|
1503
|
+
message: "Could not parse JWT",
|
1504
|
+
details: verification.error
|
1505
|
+
}
|
1506
|
+
};
|
1266
1507
|
}
|
1267
1508
|
return handler(verification.data).catch(err => {
|
1268
|
-
console.error(`Failed while processing: ${
|
1269
|
-
|
1270
|
-
|
1509
|
+
console.error(`Failed while processing: ${errorMessageType}`, err);
|
1510
|
+
return {
|
1511
|
+
status: 500,
|
1512
|
+
body: {
|
1513
|
+
message: err.message
|
1514
|
+
}
|
1515
|
+
};
|
1271
1516
|
});
|
1272
1517
|
} else {
|
1273
|
-
|
1518
|
+
return {
|
1519
|
+
status: 401,
|
1520
|
+
json: {
|
1521
|
+
message: "No token"
|
1522
|
+
}
|
1523
|
+
};
|
1274
1524
|
}
|
1275
1525
|
}
|
1276
|
-
async session(
|
1277
|
-
return this.withAuth(
|
1526
|
+
async session(cookies) {
|
1527
|
+
return this.withAuth(cookies, "session", async data => {
|
1278
1528
|
const url = new URL(`/api/val/${this.options.valName}/auth/session`, this.options.valBuildUrl);
|
1279
1529
|
const fetchRes = await fetch(url, {
|
1280
1530
|
headers: this.getAuthHeaders(data.token, "application/json")
|
1281
1531
|
});
|
1282
|
-
if (fetchRes.
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1532
|
+
if (fetchRes.status === 200) {
|
1533
|
+
return {
|
1534
|
+
status: fetchRes.status,
|
1535
|
+
json: {
|
1536
|
+
mode: "proxy",
|
1537
|
+
enabled: await this.callbacks.isEnabled(),
|
1538
|
+
...(await fetchRes.json())
|
1539
|
+
}
|
1540
|
+
};
|
1287
1541
|
} else {
|
1288
|
-
|
1542
|
+
return {
|
1543
|
+
status: fetchRes.status,
|
1544
|
+
body: {
|
1545
|
+
message: "Failed to get session"
|
1546
|
+
}
|
1547
|
+
};
|
1289
1548
|
}
|
1290
1549
|
});
|
1291
1550
|
}
|
1292
|
-
async getTree(
|
1293
|
-
return this.withAuth(
|
1551
|
+
async getTree(treePath, query, cookies) {
|
1552
|
+
return this.withAuth(cookies, "getTree", async data => {
|
1294
1553
|
const {
|
1295
1554
|
patch,
|
1296
1555
|
schema,
|
1297
1556
|
source
|
1298
|
-
} =
|
1557
|
+
} = query;
|
1299
1558
|
const commit = this.options.gitCommit;
|
1300
1559
|
if (!commit) {
|
1301
|
-
|
1302
|
-
|
1303
|
-
|
1304
|
-
|
1560
|
+
return {
|
1561
|
+
status: 400,
|
1562
|
+
body: {
|
1563
|
+
message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
|
1564
|
+
}
|
1565
|
+
};
|
1305
1566
|
}
|
1306
1567
|
const params = new URLSearchParams({
|
1307
1568
|
patch: (patch === "true").toString(),
|
@@ -1309,63 +1570,99 @@ class ProxyValServer {
|
|
1309
1570
|
source: (source === "true").toString(),
|
1310
1571
|
commit
|
1311
1572
|
});
|
1312
|
-
const url = new URL(`/v1/tree/${this.options.valName}/heads/${this.options.gitBranch}/${
|
1313
|
-
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
1317
|
-
|
1318
|
-
|
1319
|
-
|
1573
|
+
const url = new URL(`/v1/tree/${this.options.valName}/heads/${this.options.gitBranch}/${treePath}/?${params}`, this.options.valContentUrl);
|
1574
|
+
try {
|
1575
|
+
const fetchRes = await fetch(url, {
|
1576
|
+
headers: this.getAuthHeaders(data.token, "application/json")
|
1577
|
+
});
|
1578
|
+
if (fetchRes.status === 200) {
|
1579
|
+
return {
|
1580
|
+
status: fetchRes.status,
|
1581
|
+
json: await fetchRes.json()
|
1582
|
+
};
|
1583
|
+
} else {
|
1584
|
+
try {
|
1585
|
+
var _fetchRes$headers$get;
|
1586
|
+
if ((_fetchRes$headers$get = fetchRes.headers.get("Content-Type")) !== null && _fetchRes$headers$get !== void 0 && _fetchRes$headers$get.includes("application/json")) {
|
1587
|
+
const json = await fetchRes.json();
|
1588
|
+
return {
|
1589
|
+
status: fetchRes.status,
|
1590
|
+
json
|
1591
|
+
};
|
1592
|
+
}
|
1593
|
+
} catch (err) {
|
1594
|
+
console.error(err);
|
1595
|
+
}
|
1596
|
+
return {
|
1597
|
+
status: fetchRes.status,
|
1598
|
+
json: {
|
1599
|
+
message: "Unknown failure while accessing Val"
|
1600
|
+
}
|
1601
|
+
};
|
1602
|
+
}
|
1603
|
+
} catch (err) {
|
1604
|
+
return {
|
1605
|
+
status: 500,
|
1606
|
+
body: {
|
1607
|
+
message: "Failed to fetch: check network connection"
|
1608
|
+
}
|
1609
|
+
};
|
1610
|
+
}
|
1320
1611
|
});
|
1321
1612
|
}
|
1322
|
-
async getPatches(
|
1323
|
-
const patchIds =
|
1613
|
+
async getPatches(query, cookies) {
|
1614
|
+
const patchIds = query.id || [];
|
1324
1615
|
const params = patchIds.length > 0 ? `?${patchIds.map(id => `id=${encodeURIComponent(id)}`).join("&")}` : "";
|
1325
|
-
|
1616
|
+
return this.withAuth(cookies, "getPatches", async ({
|
1326
1617
|
token
|
1327
1618
|
}) => {
|
1328
|
-
const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.gitBranch}
|
1329
|
-
console.log(url);
|
1619
|
+
const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.gitBranch}/~${params}`, this.options.valContentUrl);
|
1330
1620
|
// Proxy patch to val.build
|
1331
1621
|
const fetchRes = await fetch(url, {
|
1332
1622
|
method: "GET",
|
1333
1623
|
headers: this.getAuthHeaders(token, "application/json")
|
1334
1624
|
});
|
1335
|
-
if (fetchRes.
|
1336
|
-
|
1337
|
-
|
1625
|
+
if (fetchRes.status === 200) {
|
1626
|
+
return {
|
1627
|
+
status: fetchRes.status,
|
1628
|
+
json: await fetchRes.json()
|
1629
|
+
};
|
1338
1630
|
} else {
|
1339
|
-
|
1631
|
+
return {
|
1632
|
+
status: fetchRes.status,
|
1633
|
+
body: {
|
1634
|
+
message: "Failed to get patches"
|
1635
|
+
}
|
1636
|
+
};
|
1340
1637
|
}
|
1341
|
-
}).catch(e => {
|
1342
|
-
res.status(500).send({
|
1343
|
-
error: {
|
1344
|
-
message: e === null || e === void 0 ? void 0 : e.message,
|
1345
|
-
status: 500
|
1346
|
-
}
|
1347
|
-
});
|
1348
1638
|
});
|
1349
1639
|
}
|
1350
|
-
async postPatches(
|
1640
|
+
async postPatches(body, cookies) {
|
1351
1641
|
const commit = this.options.gitCommit;
|
1352
1642
|
if (!commit) {
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1643
|
+
return {
|
1644
|
+
status: 401,
|
1645
|
+
json: {
|
1646
|
+
message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
|
1647
|
+
}
|
1648
|
+
};
|
1357
1649
|
}
|
1358
1650
|
const params = new URLSearchParams({
|
1359
1651
|
commit
|
1360
1652
|
});
|
1361
|
-
|
1653
|
+
return this.withAuth(cookies, "postPatches", async ({
|
1362
1654
|
token
|
1363
1655
|
}) => {
|
1364
1656
|
// First validate that the body has the right structure
|
1365
|
-
const patchJSON = z$1.record(PatchJSON).safeParse(
|
1657
|
+
const patchJSON = z$1.record(PatchJSON).safeParse(body);
|
1366
1658
|
if (!patchJSON.success) {
|
1367
|
-
|
1368
|
-
|
1659
|
+
return {
|
1660
|
+
status: 400,
|
1661
|
+
body: {
|
1662
|
+
message: "Invalid patch",
|
1663
|
+
details: patchJSON.error.issues
|
1664
|
+
}
|
1665
|
+
};
|
1369
1666
|
}
|
1370
1667
|
// Then parse/validate
|
1371
1668
|
// TODO:
|
@@ -1375,29 +1672,27 @@ class ProxyValServer {
|
|
1375
1672
|
// res.status(401).json(patch.error);
|
1376
1673
|
// return;
|
1377
1674
|
// }
|
1378
|
-
const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.gitBranch}
|
1675
|
+
const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.gitBranch}/~?${params}`, this.options.valContentUrl);
|
1379
1676
|
// Proxy patch to val.build
|
1380
1677
|
const fetchRes = await fetch(url, {
|
1381
1678
|
method: "POST",
|
1382
1679
|
headers: this.getAuthHeaders(token, "application/json"),
|
1383
1680
|
body: JSON.stringify(patch)
|
1384
1681
|
});
|
1385
|
-
if (fetchRes.
|
1386
|
-
|
1682
|
+
if (fetchRes.status === 200) {
|
1683
|
+
return {
|
1684
|
+
status: fetchRes.status,
|
1685
|
+
json: await fetchRes.json()
|
1686
|
+
};
|
1387
1687
|
} else {
|
1388
|
-
|
1688
|
+
return {
|
1689
|
+
status: fetchRes.status
|
1690
|
+
};
|
1389
1691
|
}
|
1390
|
-
}).catch(e => {
|
1391
|
-
res.status(500).send({
|
1392
|
-
error: {
|
1393
|
-
message: e === null || e === void 0 ? void 0 : e.message,
|
1394
|
-
status: 500
|
1395
|
-
}
|
1396
|
-
});
|
1397
1692
|
});
|
1398
1693
|
}
|
1399
|
-
async
|
1400
|
-
|
1694
|
+
async postCommit(cookies) {
|
1695
|
+
return this.withAuth(cookies, "postCommit", async ({
|
1401
1696
|
token
|
1402
1697
|
}) => {
|
1403
1698
|
const url = new URL(`/api/val/commit/${encodeURIComponent(this.options.gitBranch)}`, this.options.valBuildUrl);
|
@@ -1405,10 +1700,15 @@ class ProxyValServer {
|
|
1405
1700
|
method: "POST",
|
1406
1701
|
headers: this.getAuthHeaders(token)
|
1407
1702
|
});
|
1408
|
-
if (fetchRes.
|
1409
|
-
|
1703
|
+
if (fetchRes.status === 200) {
|
1704
|
+
return {
|
1705
|
+
status: fetchRes.status,
|
1706
|
+
json: await fetchRes.json()
|
1707
|
+
};
|
1410
1708
|
} else {
|
1411
|
-
|
1709
|
+
return {
|
1710
|
+
status: fetchRes.status
|
1711
|
+
};
|
1412
1712
|
}
|
1413
1713
|
});
|
1414
1714
|
}
|
@@ -1561,40 +1861,6 @@ function getStateFromCookie(stateCookie) {
|
|
1561
1861
|
};
|
1562
1862
|
}
|
1563
1863
|
}
|
1564
|
-
async function enable(req, res, redirectUrl) {
|
1565
|
-
const {
|
1566
|
-
redirect_to
|
1567
|
-
} = req.query;
|
1568
|
-
if (typeof redirect_to === "string" || typeof redirect_to === "undefined") {
|
1569
|
-
let redirectUrlToUse = redirect_to || "/";
|
1570
|
-
if (redirectUrl) {
|
1571
|
-
redirectUrlToUse = redirectUrl + "?redirect_to=" + encodeURIComponent(redirectUrlToUse);
|
1572
|
-
}
|
1573
|
-
res.cookie(VAL_ENABLED_COOKIE, "true", {
|
1574
|
-
httpOnly: false,
|
1575
|
-
sameSite: "lax"
|
1576
|
-
}).redirect(redirectUrlToUse);
|
1577
|
-
} else {
|
1578
|
-
res.sendStatus(400);
|
1579
|
-
}
|
1580
|
-
}
|
1581
|
-
async function disable(req, res, redirectUrl) {
|
1582
|
-
const {
|
1583
|
-
redirect_to
|
1584
|
-
} = req.query;
|
1585
|
-
if (typeof redirect_to === "string" || typeof redirect_to === "undefined") {
|
1586
|
-
let redirectUrlToUse = redirect_to || "/";
|
1587
|
-
if (redirectUrl) {
|
1588
|
-
redirectUrlToUse = redirectUrl + "?redirect_to=" + encodeURIComponent(redirectUrlToUse);
|
1589
|
-
}
|
1590
|
-
res.cookie(VAL_ENABLED_COOKIE, "false", {
|
1591
|
-
httpOnly: false,
|
1592
|
-
sameSite: "lax"
|
1593
|
-
}).redirect(redirectUrlToUse);
|
1594
|
-
} else {
|
1595
|
-
res.sendStatus(400);
|
1596
|
-
}
|
1597
|
-
}
|
1598
1864
|
function createStateCookie(state) {
|
1599
1865
|
return Buffer.from(JSON.stringify(state), "utf8").toString("base64");
|
1600
1866
|
}
|
@@ -1612,137 +1878,13 @@ const IntegratedServerJwtPayload = z$1.object({
|
|
1612
1878
|
project: z$1.string()
|
1613
1879
|
});
|
1614
1880
|
|
1615
|
-
|
1616
|
-
constructor(options) {
|
1617
|
-
this.options = options;
|
1618
|
-
}
|
1619
|
-
async session(_req, res) {
|
1620
|
-
res.json({
|
1621
|
-
mode: "local"
|
1622
|
-
});
|
1623
|
-
}
|
1624
|
-
async getTree(req, res) {
|
1625
|
-
try {
|
1626
|
-
// TODO: use the params: patch, schema, source
|
1627
|
-
const treePath = req.params["0"].replace("~", "");
|
1628
|
-
const rootDir = process.cwd();
|
1629
|
-
const moduleIds = [];
|
1630
|
-
// iterate over all .val files in the root directory
|
1631
|
-
const walk = async dir => {
|
1632
|
-
const files = await promises.readdir(dir);
|
1633
|
-
for (const file of files) {
|
1634
|
-
if ((await promises.stat(path__default.join(dir, file))).isDirectory()) {
|
1635
|
-
if (file === "node_modules") continue;
|
1636
|
-
await walk(path__default.join(dir, file));
|
1637
|
-
} else {
|
1638
|
-
const isValFile = file.endsWith(".val.js") || file.endsWith(".val.ts");
|
1639
|
-
if (!isValFile) {
|
1640
|
-
continue;
|
1641
|
-
}
|
1642
|
-
if (treePath && !path__default.join(dir, file).replace(rootDir, "").startsWith(treePath)) {
|
1643
|
-
continue;
|
1644
|
-
}
|
1645
|
-
moduleIds.push(path__default.join(dir, file).replace(rootDir, "").replace(".val.js", "").replace(".val.ts", ""));
|
1646
|
-
}
|
1647
|
-
}
|
1648
|
-
};
|
1649
|
-
const serializedModuleContent = await walk(rootDir).then(async () => {
|
1650
|
-
return Promise.all(moduleIds.map(async moduleId => {
|
1651
|
-
return await this.options.service.get(moduleId, "");
|
1652
|
-
}));
|
1653
|
-
});
|
1654
|
-
|
1655
|
-
//
|
1656
|
-
const modules = Object.fromEntries(serializedModuleContent.map(serializedModuleContent => {
|
1657
|
-
const module = {
|
1658
|
-
schema: serializedModuleContent.schema,
|
1659
|
-
source: serializedModuleContent.source,
|
1660
|
-
errors: serializedModuleContent.errors
|
1661
|
-
};
|
1662
|
-
return [serializedModuleContent.path, module];
|
1663
|
-
}));
|
1664
|
-
const apiTreeResponse = {
|
1665
|
-
modules,
|
1666
|
-
git: this.options.git
|
1667
|
-
};
|
1668
|
-
res.send(JSON.stringify(apiTreeResponse));
|
1669
|
-
} catch (err) {
|
1670
|
-
console.error(err);
|
1671
|
-
res.sendStatus(500);
|
1672
|
-
}
|
1673
|
-
}
|
1674
|
-
async enable(req, res) {
|
1675
|
-
return enable(req, res);
|
1676
|
-
}
|
1677
|
-
async disable(req, res) {
|
1678
|
-
return disable(req, res);
|
1679
|
-
}
|
1680
|
-
async postPatches(req, res) {
|
1681
|
-
// First validate that the body has the right structure
|
1682
|
-
const patchJSON = z$1.record(PatchJSON).safeParse(req.body);
|
1683
|
-
if (!patchJSON.success) {
|
1684
|
-
res.status(401).json(patchJSON.error.issues);
|
1685
|
-
return;
|
1686
|
-
}
|
1687
|
-
try {
|
1688
|
-
for (const moduleId in patchJSON.data) {
|
1689
|
-
// Then parse/validate
|
1690
|
-
// TODO: validate all and then fail instead:
|
1691
|
-
const patch = parsePatch(patchJSON.data[moduleId]);
|
1692
|
-
if (result.isErr(patch)) {
|
1693
|
-
res.status(401).json(patch.error);
|
1694
|
-
return;
|
1695
|
-
}
|
1696
|
-
await this.options.service.patch(moduleId, patch.value);
|
1697
|
-
}
|
1698
|
-
res.json({});
|
1699
|
-
} catch (err) {
|
1700
|
-
if (err instanceof PatchError) {
|
1701
|
-
res.status(400).send({
|
1702
|
-
message: err.message
|
1703
|
-
});
|
1704
|
-
} else {
|
1705
|
-
console.error(err);
|
1706
|
-
res.status(500).send({
|
1707
|
-
message: err instanceof Error ? err.message : "Unknown error"
|
1708
|
-
});
|
1709
|
-
}
|
1710
|
-
}
|
1711
|
-
}
|
1712
|
-
async badRequest(req, res) {
|
1713
|
-
console.debug("Local server does handle this request", req.url);
|
1714
|
-
res.sendStatus(400);
|
1715
|
-
}
|
1716
|
-
commit(req, res) {
|
1717
|
-
return this.badRequest(req, res);
|
1718
|
-
}
|
1719
|
-
authorize(req, res) {
|
1720
|
-
return this.badRequest(req, res);
|
1721
|
-
}
|
1722
|
-
callback(req, res) {
|
1723
|
-
return this.badRequest(req, res);
|
1724
|
-
}
|
1725
|
-
logout(req, res) {
|
1726
|
-
return this.badRequest(req, res);
|
1727
|
-
}
|
1728
|
-
getFiles(req, res) {
|
1729
|
-
return this.badRequest(req, res);
|
1730
|
-
}
|
1731
|
-
getPatches(req, res) {
|
1732
|
-
return this.badRequest(req, res);
|
1733
|
-
}
|
1734
|
-
}
|
1735
|
-
|
1736
|
-
async function _createRequestListener(route, opts) {
|
1881
|
+
async function createValServer(route, opts, callbacks) {
|
1737
1882
|
const serverOpts = await initHandlerOptions(route, opts);
|
1738
|
-
let valServer;
|
1739
1883
|
if (serverOpts.mode === "proxy") {
|
1740
|
-
|
1884
|
+
return new ProxyValServer(serverOpts, callbacks);
|
1741
1885
|
} else {
|
1742
|
-
|
1886
|
+
return new LocalValServer(serverOpts, callbacks);
|
1743
1887
|
}
|
1744
|
-
const reqHandler = createRequestHandler(valServer);
|
1745
|
-
return express().use(route, reqHandler);
|
1746
1888
|
}
|
1747
1889
|
async function initHandlerOptions(route, opts) {
|
1748
1890
|
const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
|
@@ -1786,6 +1928,8 @@ async function initHandlerOptions(route, opts) {
|
|
1786
1928
|
return {
|
1787
1929
|
mode: "local",
|
1788
1930
|
service,
|
1931
|
+
valEnableRedirectUrl: opts.valEnableRedirectUrl || process.env.VAL_ENABLE_REDIRECT_URL,
|
1932
|
+
valDisableRedirectUrl: opts.valDisableRedirectUrl || process.env.VAL_DISABLE_REDIRECT_URL,
|
1789
1933
|
git: {
|
1790
1934
|
commit: process.env.VAL_GIT_COMMIT || git.commit,
|
1791
1935
|
branch: process.env.VAL_GIT_BRANCH || git.branch
|
@@ -1848,22 +1992,127 @@ async function readCommit(gitDir, branchName) {
|
|
1848
1992
|
return undefined;
|
1849
1993
|
}
|
1850
1994
|
}
|
1851
|
-
|
1852
|
-
|
1853
|
-
|
1854
|
-
|
1855
|
-
|
1856
|
-
|
1857
|
-
|
1858
|
-
|
1859
|
-
|
1860
|
-
|
1861
|
-
|
1862
|
-
|
1995
|
+
const {
|
1996
|
+
VAL_SESSION_COOKIE,
|
1997
|
+
VAL_STATE_COOKIE
|
1998
|
+
} = Internal;
|
1999
|
+
const TREE_PATH_PREFIX = "/tree/~";
|
2000
|
+
const PATCHES_PATH_PREFIX = "/patches/~";
|
2001
|
+
const FILES_PATH_PREFIX = "/files";
|
2002
|
+
function createValApiRouter(route, valServerPromise, convert) {
|
2003
|
+
const uiRequestHandler = createUIRequestHandler();
|
2004
|
+
return async req => {
|
2005
|
+
var _req$method;
|
2006
|
+
const valServer = await valServerPromise;
|
2007
|
+
req.headers.get("content-type");
|
2008
|
+
req.headers.get("Cookie");
|
2009
|
+
const url = new URL(req.url);
|
2010
|
+
if (!url.pathname.startsWith(route)) {
|
2011
|
+
const error = {
|
2012
|
+
message: "Val: routes are not configured correctly",
|
2013
|
+
details: `Check you api routes. Expected pathname to start with "${route}", but it was: "${url.pathname}"`
|
2014
|
+
};
|
2015
|
+
console.error(error);
|
2016
|
+
return convert({
|
2017
|
+
status: 500,
|
2018
|
+
json: error
|
2019
|
+
});
|
2020
|
+
}
|
2021
|
+
const method = (_req$method = req.method) === null || _req$method === void 0 ? void 0 : _req$method.toUpperCase();
|
2022
|
+
function withTreePath(path, prefix) {
|
2023
|
+
return async useTreePath => {
|
2024
|
+
const pathIndex = path.indexOf("~");
|
2025
|
+
if (path.startsWith(prefix) && pathIndex !== -1) {
|
2026
|
+
return useTreePath(path.slice(pathIndex + 1));
|
2027
|
+
} else {
|
2028
|
+
if (prefix.indexOf("/~") === -1) {
|
2029
|
+
return convert({
|
2030
|
+
status: 500,
|
2031
|
+
json: {
|
2032
|
+
message: `Route is incorrectly formed: ${prefix}!`
|
2033
|
+
}
|
2034
|
+
});
|
2035
|
+
}
|
2036
|
+
return convert({
|
2037
|
+
status: 404,
|
2038
|
+
json: {
|
2039
|
+
message: `Malformed ${prefix} path! Expected: '${prefix}'`
|
2040
|
+
}
|
2041
|
+
});
|
2042
|
+
}
|
2043
|
+
};
|
2044
|
+
}
|
2045
|
+
const path = url.pathname.slice(route.length);
|
2046
|
+
if (path.startsWith("/static")) {
|
2047
|
+
return convert(await uiRequestHandler(path.slice("/static".length)));
|
2048
|
+
} else if (path === "/session") {
|
2049
|
+
return convert(await valServer.session(getCookies(req, [VAL_SESSION_COOKIE])));
|
2050
|
+
} else if (path === "/authorize") {
|
2051
|
+
return convert(await valServer.authorize({
|
2052
|
+
redirect_to: url.searchParams.get("redirect_to") || undefined
|
2053
|
+
}));
|
2054
|
+
} else if (path === "/callback") {
|
2055
|
+
return convert(await valServer.callback({
|
2056
|
+
code: url.searchParams.get("code") || undefined,
|
2057
|
+
state: url.searchParams.get("state") || undefined
|
2058
|
+
}, getCookies(req, [VAL_STATE_COOKIE])));
|
2059
|
+
} else if (path === "/logout") {
|
2060
|
+
return convert(await valServer.logout());
|
2061
|
+
} else if (path === "/enable") {
|
2062
|
+
return convert(await valServer.enable({
|
2063
|
+
redirect_to: url.searchParams.get("redirect_to") || undefined
|
2064
|
+
}));
|
2065
|
+
} else if (path === "/disable") {
|
2066
|
+
return convert(await valServer.disable({
|
2067
|
+
redirect_to: url.searchParams.get("redirect_to") || undefined
|
2068
|
+
}));
|
2069
|
+
} else if (method === "POST" && path === "/commit") {
|
2070
|
+
return convert(await valServer.postCommit(getCookies(req, [VAL_SESSION_COOKIE])));
|
2071
|
+
} else if (method === "GET" && path.startsWith(TREE_PATH_PREFIX)) {
|
2072
|
+
return withTreePath(path, TREE_PATH_PREFIX)(async treePath => convert(await valServer.getTree(treePath, {
|
2073
|
+
patch: url.searchParams.get("patch") || undefined,
|
2074
|
+
schema: url.searchParams.get("schema") || undefined,
|
2075
|
+
source: url.searchParams.get("source") || undefined
|
2076
|
+
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
2077
|
+
} else if (method === "GET" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
2078
|
+
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.getPatches({
|
2079
|
+
id: url.searchParams.getAll("id")
|
2080
|
+
}, getCookies(req, [VAL_SESSION_COOKIE]))));
|
2081
|
+
} else if (method === "POST" && path.startsWith(PATCHES_PATH_PREFIX)) {
|
2082
|
+
const body = await req.json();
|
2083
|
+
return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.postPatches(body, getCookies(req, [VAL_SESSION_COOKIE]))));
|
2084
|
+
} else if (path.startsWith(FILES_PATH_PREFIX)) {
|
2085
|
+
const treePath = path.slice(FILES_PATH_PREFIX.length);
|
2086
|
+
return convert(await valServer.getFiles(treePath, {
|
2087
|
+
sha256: url.searchParams.get("sha256") || undefined
|
2088
|
+
}, getCookies(req, [VAL_SESSION_COOKIE])));
|
2089
|
+
} else {
|
2090
|
+
return convert({
|
2091
|
+
status: 404,
|
2092
|
+
json: {
|
2093
|
+
message: "Not Found",
|
2094
|
+
details: {
|
2095
|
+
method,
|
2096
|
+
path
|
2097
|
+
}
|
2098
|
+
}
|
2099
|
+
});
|
1863
2100
|
}
|
1864
2101
|
};
|
1865
2102
|
}
|
1866
2103
|
|
2104
|
+
// TODO: is this naive implementation is too naive?
|
2105
|
+
function getCookies(req, names) {
|
2106
|
+
var _req$headers$get;
|
2107
|
+
return ((_req$headers$get = req.headers.get("Cookie")) === null || _req$headers$get === void 0 ? void 0 : _req$headers$get.split("; ").reduce((acc, cookie) => {
|
2108
|
+
const [name, value] = cookie.split("=");
|
2109
|
+
if (names.includes(name.trim())) {
|
2110
|
+
acc[name.trim()] = decodeURIComponent(value.trim());
|
2111
|
+
}
|
2112
|
+
return acc;
|
2113
|
+
}, {})) || {};
|
2114
|
+
}
|
2115
|
+
|
1867
2116
|
/**
|
1868
2117
|
* An implementation of methods in the various ts.*Host interfaces
|
1869
2118
|
* that uses ValFS to resolve modules and read/write files.
|
@@ -2004,4 +2253,4 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
2004
2253
|
};
|
2005
2254
|
}
|
2006
2255
|
|
2007
|
-
export { LocalValServer, PatchJSON, Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createFixPatch,
|
2256
|
+
export { LocalValServer, PatchJSON, Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createFixPatch, createService, createValApiRouter, createValServer, decodeJwt, encodeJwt, formatSyntaxErrorTree, getCompilerOptions, getExpire, patchSourceFile, safeReadGit };
|