@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.
@@ -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 crypto from 'crypto';
14
- import { Readable } from 'stream';
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("Path refers to non-existing object/array"));
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, sourceFile.text, "utf8");
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 = 100 * 1024 * 1024; // 100 mb
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(req, res) {
1172
- return this.withAuth(req, res, async data => {
1173
- const url = new URL(`/v1/files/${this.options.valName}/${req.params["0"]}`, this.options.valContentUrl);
1174
- if (typeof req.query.sha256 === "string") {
1175
- url.searchParams.append("sha256", req.query.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
- const contentType = fetchRes.headers.get("content-type");
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
- new BrowserReadableStreamWrapper(fetchRes.body).pipe(res);
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
- console.warn("No body in response");
1195
- res.sendStatus(500);
1341
+ return {
1342
+ status: 500,
1343
+ body: {
1344
+ message: "No body in response"
1345
+ }
1346
+ };
1196
1347
  }
1197
1348
  } else {
1198
- res.sendStatus(fetchRes.status);
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(req, res) {
1203
- const {
1204
- redirect_to
1205
- } = req.query;
1206
- if (typeof redirect_to !== "string") {
1207
- res.redirect(this.getAppErrorUrl("Login failed: missing redirect_to param"));
1208
- return;
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
- res.cookie(VAL_STATE_COOKIE, createStateCookie({
1214
- redirect_to,
1215
- token
1216
- }), {
1217
- httpOnly: true,
1218
- sameSite: "lax",
1219
- expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
1220
- }).redirect(appAuthorizeUrl);
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(req, res) {
1223
- return enable(req, res, this.options.valEnableRedirectUrl);
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(req, res) {
1226
- return disable(req, res, this.options.valEnableRedirectUrl);
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(req, res) {
1419
+ async callback(query, cookies) {
1229
1420
  const {
1230
1421
  success: callbackReqSuccess,
1231
1422
  error: callbackReqError
1232
- } = verifyCallbackReq(req.cookies[VAL_STATE_COOKIE], req.query);
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
- res.redirect(this.getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`));
1237
- return;
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
- res.redirect(this.getAppErrorUrl("Failed to exchange code for user"));
1242
- return;
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
- res.cookie(VAL_SESSION_COOKIE, cookie, {
1250
- httpOnly: true,
1251
- sameSite: "strict",
1252
- secure: true,
1253
- expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
1254
- }).redirect(callbackReqSuccess.redirect_uri || "/");
1255
- }
1256
- async logout(_req, res) {
1257
- res.clearCookie(VAL_SESSION_COOKIE).clearCookie(VAL_STATE_COOKIE).sendStatus(200);
1258
- }
1259
- async withAuth(req, res, handler) {
1260
- const cookie = req.cookies[VAL_SESSION_COOKIE];
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 verification = IntegratedServerJwtPayload.safeParse(decodeJwt(cookie, this.options.valSecret));
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
- res.sendStatus(401);
1265
- return;
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: ${req.url}`, err);
1269
- res.sendStatus(500);
1270
- return undefined;
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
- res.sendStatus(401);
1518
+ return {
1519
+ status: 401,
1520
+ json: {
1521
+ message: "No token"
1522
+ }
1523
+ };
1274
1524
  }
1275
1525
  }
1276
- async session(req, res) {
1277
- return this.withAuth(req, res, async data => {
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.ok) {
1283
- res.status(fetchRes.status).json({
1284
- mode: "proxy",
1285
- ...(await fetchRes.json())
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
- res.sendStatus(fetchRes.status);
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(req, res) {
1293
- return this.withAuth(req, res, async data => {
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
- } = req.query;
1557
+ } = query;
1299
1558
  const commit = this.options.gitCommit;
1300
1559
  if (!commit) {
1301
- res.status(401).json({
1302
- error: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
1303
- });
1304
- return;
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}/${req.params["0"]}/?${params}`, this.options.valContentUrl);
1313
- const json = await fetch(url, {
1314
- headers: this.getAuthHeaders(data.token, "application/json")
1315
- }).then(res => res.json()).catch(err => {
1316
- console.error(err);
1317
- throw err;
1318
- });
1319
- res.send(json);
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(req, res) {
1323
- const patchIds = typeof req.params["id"] === "string" ? [req.params["id"]] : Array.isArray(req.params["id"]) ? req.params["id"] : [];
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
- await this.withAuth(req, res, async ({
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}/${req.params["0"]}${params}`, this.options.valContentUrl);
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.ok) {
1336
- const json = await fetchRes.json();
1337
- res.status(fetchRes.status).json(json);
1625
+ if (fetchRes.status === 200) {
1626
+ return {
1627
+ status: fetchRes.status,
1628
+ json: await fetchRes.json()
1629
+ };
1338
1630
  } else {
1339
- res.sendStatus(fetchRes.status);
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(req, res) {
1640
+ async postPatches(body, cookies) {
1351
1641
  const commit = this.options.gitCommit;
1352
1642
  if (!commit) {
1353
- res.status(401).json({
1354
- error: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
1355
- });
1356
- return;
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
- await this.withAuth(req, res, async ({
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(req.body);
1657
+ const patchJSON = z$1.record(PatchJSON).safeParse(body);
1366
1658
  if (!patchJSON.success) {
1367
- res.status(401).json(patchJSON.error.issues);
1368
- return;
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}/${req.params["0"]}/?${params}`, this.options.valContentUrl);
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.ok) {
1386
- res.status(fetchRes.status).json(await fetchRes.json());
1682
+ if (fetchRes.status === 200) {
1683
+ return {
1684
+ status: fetchRes.status,
1685
+ json: await fetchRes.json()
1686
+ };
1387
1687
  } else {
1388
- res.sendStatus(fetchRes.status);
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 commit(req, res) {
1400
- await this.withAuth(req, res, async ({
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.ok) {
1409
- res.status(fetchRes.status).json(await fetchRes.json());
1703
+ if (fetchRes.status === 200) {
1704
+ return {
1705
+ status: fetchRes.status,
1706
+ json: await fetchRes.json()
1707
+ };
1410
1708
  } else {
1411
- res.sendStatus(fetchRes.status);
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
- class LocalValServer {
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
- valServer = new ProxyValServer(serverOpts);
1884
+ return new ProxyValServer(serverOpts, callbacks);
1741
1885
  } else {
1742
- valServer = new LocalValServer(serverOpts);
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
- // TODO: rename to createValApiHandlers?
1853
- function createRequestListener(route, opts) {
1854
- const handler = _createRequestListener(route, opts);
1855
- return async (req, res) => {
1856
- try {
1857
- return (await handler)(req, res);
1858
- } catch (e) {
1859
- res.statusCode = 500;
1860
- res.write(e instanceof Error ? e.message : "Unknown error");
1861
- res.end();
1862
- return;
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, createRequestHandler, createRequestListener, createService, decodeJwt, encodeJwt, formatSyntaxErrorTree, getCompilerOptions, getExpire, patchSourceFile, safeReadGit };
2256
+ export { LocalValServer, PatchJSON, Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createFixPatch, createService, createValApiRouter, createValServer, decodeJwt, encodeJwt, formatSyntaxErrorTree, getCompilerOptions, getExpire, patchSourceFile, safeReadGit };