@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.
@@ -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 crypto = require('crypto');
17
- var stream = require('stream');
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("Path refers to non-existing object/array"));
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, sourceFile.text, "utf8");
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 = 100 * 1024 * 1024; // 100 mb
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(req, res) {
1203
- return this.withAuth(req, res, async data => {
1204
- const url = new URL(`/v1/files/${this.options.valName}/${req.params["0"]}`, this.options.valContentUrl);
1205
- if (typeof req.query.sha256 === "string") {
1206
- url.searchParams.append("sha256", req.query.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
- const contentType = fetchRes.headers.get("content-type");
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
- new BrowserReadableStreamWrapper(fetchRes.body).pipe(res);
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
- console.warn("No body in response");
1226
- res.sendStatus(500);
1371
+ return {
1372
+ status: 500,
1373
+ body: {
1374
+ message: "No body in response"
1375
+ }
1376
+ };
1227
1377
  }
1228
1378
  } else {
1229
- res.sendStatus(fetchRes.status);
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(req, res) {
1234
- const {
1235
- redirect_to
1236
- } = req.query;
1237
- if (typeof redirect_to !== "string") {
1238
- res.redirect(this.getAppErrorUrl("Login failed: missing redirect_to param"));
1239
- return;
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
- res.cookie(VAL_STATE_COOKIE, createStateCookie({
1245
- redirect_to,
1246
- token
1247
- }), {
1248
- httpOnly: true,
1249
- sameSite: "lax",
1250
- expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
1251
- }).redirect(appAuthorizeUrl);
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(req, res) {
1254
- return enable(req, res, this.options.valEnableRedirectUrl);
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(req, res) {
1257
- return disable(req, res, this.options.valEnableRedirectUrl);
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(req, res) {
1449
+ async callback(query, cookies) {
1260
1450
  const {
1261
1451
  success: callbackReqSuccess,
1262
1452
  error: callbackReqError
1263
- } = verifyCallbackReq(req.cookies[VAL_STATE_COOKIE], req.query);
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
- res.redirect(this.getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`));
1268
- return;
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
- res.redirect(this.getAppErrorUrl("Failed to exchange code for user"));
1273
- return;
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
- res.cookie(VAL_SESSION_COOKIE, cookie, {
1281
- httpOnly: true,
1282
- sameSite: "strict",
1283
- secure: true,
1284
- expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
1285
- }).redirect(callbackReqSuccess.redirect_uri || "/");
1286
- }
1287
- async logout(_req, res) {
1288
- res.clearCookie(VAL_SESSION_COOKIE).clearCookie(VAL_STATE_COOKIE).sendStatus(200);
1289
- }
1290
- async withAuth(req, res, handler) {
1291
- const cookie = req.cookies[VAL_SESSION_COOKIE];
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 verification = IntegratedServerJwtPayload.safeParse(decodeJwt(cookie, this.options.valSecret));
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
- res.sendStatus(401);
1296
- return;
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: ${req.url}`, err);
1300
- res.sendStatus(500);
1301
- return undefined;
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
- res.sendStatus(401);
1548
+ return {
1549
+ status: 401,
1550
+ json: {
1551
+ message: "No token"
1552
+ }
1553
+ };
1305
1554
  }
1306
1555
  }
1307
- async session(req, res) {
1308
- return this.withAuth(req, res, async data => {
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.ok) {
1314
- res.status(fetchRes.status).json({
1315
- mode: "proxy",
1316
- ...(await fetchRes.json())
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
- res.sendStatus(fetchRes.status);
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(req, res) {
1324
- return this.withAuth(req, res, async data => {
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
- } = req.query;
1587
+ } = query;
1330
1588
  const commit = this.options.gitCommit;
1331
1589
  if (!commit) {
1332
- res.status(401).json({
1333
- error: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
1334
- });
1335
- return;
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}/${req.params["0"]}/?${params}`, this.options.valContentUrl);
1344
- const json = await fetch(url, {
1345
- headers: this.getAuthHeaders(data.token, "application/json")
1346
- }).then(res => res.json()).catch(err => {
1347
- console.error(err);
1348
- throw err;
1349
- });
1350
- res.send(json);
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(req, res) {
1354
- const patchIds = typeof req.params["id"] === "string" ? [req.params["id"]] : Array.isArray(req.params["id"]) ? req.params["id"] : [];
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
- await this.withAuth(req, res, async ({
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}/${req.params["0"]}${params}`, this.options.valContentUrl);
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.ok) {
1367
- const json = await fetchRes.json();
1368
- res.status(fetchRes.status).json(json);
1655
+ if (fetchRes.status === 200) {
1656
+ return {
1657
+ status: fetchRes.status,
1658
+ json: await fetchRes.json()
1659
+ };
1369
1660
  } else {
1370
- res.sendStatus(fetchRes.status);
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(req, res) {
1670
+ async postPatches(body, cookies) {
1382
1671
  const commit = this.options.gitCommit;
1383
1672
  if (!commit) {
1384
- res.status(401).json({
1385
- error: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
1386
- });
1387
- return;
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
- await this.withAuth(req, res, async ({
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(req.body);
1687
+ const patchJSON = z.z.record(PatchJSON).safeParse(body);
1397
1688
  if (!patchJSON.success) {
1398
- res.status(401).json(patchJSON.error.issues);
1399
- return;
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}/${req.params["0"]}/?${params}`, this.options.valContentUrl);
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.ok) {
1417
- res.status(fetchRes.status).json(await fetchRes.json());
1712
+ if (fetchRes.status === 200) {
1713
+ return {
1714
+ status: fetchRes.status,
1715
+ json: await fetchRes.json()
1716
+ };
1418
1717
  } else {
1419
- res.sendStatus(fetchRes.status);
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 commit(req, res) {
1431
- await this.withAuth(req, res, async ({
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.ok) {
1440
- res.status(fetchRes.status).json(await fetchRes.json());
1733
+ if (fetchRes.status === 200) {
1734
+ return {
1735
+ status: fetchRes.status,
1736
+ json: await fetchRes.json()
1737
+ };
1441
1738
  } else {
1442
- res.sendStatus(fetchRes.status);
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
- class LocalValServer {
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
- valServer = new ProxyValServer(serverOpts);
1914
+ return new ProxyValServer(serverOpts, callbacks);
1772
1915
  } else {
1773
- valServer = new LocalValServer(serverOpts);
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
- // TODO: rename to createValApiHandlers?
1884
- function createRequestListener(route, opts) {
1885
- const handler = _createRequestListener(route, opts);
1886
- return async (req, res) => {
1887
- try {
1888
- return (await handler)(req, res);
1889
- } catch (e) {
1890
- res.statusCode = 500;
1891
- res.write(e instanceof Error ? e.message : "Unknown error");
1892
- res.end();
1893
- return;
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;