@valbuild/server 0.12.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.
Files changed (51) hide show
  1. package/.babelrc.json +5 -0
  2. package/CHANGELOG.md +0 -0
  3. package/dist/declarations/src/LocalValServer.d.ts +22 -0
  4. package/dist/declarations/src/ProxyValServer.d.ts +52 -0
  5. package/dist/declarations/src/SerializedModuleContent.d.ts +7 -0
  6. package/dist/declarations/src/Service.d.ts +24 -0
  7. package/dist/declarations/src/ValFS.d.ts +12 -0
  8. package/dist/declarations/src/ValFSHost.d.ts +23 -0
  9. package/dist/declarations/src/ValModuleLoader.d.ts +14 -0
  10. package/dist/declarations/src/ValQuickJSRuntime.d.ts +7 -0
  11. package/dist/declarations/src/ValServer.d.ts +14 -0
  12. package/dist/declarations/src/ValSourceFileHandler.d.ts +12 -0
  13. package/dist/declarations/src/createRequestHandler.d.ts +3 -0
  14. package/dist/declarations/src/expressHelpers.d.ts +4 -0
  15. package/dist/declarations/src/getCompilerOptions.d.ts +2 -0
  16. package/dist/declarations/src/hosting.d.ts +71 -0
  17. package/dist/declarations/src/index.d.ts +12 -0
  18. package/dist/declarations/src/jwt.d.ts +3 -0
  19. package/dist/declarations/src/patch/ts/ops.d.ts +27 -0
  20. package/dist/declarations/src/patch/ts/syntax.d.ts +49 -0
  21. package/dist/declarations/src/patch/ts/valModule.d.ts +8 -0
  22. package/dist/declarations/src/patch/validation.d.ts +4 -0
  23. package/dist/declarations/src/patchValFile.d.ts +9 -0
  24. package/dist/declarations/src/readValFile.d.ts +3 -0
  25. package/dist/valbuild-server.cjs.d.ts +2 -0
  26. package/dist/valbuild-server.cjs.d.ts.map +1 -0
  27. package/dist/valbuild-server.cjs.dev.js +1500 -0
  28. package/dist/valbuild-server.cjs.js +7 -0
  29. package/dist/valbuild-server.cjs.prod.js +1500 -0
  30. package/dist/valbuild-server.esm.js +1478 -0
  31. package/package.json +37 -0
  32. package/test/example-projects/basic-next-javascript/jsconfig.json +8 -0
  33. package/test/example-projects/basic-next-javascript/package.json +23 -0
  34. package/test/example-projects/basic-next-javascript/pages/blogs.val.js +20 -0
  35. package/test/example-projects/basic-next-javascript/val.config.js +4 -0
  36. package/test/example-projects/basic-next-src-typescript/package.json +23 -0
  37. package/test/example-projects/basic-next-src-typescript/src/pages/blogs.val.ts +20 -0
  38. package/test/example-projects/basic-next-src-typescript/src/val.config.ts +5 -0
  39. package/test/example-projects/basic-next-src-typescript/tsconfig.json +24 -0
  40. package/test/example-projects/basic-next-typescript/package.json +23 -0
  41. package/test/example-projects/basic-next-typescript/pages/blogs.val.ts +20 -0
  42. package/test/example-projects/basic-next-typescript/tsconfig.json +25 -0
  43. package/test/example-projects/basic-next-typescript/val.config.ts +5 -0
  44. package/test/example-projects/typescript-description-files/README.md +2 -0
  45. package/test/example-projects/typescript-description-files/jsconfig.json +8 -0
  46. package/test/example-projects/typescript-description-files/package.json +23 -0
  47. package/test/example-projects/typescript-description-files/pages/blogs.val.d.ts +7 -0
  48. package/test/example-projects/typescript-description-files/pages/blogs.val.js +19 -0
  49. package/test/example-projects/typescript-description-files/val.config.d.ts +3 -0
  50. package/test/example-projects/typescript-description-files/val.config.js +5 -0
  51. package/tsconfig.json +11 -0
@@ -0,0 +1,1478 @@
1
+ import { newQuickJSWASMModule } from 'quickjs-emscripten';
2
+ import ts from 'typescript';
3
+ import { result, pipe } from '@valbuild/core/fp';
4
+ import { FILE_REF_PROP, derefPatch, Internal, Schema } from '@valbuild/core';
5
+ import { deepEqual, isNotRoot, PatchError, parseAndValidateArrayIndex, applyPatch, parsePatch } from '@valbuild/core/patch';
6
+ import path from 'path';
7
+ import fs from 'fs';
8
+ import express, { Router } from 'express';
9
+ import { createRequestHandler as createRequestHandler$1 } from '@valbuild/ui/server';
10
+ import z, { z as z$1 } from 'zod';
11
+ import crypto from 'crypto';
12
+
13
+ class ValSyntaxError {
14
+ constructor(message, node) {
15
+ this.message = message;
16
+ this.node = node;
17
+ }
18
+ }
19
+ function forEachError(tree, callback) {
20
+ if (Array.isArray(tree)) {
21
+ for (const subtree of tree) {
22
+ forEachError(subtree, callback);
23
+ }
24
+ } else {
25
+ callback(tree);
26
+ }
27
+ }
28
+ function flatMapErrors(tree, cb) {
29
+ const result = [];
30
+ forEachError(tree, error => result.push(cb(error)));
31
+ return result;
32
+ }
33
+ function formatSyntaxError(error, sourceFile) {
34
+ const pos = sourceFile.getLineAndCharacterOfPosition(error.node.pos);
35
+ return `${pos.line}:${pos.character} ${error.message}`;
36
+ }
37
+ function formatSyntaxErrorTree(tree, sourceFile) {
38
+ return flatMapErrors(tree, error => formatSyntaxError(error, sourceFile));
39
+ }
40
+ function isLiteralPropertyName(name) {
41
+ return ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name);
42
+ }
43
+ function validateObjectProperties(nodes) {
44
+ const errors = [];
45
+ for (const node of nodes) {
46
+ if (!ts.isPropertyAssignment(node)) {
47
+ errors.push(new ValSyntaxError("Object literal element must be property assignment", node));
48
+ } else if (!isLiteralPropertyName(node.name)) {
49
+ errors.push(new ValSyntaxError("Object literal element key must be an identifier or a literal", node));
50
+ }
51
+ }
52
+ if (errors.length > 0) {
53
+ return result.err(errors);
54
+ }
55
+ return result.ok(nodes);
56
+ }
57
+
58
+ /**
59
+ * Validates that the expression is a JSON compatible type of expression without
60
+ * validating its children.
61
+ */
62
+ function shallowValidateExpression(value) {
63
+ return ts.isStringLiteralLike(value) || ts.isNumericLiteral(value) || value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword || ts.isArrayLiteralExpression(value) || ts.isObjectLiteralExpression(value) || isValFileMethodCall(value) ? undefined : new ValSyntaxError("Expression must be a literal or call val.file", value);
64
+ }
65
+
66
+ /**
67
+ * Evaluates the expression as a JSON value
68
+ */
69
+ function evaluateExpression(value) {
70
+ // The text property of a LiteralExpression stores the interpreted value of the literal in text form. For a StringLiteral,
71
+ // or any literal of a template, this means quotes have been removed and escapes have been converted to actual characters.
72
+ // For a NumericLiteral, the stored value is the toString() representation of the number. For example 1, 1.00, and 1e0 are all stored as just "1".
73
+ // https://github.com/microsoft/TypeScript/blob/4b794fe1dd0d184d3f8f17e94d8187eace57c91e/src/compiler/types.ts#L2127-L2131
74
+
75
+ if (ts.isStringLiteralLike(value)) {
76
+ return result.ok(value.text);
77
+ } else if (ts.isNumericLiteral(value)) {
78
+ return result.ok(Number(value.text));
79
+ } else if (value.kind === ts.SyntaxKind.TrueKeyword) {
80
+ return result.ok(true);
81
+ } else if (value.kind === ts.SyntaxKind.FalseKeyword) {
82
+ return result.ok(false);
83
+ } else if (value.kind === ts.SyntaxKind.NullKeyword) {
84
+ return result.ok(null);
85
+ } else if (ts.isArrayLiteralExpression(value)) {
86
+ return result.all(value.elements.map(evaluateExpression));
87
+ } else if (ts.isObjectLiteralExpression(value)) {
88
+ return pipe(validateObjectProperties(value.properties), result.flatMap(assignments => pipe(assignments.map(assignment => pipe(evaluateExpression(assignment.initializer), result.map(value => [assignment.name.text, value]))), result.all)), result.map(Object.fromEntries));
89
+ } else if (isValFileMethodCall(value)) {
90
+ return pipe(findValFileNodeArg(value), result.flatMap(ref => {
91
+ if (value.arguments.length === 2) {
92
+ return pipe(evaluateExpression(value.arguments[1]), result.map(metadata => ({
93
+ [FILE_REF_PROP]: ref.text,
94
+ _type: "file",
95
+ metadata
96
+ })));
97
+ } else {
98
+ return result.ok({
99
+ [FILE_REF_PROP]: ref.text,
100
+ _type: "file"
101
+ });
102
+ }
103
+ }));
104
+ } else {
105
+ return result.err(new ValSyntaxError("Expression must be a literal or call val.file", value));
106
+ }
107
+ }
108
+ function findObjectPropertyAssignment(value, key) {
109
+ return pipe(validateObjectProperties(value.properties), result.flatMap(assignments => {
110
+ const matchingAssignments = assignments.filter(assignment => assignment.name.text === key);
111
+ if (matchingAssignments.length === 0) return result.ok(undefined);
112
+ if (matchingAssignments.length > 1) {
113
+ return result.err(new ValSyntaxError(`Object key "${key}" is ambiguous`, value));
114
+ }
115
+ const [assignment] = matchingAssignments;
116
+ return result.ok(assignment);
117
+ }));
118
+ }
119
+ function isValFileMethodCall(node) {
120
+ return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "val" && node.expression.name.text === "file";
121
+ }
122
+ function findValFileNodeArg(node) {
123
+ if (node.arguments.length === 0) {
124
+ return result.err(new ValSyntaxError(`Invalid val.file() call: missing ref argument`, node));
125
+ } else if (node.arguments.length > 2) {
126
+ return result.err(new ValSyntaxError(`Invalid val.file() call: too many arguments ${node.arguments.length}}`, node));
127
+ } else if (!ts.isStringLiteral(node.arguments[0])) {
128
+ return result.err(new ValSyntaxError(`Invalid val.file() call: ref must be a string literal`, node));
129
+ }
130
+ const refNode = node.arguments[0];
131
+ return result.ok(refNode);
132
+ }
133
+ function findValFileMetadataArg(node) {
134
+ if (node.arguments.length === 0) {
135
+ return result.err(new ValSyntaxError(`Invalid val.file() call: missing ref argument`, node));
136
+ } else if (node.arguments.length > 2) {
137
+ return result.err(new ValSyntaxError(`Invalid val.file() call: too many arguments ${node.arguments.length}}`, node));
138
+ } else if (node.arguments.length === 2) {
139
+ if (!ts.isObjectLiteralExpression(node.arguments[1])) {
140
+ return result.err(new ValSyntaxError(`Invalid val.file() call: metadata must be a object literal`, node));
141
+ }
142
+ return result.ok(node.arguments[1]);
143
+ }
144
+ return result.ok(undefined);
145
+ }
146
+
147
+ /**
148
+ * Given a list of expressions, validates that all the expressions are not
149
+ * spread elements. In other words, it ensures that the expressions are the
150
+ * initializers of the values at their respective indices in the evaluated list.
151
+ */
152
+ function validateInitializers(nodes) {
153
+ for (const node of nodes) {
154
+ if (ts.isSpreadElement(node)) {
155
+ return new ValSyntaxError("Unexpected spread element", node);
156
+ }
157
+ }
158
+ return undefined;
159
+ }
160
+
161
+ function isPath(node, path) {
162
+ let currentNode = node;
163
+ for (let i = path.length - 1; i > 0; --i) {
164
+ const name = path[i];
165
+ if (!ts.isPropertyAccessExpression(currentNode)) {
166
+ return false;
167
+ }
168
+ if (!ts.isIdentifier(currentNode.name) || currentNode.name.text !== name) {
169
+ return false;
170
+ }
171
+ currentNode = currentNode.expression;
172
+ }
173
+ return ts.isIdentifier(currentNode) && currentNode.text === path[0];
174
+ }
175
+ function validateArguments(node, validators) {
176
+ return result.allV([node.arguments.length === validators.length ? result.voidOk : result.err(new ValSyntaxError(`Expected ${validators.length} arguments`, node)), ...node.arguments.slice(0, validators.length).map((argument, index) => validators[index](argument))]);
177
+ }
178
+ function analyzeDefaultExport(node) {
179
+ const valContentCall = node.expression;
180
+ if (!ts.isCallExpression(valContentCall)) {
181
+ return result.err(new ValSyntaxError("Expected default expression to be a call expression", valContentCall));
182
+ }
183
+ if (!isPath(valContentCall.expression, ["val", "content"])) {
184
+ return result.err(new ValSyntaxError("Expected default expression to be calling val.content", valContentCall.expression));
185
+ }
186
+ return pipe(validateArguments(valContentCall, [id => {
187
+ // TODO: validate ID value here?
188
+ if (!ts.isStringLiteralLike(id)) {
189
+ return result.err(new ValSyntaxError("Expected first argument to val.content to be a string literal", id));
190
+ }
191
+ return result.voidOk;
192
+ }, () => {
193
+ return result.voidOk;
194
+ }, () => {
195
+ return result.voidOk;
196
+ }]), result.map(() => {
197
+ const [, schema, source] = valContentCall.arguments;
198
+ return {
199
+ schema,
200
+ source
201
+ };
202
+ }));
203
+ }
204
+ function analyzeValModule(sourceFile) {
205
+ const analysis = sourceFile.forEachChild(node => {
206
+ if (ts.isExportAssignment(node)) {
207
+ return analyzeDefaultExport(node);
208
+ }
209
+ });
210
+ if (!analysis) {
211
+ throw Error("Failed to find fixed content node in val module");
212
+ }
213
+ return analysis;
214
+ }
215
+
216
+ function isValidIdentifier(text) {
217
+ if (text.length === 0) {
218
+ return false;
219
+ }
220
+ if (!ts.isIdentifierStart(text.charCodeAt(0), ts.ScriptTarget.ES2020)) {
221
+ return false;
222
+ }
223
+ for (let i = 1; i < text.length; ++i) {
224
+ if (!ts.isIdentifierPart(text.charCodeAt(i), ts.ScriptTarget.ES2020)) {
225
+ return false;
226
+ }
227
+ }
228
+ return true;
229
+ }
230
+ function createPropertyAssignment(key, value) {
231
+ return ts.factory.createPropertyAssignment(isValidIdentifier(key) ? ts.factory.createIdentifier(key) : ts.factory.createStringLiteral(key), toExpression(value));
232
+ }
233
+ function createValFileReference(ref) {
234
+ return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("val"), ts.factory.createIdentifier("file")), undefined, [ts.factory.createStringLiteral(ref)]);
235
+ }
236
+ function toExpression(value) {
237
+ if (typeof value === "string") {
238
+ // TODO: Use configuration/heuristics to determine use of single quote or double quote
239
+ return ts.factory.createStringLiteral(value);
240
+ } else if (typeof value === "number") {
241
+ return ts.factory.createNumericLiteral(value);
242
+ } else if (typeof value === "boolean") {
243
+ return value ? ts.factory.createTrue() : ts.factory.createFalse();
244
+ } else if (value === null) {
245
+ return ts.factory.createNull();
246
+ } else if (Array.isArray(value)) {
247
+ return ts.factory.createArrayLiteralExpression(value.map(toExpression));
248
+ } else if (typeof value === "object") {
249
+ if (isValFileValue(value)) {
250
+ return createValFileReference(value[FILE_REF_PROP]);
251
+ }
252
+ return ts.factory.createObjectLiteralExpression(Object.entries(value).map(([key, value]) => createPropertyAssignment(key, value)));
253
+ } else {
254
+ return ts.factory.createStringLiteral(value);
255
+ }
256
+ }
257
+
258
+ // TODO: Choose newline based on project settings/heuristics/system default?
259
+ const newLine = ts.NewLineKind.LineFeed;
260
+ // TODO: Handle indentation of printed code
261
+ const printer = ts.createPrinter({
262
+ newLine: newLine
263
+ // neverAsciiEscape: true,
264
+ });
265
+
266
+ function replaceNodeValue(document, node, value) {
267
+ const replacementText = printer.printNode(ts.EmitHint.Unspecified, toExpression(value), document);
268
+ const span = ts.createTextSpanFromBounds(node.getStart(document, false), node.end);
269
+ const newText = `${document.text.substring(0, span.start)}${replacementText}${document.text.substring(ts.textSpanEnd(span))}`;
270
+ return [document.update(newText, ts.createTextChangeRange(span, replacementText.length)), node];
271
+ }
272
+ function isIndentation(s) {
273
+ for (let i = 0; i < s.length; ++i) {
274
+ const c = s.charAt(i);
275
+ if (c !== " " && c !== "\t") {
276
+ return false;
277
+ }
278
+ }
279
+ return true;
280
+ }
281
+ function newLineStr(kind) {
282
+ if (kind === ts.NewLineKind.CarriageReturnLineFeed) {
283
+ return "\r\n";
284
+ } else {
285
+ return "\n";
286
+ }
287
+ }
288
+ function getSeparator(document, neighbor) {
289
+ const startPos = neighbor.getStart(document, true);
290
+ const basis = document.getLineAndCharacterOfPosition(startPos);
291
+ const lineStartPos = document.getPositionOfLineAndCharacter(basis.line, 0);
292
+ const maybeIndentation = document.getText().substring(lineStartPos, startPos);
293
+ if (isIndentation(maybeIndentation)) {
294
+ return `,${newLineStr(newLine)}${maybeIndentation}`;
295
+ } else {
296
+ return `, `;
297
+ }
298
+ }
299
+ function insertAt(document, nodes, index, node) {
300
+ let span;
301
+ let replacementText;
302
+ if (nodes.length === 0) {
303
+ // Replace entire range of nodes
304
+ replacementText = printer.printNode(ts.EmitHint.Unspecified, node, document);
305
+ span = ts.createTextSpanFromBounds(nodes.pos, nodes.end);
306
+ } else if (index === nodes.length) {
307
+ // Insert after last node
308
+ const neighbor = nodes[nodes.length - 1];
309
+ replacementText = `${getSeparator(document, neighbor)}${printer.printNode(ts.EmitHint.Unspecified, node, document)}`;
310
+ span = ts.createTextSpan(neighbor.end, 0);
311
+ } else {
312
+ // Insert before node
313
+ const neighbor = nodes[index];
314
+ replacementText = `${printer.printNode(ts.EmitHint.Unspecified, node, document)}${getSeparator(document, neighbor)}`;
315
+ span = ts.createTextSpan(neighbor.getStart(document, true), 0);
316
+ }
317
+ const newText = `${document.text.substring(0, span.start)}${replacementText}${document.text.substring(ts.textSpanEnd(span))}`;
318
+ return document.update(newText, ts.createTextChangeRange(span, replacementText.length));
319
+ }
320
+ function removeAt(document, nodes, index) {
321
+ const node = nodes[index];
322
+ let span;
323
+ if (nodes.length === 1) {
324
+ span = ts.createTextSpanFromBounds(nodes.pos, nodes.end);
325
+ } else if (index === nodes.length - 1) {
326
+ // Remove until previous node
327
+ const neighbor = nodes[index - 1];
328
+ span = ts.createTextSpanFromBounds(neighbor.end, node.end);
329
+ } else {
330
+ // Remove before next node
331
+ const neighbor = nodes[index + 1];
332
+ span = ts.createTextSpanFromBounds(node.getStart(document, true), neighbor.getStart(document, true));
333
+ }
334
+ const newText = `${document.text.substring(0, span.start)}${document.text.substring(ts.textSpanEnd(span))}`;
335
+ return [document.update(newText, ts.createTextChangeRange(span, 0)), node];
336
+ }
337
+ function parseAndValidateArrayInsertIndex(key, nodes) {
338
+ if (key === "-") {
339
+ // For insertion, all nodes up until the insertion index must be valid
340
+ // initializers
341
+ const err = validateInitializers(nodes);
342
+ if (err) {
343
+ return result.err(err);
344
+ }
345
+ return result.ok(nodes.length);
346
+ }
347
+ return pipe(parseAndValidateArrayIndex(key), result.flatMap(index => {
348
+ // For insertion, all nodes up until the insertion index must be valid
349
+ // initializers
350
+ const err = validateInitializers(nodes.slice(0, index));
351
+ if (err) {
352
+ return result.err(err);
353
+ }
354
+ if (index > nodes.length) {
355
+ return result.err(new PatchError("Array index out of bounds"));
356
+ } else {
357
+ return result.ok(index);
358
+ }
359
+ }));
360
+ }
361
+ function parseAndValidateArrayInboundsIndex(key, nodes) {
362
+ return pipe(parseAndValidateArrayIndex(key), result.flatMap(index => {
363
+ // For in-bounds operations, all nodes up until and including the index
364
+ // must be valid initializers
365
+ const err = validateInitializers(nodes.slice(0, index + 1));
366
+ if (err) {
367
+ return result.err(err);
368
+ }
369
+ if (index >= nodes.length) {
370
+ return result.err(new PatchError("Array index out of bounds"));
371
+ } else {
372
+ return result.ok(index);
373
+ }
374
+ }));
375
+ }
376
+ function replaceInNode(document, node, key, value) {
377
+ if (ts.isArrayLiteralExpression(node)) {
378
+ return pipe(parseAndValidateArrayInboundsIndex(key, node.elements), result.map(index => replaceNodeValue(document, node.elements[index], value)));
379
+ } else if (ts.isObjectLiteralExpression(node)) {
380
+ return pipe(findObjectPropertyAssignment(node, key), result.flatMap(assignment => {
381
+ if (!assignment) {
382
+ return result.err(new PatchError("Cannot replace object element which does not exist"));
383
+ }
384
+ return result.ok(assignment);
385
+ }), result.map(assignment => replaceNodeValue(document, assignment.initializer, value)));
386
+ } else if (isValFileMethodCall(node)) {
387
+ if (key === FILE_REF_PROP) {
388
+ if (typeof value !== "string") {
389
+ return result.err(new PatchError("Cannot replace val.file reference with non-string value"));
390
+ }
391
+ return pipe(findValFileNodeArg(node), result.map(refNode => replaceNodeValue(document, refNode, value)));
392
+ } else {
393
+ return pipe(findValFileMetadataArg(node), result.flatMap(metadataArgNode => {
394
+ if (!metadataArgNode) {
395
+ return result.err(new PatchError("Cannot replace in val.file metadata when it does not exist"));
396
+ }
397
+ if (key !== "metadata") {
398
+ return result.err(new PatchError(`Cannot replace val.file metadata key ${key} when it does not exist`));
399
+ }
400
+ return replaceInNode(document,
401
+ // TODO: creating a fake object here might not be right - seems to work though
402
+ ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
403
+ }));
404
+ }
405
+ } else {
406
+ return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot replace in non-object/array"));
407
+ }
408
+ }
409
+ function replaceAtPath(document, rootNode, path, value) {
410
+ if (isNotRoot(path)) {
411
+ return pipe(getPointerFromPath(rootNode, path), result.flatMap(([node, key]) => replaceInNode(document, node, key, value)));
412
+ } else {
413
+ return result.ok(replaceNodeValue(document, rootNode, value));
414
+ }
415
+ }
416
+ function getFromNode(node, key) {
417
+ if (ts.isArrayLiteralExpression(node)) {
418
+ return pipe(parseAndValidateArrayInboundsIndex(key, node.elements), result.map(index => node.elements[index]));
419
+ } else if (ts.isObjectLiteralExpression(node)) {
420
+ return pipe(findObjectPropertyAssignment(node, key), result.map(assignment => assignment === null || assignment === void 0 ? void 0 : assignment.initializer));
421
+ } else if (isValFileMethodCall(node)) {
422
+ if (key === FILE_REF_PROP) {
423
+ return findValFileNodeArg(node);
424
+ }
425
+ return findValFileMetadataArg(node);
426
+ } else {
427
+ return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot access non-object/array"));
428
+ }
429
+ }
430
+ function getPointerFromPath(node, path) {
431
+ let targetNode = node;
432
+ let key = path[0];
433
+ for (let i = 0; i < path.length - 1; ++i, key = path[i]) {
434
+ const childNode = getFromNode(targetNode, key);
435
+ if (result.isErr(childNode)) {
436
+ return childNode;
437
+ }
438
+ if (childNode.value === undefined) {
439
+ return result.err(new PatchError("Path refers to non-existing object/array"));
440
+ }
441
+ targetNode = childNode.value;
442
+ }
443
+ return result.ok([targetNode, key]);
444
+ }
445
+ function getAtPath(rootNode, path) {
446
+ return pipe(path, result.flatMapReduce((node, key) => pipe(getFromNode(node, key), result.filterOrElse(childNode => childNode !== undefined, () => new PatchError("Path refers to non-existing object/array"))), rootNode));
447
+ }
448
+ function removeFromNode(document, node, key) {
449
+ if (ts.isArrayLiteralExpression(node)) {
450
+ return pipe(parseAndValidateArrayInboundsIndex(key, node.elements), result.map(index => removeAt(document, node.elements, index)));
451
+ } else if (ts.isObjectLiteralExpression(node)) {
452
+ return pipe(findObjectPropertyAssignment(node, key), result.flatMap(assignment => {
453
+ if (!assignment) {
454
+ return result.err(new PatchError("Cannot replace object element which does not exist"));
455
+ }
456
+ return result.ok(assignment);
457
+ }), result.map(assignment => [removeAt(document, node.properties, node.properties.indexOf(assignment))[0], assignment.initializer]));
458
+ } else if (isValFileMethodCall(node)) {
459
+ if (key === FILE_REF_PROP) {
460
+ return result.err(new PatchError("Cannot remove a ref from val.file"));
461
+ } else {
462
+ return pipe(findValFileMetadataArg(node), result.flatMap(metadataArgNode => {
463
+ if (!metadataArgNode) {
464
+ return result.err(new PatchError("Cannot remove from val.file metadata when it does not exist"));
465
+ }
466
+ return removeFromNode(document, metadataArgNode, key);
467
+ }));
468
+ }
469
+ } else {
470
+ return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot remove from non-object/array"));
471
+ }
472
+ }
473
+ function removeAtPath(document, rootNode, path) {
474
+ return pipe(getPointerFromPath(rootNode, path), result.flatMap(([node, key]) => removeFromNode(document, node, key)));
475
+ }
476
+ function isValFileValue(value) {
477
+ return !!(typeof value === "object" && value && FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string");
478
+ }
479
+ function addToNode(document, node, key, value) {
480
+ if (ts.isArrayLiteralExpression(node)) {
481
+ return pipe(parseAndValidateArrayInsertIndex(key, node.elements), result.map(index => [insertAt(document, node.elements, index, toExpression(value))]));
482
+ } else if (ts.isObjectLiteralExpression(node)) {
483
+ if (key === FILE_REF_PROP) {
484
+ return result.err(new PatchError("Cannot add a key ref to object"));
485
+ }
486
+ return pipe(findObjectPropertyAssignment(node, key), result.map(assignment => {
487
+ if (!assignment) {
488
+ return [insertAt(document, node.properties, node.properties.length, createPropertyAssignment(key, value))];
489
+ } else {
490
+ return replaceNodeValue(document, assignment.initializer, value);
491
+ }
492
+ }));
493
+ } else if (isValFileMethodCall(node)) {
494
+ if (key === FILE_REF_PROP) {
495
+ if (typeof value !== "string") {
496
+ return result.err(new PatchError(`Cannot add ${FILE_REF_PROP} key to val.file with non-string value`));
497
+ }
498
+ return pipe(findValFileNodeArg(node), result.map(arg => replaceNodeValue(document, arg, value)));
499
+ } else {
500
+ return pipe(findValFileMetadataArg(node), result.flatMap(metadataArgNode => {
501
+ if (metadataArgNode) {
502
+ return result.err(new PatchError("Cannot add metadata to val.file when it already exists"));
503
+ }
504
+ if (key !== "metadata") {
505
+ return result.err(new PatchError(`Cannot add ${key} key to val.file: only metadata is allowed`));
506
+ }
507
+ return result.ok([insertAt(document, node.arguments, node.arguments.length, toExpression(value))]);
508
+ }));
509
+ }
510
+ } else {
511
+ return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot add to non-object/array"));
512
+ }
513
+ }
514
+ function addAtPath(document, rootNode, path, value) {
515
+ if (isNotRoot(path)) {
516
+ return pipe(getPointerFromPath(rootNode, path), result.flatMap(([node, key]) => addToNode(document, node, key, value)));
517
+ } else {
518
+ return result.ok(replaceNodeValue(document, rootNode, value));
519
+ }
520
+ }
521
+ function pickDocument([document]) {
522
+ return document;
523
+ }
524
+ class TSOps {
525
+ constructor(findRoot) {
526
+ this.findRoot = findRoot;
527
+ }
528
+ get(document, path) {
529
+ return pipe(document, this.findRoot, result.flatMap(rootNode => getAtPath(rootNode, path)), result.flatMap(evaluateExpression));
530
+ }
531
+ add(document, path, value) {
532
+ return pipe(document, this.findRoot, result.flatMap(rootNode => addAtPath(document, rootNode, path, value)), result.map(pickDocument));
533
+ }
534
+ remove(document, path) {
535
+ return pipe(document, this.findRoot, result.flatMap(rootNode => removeAtPath(document, rootNode, path)), result.map(pickDocument));
536
+ }
537
+ replace(document, path, value) {
538
+ return pipe(document, this.findRoot, result.flatMap(rootNode => replaceAtPath(document, rootNode, path, value)), result.map(pickDocument));
539
+ }
540
+ move(document, from, path) {
541
+ return pipe(document, this.findRoot, result.flatMap(rootNode => removeAtPath(document, rootNode, from)), result.flatMap(([document, removedNode]) => pipe(evaluateExpression(removedNode), result.map(removedValue => [document, removedValue]))), result.flatMap(([document, removedValue]) => pipe(document, this.findRoot, result.flatMap(root => addAtPath(document, root, path, removedValue)))), result.map(pickDocument));
542
+ }
543
+ copy(document, from, path) {
544
+ return pipe(document, this.findRoot, result.flatMap(rootNode => pipe(getAtPath(rootNode, from), result.flatMap(evaluateExpression), result.flatMap(value => addAtPath(document, rootNode, path, value)))), result.map(pickDocument));
545
+ }
546
+ test(document, path, value) {
547
+ return pipe(document, this.findRoot, result.flatMap(rootNode => getAtPath(rootNode, path)), result.flatMap(evaluateExpression), result.map(documentValue => deepEqual(value, documentValue)));
548
+ }
549
+ }
550
+
551
+ const readValFile = async (id, valConfigPath, runtime) => {
552
+ const context = runtime.newContext();
553
+ try {
554
+ const modulePath = `.${id}.val`;
555
+ const code = `import * as valModule from ${JSON.stringify(modulePath)};
556
+ import { Internal } from "@valbuild/core";
557
+ globalThis.valModule = {
558
+ id: valModule?.default && Internal.getValPath(valModule?.default),
559
+ schema: valModule?.default && Internal.getSchema(valModule?.default)?.serialize(),
560
+ source: valModule?.default && Internal.getRawSource(valModule?.default),
561
+ };
562
+ `;
563
+ const result = context.evalCode(code,
564
+ // Synthetic module name
565
+ path.join(path.dirname(valConfigPath), "<val>"));
566
+ if (result.error) {
567
+ const error = result.error.consume(context.dump);
568
+ console.error("Got error", error); // TODO: use this to figure out how to strip out QuickJS specific errors and get the actual stack
569
+
570
+ throw new Error(`Could not read val id: ${id}. Cause:\n${error.name}: ${error.message}${error.stack ? error.stack : ""}`);
571
+ } else {
572
+ result.value.dispose();
573
+ const valModule = context.getProp(context.global, "valModule").consume(context.dump);
574
+ const errors = [];
575
+ if (!valModule) {
576
+ errors.push(`Could not find any modules at: ${id}`);
577
+ } else {
578
+ if (valModule.id !== id) {
579
+ errors.push(`Expected val id: '${id}' but got: '${valModule.id}'`);
580
+ }
581
+ if (!(valModule !== null && valModule !== void 0 && valModule.schema)) {
582
+ errors.push(`Expected val id: '${id}' to have a schema`);
583
+ }
584
+ if (!(valModule !== null && valModule !== void 0 && valModule.source)) {
585
+ errors.push(`Expected val id: '${id}' to have a source`);
586
+ }
587
+ }
588
+ if (errors.length > 0) {
589
+ throw Error(`While processing module of id: ${id}, we got the following errors:\n${errors.join("\n")}`);
590
+ }
591
+ return {
592
+ path: valModule.id,
593
+ // This might not be the asked id/path, however, that should be handled further up in the call chain
594
+ source: valModule.source,
595
+ schema: valModule.schema
596
+ };
597
+ }
598
+ } finally {
599
+ context.dispose();
600
+ }
601
+ };
602
+
603
+ const ops = new TSOps(document => {
604
+ return pipe(analyzeValModule(document), result.map(({
605
+ source
606
+ }) => source));
607
+ });
608
+
609
+ // TODO: rename to patchValFiles since we may write multiple files
610
+ const patchValFile = async (id, valConfigPath, patch, sourceFileHandler, runtime) => {
611
+ const filePath = sourceFileHandler.resolveSourceModulePath(valConfigPath, `.${id}.val`);
612
+ const sourceFile = sourceFileHandler.getSourceFile(filePath);
613
+ if (!sourceFile) {
614
+ throw Error(`Source file ${filePath} not found`);
615
+ }
616
+ const derefRes = derefPatch(patch, sourceFile, ops);
617
+ if (result.isErr(derefRes)) {
618
+ throw derefRes.error;
619
+ }
620
+ const dereferencedPatch = derefRes.value.dereferencedPatch; // TODO: add ref changes to remote replace/add, ...
621
+ const newSourceFile = patchSourceFile(sourceFile, dereferencedPatch);
622
+ if (result.isErr(newSourceFile)) {
623
+ if (newSourceFile.error instanceof PatchError) {
624
+ throw newSourceFile.error;
625
+ } else {
626
+ throw new Error(`${filePath}\n${flatMapErrors(newSourceFile.error, error => formatSyntaxError(error, sourceFile)).join("\n")}`);
627
+ }
628
+ }
629
+ for (const [filePath, content] of Object.entries(derefRes.value.fileUpdates)) {
630
+ // Evaluate if we want to make these writes (more) atomic with a temp file and a move.
631
+ // This can potentially fill mid-way if there is not enough space on disk for example...
632
+ // However, that might be add add bit more complexity in our host and virtual file systems?
633
+ // Example:
634
+ // const tempFilePath = sourceFileHandler.writeTempFile(
635
+ // Buffer.from(content, "base64").toString("binary")
636
+ // );
637
+ // sourceFileHandler.moveFile(tempFilePath, "." + filePath);
638
+ sourceFileHandler.writeFile("." + filePath, convertDataUrlToBase64(content).toString("binary"), "binary");
639
+ }
640
+ for (const [ref, patch] of Object.entries(derefRes.value.remotePatches)) {
641
+ throw Error(`Cannot update remote ${ref} with ${JSON.stringify(patch)}: not implemented`);
642
+ }
643
+ sourceFileHandler.writeSourceFile(newSourceFile.value);
644
+ return readValFile(id, valConfigPath, runtime);
645
+ };
646
+ function convertDataUrlToBase64(dataUrl) {
647
+ const base64 = dataUrl.slice(dataUrl.indexOf(",") + 1);
648
+ return Buffer.from(base64, "base64");
649
+ }
650
+ const patchSourceFile = (sourceFile, patch) => {
651
+ return applyPatch(sourceFile, ops, patch);
652
+ };
653
+
654
+ const getCompilerOptions = (rootDir, parseConfigHost) => {
655
+ const tsConfigPath = path.resolve(rootDir, "tsconfig.json");
656
+ const jsConfigPath = path.resolve(rootDir, "jsconfig.json");
657
+ let configFilePath;
658
+ if (parseConfigHost.fileExists(jsConfigPath)) {
659
+ configFilePath = jsConfigPath;
660
+ } else if (parseConfigHost.fileExists(tsConfigPath)) {
661
+ configFilePath = tsConfigPath;
662
+ } else {
663
+ throw Error(`Could not read config from: "${tsConfigPath}" nor "${jsConfigPath}". Root dir: "${rootDir}"`);
664
+ }
665
+ const {
666
+ config,
667
+ error
668
+ } = ts.readConfigFile(configFilePath, parseConfigHost.readFile.bind(parseConfigHost));
669
+ if (error) {
670
+ if (typeof error.messageText === "string") {
671
+ throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText}`);
672
+ }
673
+ throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText.messageText}`);
674
+ }
675
+ const optionsOverrides = undefined;
676
+ const parsedConfigFile = ts.parseJsonConfigFileContent(config, parseConfigHost, rootDir, optionsOverrides, configFilePath);
677
+ if (parsedConfigFile.errors.length > 0) {
678
+ throw Error(`Could not parse config file: ${configFilePath}. Errors: ${parsedConfigFile.errors.map(e => e.messageText).join("\n")}`);
679
+ }
680
+ return parsedConfigFile.options;
681
+ };
682
+
683
+ class ValSourceFileHandler {
684
+ constructor(projectRoot, compilerOptions, host = {
685
+ ...ts.sys,
686
+ writeFile: fs.writeFileSync
687
+ }) {
688
+ this.projectRoot = projectRoot;
689
+ this.compilerOptions = compilerOptions;
690
+ this.host = host;
691
+ }
692
+ getSourceFile(filePath) {
693
+ const fileContent = this.host.readFile(filePath);
694
+ const scriptTarget = this.compilerOptions.target ?? ts.ScriptTarget.ES2020;
695
+ if (fileContent) {
696
+ return ts.createSourceFile(filePath, fileContent, scriptTarget);
697
+ }
698
+ }
699
+ writeSourceFile(sourceFile) {
700
+ return this.writeFile(sourceFile.fileName, sourceFile.text, "utf8");
701
+ }
702
+ writeFile(filePath, content, encoding) {
703
+ this.host.writeFile(filePath, content, encoding);
704
+ }
705
+ resolveSourceModulePath(containingFilePath, requestedModuleName) {
706
+ const resolutionRes = ts.resolveModuleName(requestedModuleName, path.isAbsolute(containingFilePath) ? containingFilePath : path.resolve(this.projectRoot, containingFilePath), this.compilerOptions, this.host, undefined, undefined, ts.ModuleKind.ESNext);
707
+ const resolvedModule = resolutionRes.resolvedModule;
708
+ if (!resolvedModule) {
709
+ throw Error(`Could not resolve module "${requestedModuleName}", base: "${containingFilePath}": No resolved modules returned: ${JSON.stringify(resolutionRes)}`);
710
+ }
711
+ return resolvedModule.resolvedFileName;
712
+ }
713
+ }
714
+
715
+ const JsFileLookupMapping = [
716
+ // NOTE: first one matching will be used
717
+ [".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js"]]];
718
+ class ValModuleLoader {
719
+ constructor(projectRoot, compilerOptions, sourceFileHandler, host = {
720
+ ...ts.sys,
721
+ writeFile: fs.writeFileSync
722
+ }) {
723
+ this.projectRoot = projectRoot;
724
+ this.compilerOptions = compilerOptions;
725
+ this.sourceFileHandler = sourceFileHandler;
726
+ this.host = host;
727
+ }
728
+ getModule(modulePath) {
729
+ if (!modulePath) {
730
+ throw Error(`Illegal module path: "${modulePath}"`);
731
+ }
732
+ const code = this.host.readFile(modulePath);
733
+ if (!code) {
734
+ throw Error(`Could not read file "${modulePath}"`);
735
+ }
736
+ return ts.transpile(code, {
737
+ ...this.compilerOptions,
738
+ // allowJs: true,
739
+ // rootDir: this.compilerOptions.rootDir,
740
+ module: ts.ModuleKind.ESNext,
741
+ target: ts.ScriptTarget.ES2020 // QuickJS supports a lot of ES2020: https://test262.report/, however not all cases are in that report (e.g. export const {} = {})
742
+ // moduleResolution: ts.ModuleResolutionKind.NodeNext,
743
+ // target: ts.ScriptTarget.ES2020, // QuickJs runs in ES2020 so we must use that
744
+ });
745
+ }
746
+
747
+ resolveModulePath(containingFilePath, requestedModuleName) {
748
+ var _this$host$realpath, _this$host;
749
+ const sourceFileName = this.sourceFileHandler.resolveSourceModulePath(containingFilePath, requestedModuleName);
750
+ const matches = this.findMatchingJsFile(sourceFileName);
751
+ if (matches.match === false) {
752
+ throw Error(`Could not find matching js file for module "${requestedModuleName}". Tried:\n${matches.tried.join("\n")}`);
753
+ }
754
+ const filePath = matches.match;
755
+ // resolve all symlinks (preconstruct for example symlinks the dist folder)
756
+ const followedPath = ((_this$host$realpath = (_this$host = this.host).realpath) === null || _this$host$realpath === void 0 ? void 0 : _this$host$realpath.call(_this$host, filePath)) ?? filePath;
757
+ if (!followedPath) {
758
+ throw Error(`File path was empty: "${filePath}", containing file: "${containingFilePath}", requested module: "${requestedModuleName}"`);
759
+ }
760
+ return followedPath;
761
+ }
762
+ findMatchingJsFile(filePath) {
763
+ let requiresReplacements = false;
764
+ for (const [currentEnding] of JsFileLookupMapping) {
765
+ if (filePath.endsWith(currentEnding)) {
766
+ requiresReplacements = true;
767
+ break;
768
+ }
769
+ }
770
+ // avoid unnecessary calls to fileExists if we don't need to replace anything
771
+ if (!requiresReplacements) {
772
+ if (this.host.fileExists(filePath)) {
773
+ return {
774
+ match: filePath
775
+ };
776
+ }
777
+ }
778
+ const tried = [];
779
+ for (const [currentEnding, replacements] of JsFileLookupMapping) {
780
+ if (filePath.endsWith(currentEnding)) {
781
+ for (const replacement of replacements) {
782
+ const newFilePath = filePath.slice(0, -currentEnding.length) + replacement;
783
+ if (this.host.fileExists(newFilePath)) {
784
+ return {
785
+ match: newFilePath
786
+ };
787
+ } else {
788
+ tried.push(newFilePath);
789
+ }
790
+ }
791
+ }
792
+ }
793
+ return {
794
+ match: false,
795
+ tried: tried.concat(filePath)
796
+ };
797
+ }
798
+ }
799
+
800
+ async function newValQuickJSRuntime(quickJSModule, moduleLoader, {
801
+ maxStackSize = 1024 * 640,
802
+ // TODO: these were randomly chosen, we should figure out what the right values are:
803
+ memoryLimit = 1024 * 640
804
+ } = {}) {
805
+ const runtime = quickJSModule.newRuntime();
806
+ runtime.setMaxStackSize(maxStackSize);
807
+ runtime.setMemoryLimit(memoryLimit);
808
+ runtime.setModuleLoader(modulePath => {
809
+ try {
810
+ return {
811
+ value: moduleLoader.getModule(modulePath)
812
+ };
813
+ } catch (e) {
814
+ return {
815
+ error: Error(`Could not resolve module: ${modulePath}'`)
816
+ };
817
+ }
818
+ }, (baseModuleName, requestedName) => {
819
+ try {
820
+ const modulePath = moduleLoader.resolveModulePath(baseModuleName, requestedName);
821
+ return {
822
+ value: modulePath
823
+ };
824
+ } catch (e) {
825
+ console.debug(`Could not resolve ${requestedName} in ${baseModuleName}`, e);
826
+ return {
827
+ value: requestedName
828
+ };
829
+ }
830
+ });
831
+ return runtime;
832
+ }
833
+
834
+ async function createService(projectRoot, opts, host = {
835
+ ...ts.sys,
836
+ writeFile: fs.writeFileSync
837
+ }) {
838
+ const compilerOptions = getCompilerOptions(projectRoot, host);
839
+ const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
840
+ const loader = new ValModuleLoader(projectRoot, compilerOptions, sourceFileHandler, host);
841
+ const module = await newQuickJSWASMModule();
842
+ const runtime = await newValQuickJSRuntime(module, loader);
843
+ return new Service(opts, sourceFileHandler, runtime);
844
+ }
845
+ class Service {
846
+ constructor({
847
+ valConfigPath
848
+ }, sourceFileHandler, runtime) {
849
+ this.sourceFileHandler = sourceFileHandler;
850
+ this.runtime = runtime;
851
+ this.valConfigPath = valConfigPath;
852
+ }
853
+ async get(moduleId, modulePath) {
854
+ const valModule = await readValFile(moduleId, this.valConfigPath, this.runtime);
855
+ const resolved = Internal.resolvePath(modulePath, valModule.source, valModule.schema);
856
+ return {
857
+ path: [moduleId, resolved.path].join("."),
858
+ schema: resolved.schema instanceof Schema ? resolved.schema.serialize() : resolved.schema,
859
+ source: resolved.source
860
+ };
861
+ }
862
+ async patch(moduleId, patch) {
863
+ return patchValFile(moduleId, this.valConfigPath, patch, this.sourceFileHandler, this.runtime);
864
+ }
865
+ dispose() {
866
+ this.runtime.dispose();
867
+ }
868
+ }
869
+
870
+ function createRequestHandler(valServer) {
871
+ const router = Router();
872
+ router.use("/static", createRequestHandler$1());
873
+ router.get("/session", valServer.session.bind(valServer));
874
+ router.get("/authorize", valServer.authorize.bind(valServer));
875
+ router.get("/callback", valServer.callback.bind(valServer));
876
+ router.get("/logout", valServer.logout.bind(valServer));
877
+ router.get("/ids/*", valServer.getIds.bind(valServer));
878
+ router.patch("/ids/*", express.json({
879
+ type: "application/json-patch+json",
880
+ limit: "10mb"
881
+ }), valServer.patchIds.bind(valServer));
882
+ router.post("/commit", valServer.commit.bind(valServer));
883
+ return router;
884
+ }
885
+
886
+ const JSONValueT = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(JSONValueT), z.record(JSONValueT)]));
887
+
888
+ /**
889
+ * Raw JSON patch operation.
890
+ */
891
+ const OperationJSONT = z.discriminatedUnion("op", [z.object({
892
+ op: z.literal("add"),
893
+ path: z.string(),
894
+ value: JSONValueT
895
+ }).strict(), z.object({
896
+ op: z.literal("remove"),
897
+ /**
898
+ * Must be non-root
899
+ */
900
+ path: z.string()
901
+ }).strict(), z.object({
902
+ op: z.literal("replace"),
903
+ path: z.string(),
904
+ value: JSONValueT
905
+ }).strict(), z.object({
906
+ op: z.literal("move"),
907
+ /**
908
+ * Must be non-root and not a proper prefix of "path".
909
+ */
910
+ from: z.string(),
911
+ path: z.string()
912
+ }).strict(), z.object({
913
+ op: z.literal("copy"),
914
+ from: z.string(),
915
+ path: z.string()
916
+ }).strict(), z.object({
917
+ op: z.literal("test"),
918
+ path: z.string(),
919
+ value: JSONValueT
920
+ }).strict()]);
921
+ const PatchJSON = z.array(OperationJSONT);
922
+
923
+ function getPathFromParams(params) {
924
+ return `/${params[0]}`;
925
+ }
926
+
927
+ class LocalValServer {
928
+ constructor(options) {
929
+ this.options = options;
930
+ }
931
+ async session(_req, res) {
932
+ res.json({
933
+ mode: "local"
934
+ });
935
+ }
936
+ async getIds(req, res) {
937
+ try {
938
+ console.log(req.params);
939
+ const path = getPathFromParams(req.params);
940
+ const [moduleId, modulePath] = Internal.splitModuleIdAndModulePath(path);
941
+ const valModule = await this.options.service.get(moduleId, modulePath);
942
+ res.json(valModule);
943
+ } catch (err) {
944
+ console.error(err);
945
+ res.sendStatus(500);
946
+ }
947
+ }
948
+ async patchIds(req, res) {
949
+ // First validate that the body has the right structure
950
+ const patchJSON = PatchJSON.safeParse(req.body);
951
+ if (!patchJSON.success) {
952
+ res.status(401).json(patchJSON.error.issues);
953
+ return;
954
+ }
955
+ // Then parse/validate
956
+ const patch = parsePatch(patchJSON.data);
957
+ if (result.isErr(patch)) {
958
+ res.status(401).json(patch.error);
959
+ return;
960
+ }
961
+ const id = getPathFromParams(req.params);
962
+ try {
963
+ const valModule = await this.options.service.patch(id, patch.value);
964
+ res.json(valModule);
965
+ } catch (err) {
966
+ if (err instanceof PatchError) {
967
+ res.status(401).send(err.message);
968
+ } else {
969
+ console.error(err);
970
+ res.status(500).send(err instanceof Error ? err.message : "Unknown error");
971
+ }
972
+ }
973
+ }
974
+ async badRequest(req, res) {
975
+ console.debug("Local server does handle this request", req.url);
976
+ res.sendStatus(400);
977
+ }
978
+ commit(req, res) {
979
+ return this.badRequest(req, res);
980
+ }
981
+ authorize(req, res) {
982
+ return this.badRequest(req, res);
983
+ }
984
+ callback(req, res) {
985
+ return this.badRequest(req, res);
986
+ }
987
+ logout(req, res) {
988
+ return this.badRequest(req, res);
989
+ }
990
+ }
991
+
992
+ function decodeJwt(token, secretKey) {
993
+ const [headerBase64, payloadBase64, signatureBase64, ...rest] = token.split(".");
994
+ if (!headerBase64 || !payloadBase64 || !signatureBase64 || rest.length > 0) {
995
+ console.debug("Invalid JWT: format is not exactly {header}.{payload}.{signature}", token);
996
+ return null;
997
+ }
998
+ try {
999
+ const parsedHeader = JSON.parse(Buffer.from(headerBase64, "base64").toString("utf8"));
1000
+ const headerVerification = JwtHeaderSchema.safeParse(parsedHeader);
1001
+ if (!headerVerification.success) {
1002
+ console.debug("Invalid JWT: invalid header", parsedHeader);
1003
+ return null;
1004
+ }
1005
+ if (headerVerification.data.typ !== jwtHeader.typ) {
1006
+ console.debug("Invalid JWT: invalid header typ", parsedHeader);
1007
+ return null;
1008
+ }
1009
+ if (headerVerification.data.alg !== jwtHeader.alg) {
1010
+ console.debug("Invalid JWT: invalid header alg", parsedHeader);
1011
+ return null;
1012
+ }
1013
+ } catch (err) {
1014
+ console.debug("Invalid JWT: could not parse header", err);
1015
+ return null;
1016
+ }
1017
+ if (secretKey) {
1018
+ const signature = crypto.createHmac("sha256", secretKey).update(`${headerBase64}.${payloadBase64}`).digest("base64");
1019
+ if (signature !== signatureBase64) {
1020
+ console.debug("Invalid JWT: invalid signature");
1021
+ return null;
1022
+ }
1023
+ }
1024
+ try {
1025
+ const parsedPayload = JSON.parse(Buffer.from(payloadBase64, "base64").toString("utf8"));
1026
+ return parsedPayload;
1027
+ } catch (err) {
1028
+ console.debug("Invalid JWT: could not parse payload", err);
1029
+ return null;
1030
+ }
1031
+ }
1032
+ function getExpire() {
1033
+ return Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 4; // 4 days
1034
+ }
1035
+
1036
+ const JwtHeaderSchema = z$1.object({
1037
+ alg: z$1.literal("HS256"),
1038
+ typ: z$1.literal("JWT")
1039
+ });
1040
+ const jwtHeader = {
1041
+ alg: "HS256",
1042
+ typ: "JWT"
1043
+ };
1044
+ const jwtHeaderBase64 = Buffer.from(JSON.stringify(jwtHeader)).toString("base64");
1045
+ function encodeJwt(payload, sessionKey) {
1046
+ // NOTE: this is only used for authentication, not for authorization (i.e. what a user can do) - this is handled when actually doing operations
1047
+ const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString("base64");
1048
+ return `${jwtHeaderBase64}.${payloadBase64}.${crypto.createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
1049
+ }
1050
+
1051
+ const VAL_SESSION_COOKIE = "val_session";
1052
+ const VAL_STATE_COOKIE = "val_state";
1053
+ class ProxyValServer {
1054
+ constructor(options) {
1055
+ this.options = options;
1056
+ }
1057
+ async authorize(req, res) {
1058
+ const {
1059
+ redirect_to
1060
+ } = req.query;
1061
+ if (typeof redirect_to !== "string") {
1062
+ res.redirect(this.getAppErrorUrl("Login failed: missing redirect_to param"));
1063
+ return;
1064
+ }
1065
+ const token = crypto.randomUUID();
1066
+ const redirectUrl = new URL(redirect_to);
1067
+ const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
1068
+ res.cookie(VAL_STATE_COOKIE, createStateCookie({
1069
+ redirect_to,
1070
+ token
1071
+ }), {
1072
+ httpOnly: true,
1073
+ sameSite: "lax",
1074
+ expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
1075
+ }).redirect(appAuthorizeUrl);
1076
+ }
1077
+ async callback(req, res) {
1078
+ const {
1079
+ success: callbackReqSuccess,
1080
+ error: callbackReqError
1081
+ } = verifyCallbackReq(req.cookies[VAL_STATE_COOKIE], req.query);
1082
+ res.clearCookie(VAL_STATE_COOKIE); // we don't need this anymore
1083
+
1084
+ if (callbackReqError !== null) {
1085
+ res.redirect(this.getAppErrorUrl(`Authorization callback failed. Details: ${callbackReqError}`));
1086
+ return;
1087
+ }
1088
+ const data = await this.consumeCode(callbackReqSuccess.code);
1089
+ if (data === null) {
1090
+ res.redirect(this.getAppErrorUrl("Failed to exchange code for user"));
1091
+ return;
1092
+ }
1093
+ const exp = getExpire();
1094
+ const cookie = encodeJwt({
1095
+ ...data,
1096
+ exp // this is the client side exp
1097
+ }, this.options.valSecret);
1098
+ res.cookie(VAL_SESSION_COOKIE, cookie, {
1099
+ httpOnly: true,
1100
+ sameSite: "strict",
1101
+ secure: true,
1102
+ expires: new Date(exp * 1000) // NOTE: this is not used for authorization, only for authentication
1103
+ }).redirect(callbackReqSuccess.redirect_uri || "/");
1104
+ }
1105
+ async logout(_req, res) {
1106
+ res.clearCookie(VAL_SESSION_COOKIE).clearCookie(VAL_STATE_COOKIE).sendStatus(200);
1107
+ }
1108
+ async withAuth(req, res, handler) {
1109
+ const cookie = req.cookies[VAL_SESSION_COOKIE];
1110
+ if (typeof cookie === "string") {
1111
+ const verification = IntegratedServerJwtPayload.safeParse(decodeJwt(cookie, this.options.valSecret));
1112
+ if (!verification.success) {
1113
+ res.sendStatus(401);
1114
+ return;
1115
+ }
1116
+ return handler(verification.data);
1117
+ } else {
1118
+ res.sendStatus(401);
1119
+ }
1120
+ }
1121
+ async session(req, res) {
1122
+ return this.withAuth(req, res, async data => {
1123
+ const url = new URL("/api/val/auth/user/session", this.options.valBuildUrl);
1124
+ const fetchRes = await fetch(url, {
1125
+ headers: this.getAuthHeaders(data.token, "application/json")
1126
+ });
1127
+ if (fetchRes.ok) {
1128
+ res.status(fetchRes.status).json({
1129
+ mode: "proxy",
1130
+ ...(await fetchRes.json())
1131
+ });
1132
+ } else {
1133
+ res.sendStatus(fetchRes.status);
1134
+ }
1135
+ });
1136
+ }
1137
+ async getIds(req, res) {
1138
+ return this.withAuth(req, res, async ({
1139
+ token
1140
+ }) => {
1141
+ const id = getPathFromParams(req.params);
1142
+ const url = new URL(`/api/val/modules/${encodeURIComponent(this.options.gitCommit)}${id}`, this.options.valBuildUrl);
1143
+ const fetchRes = await fetch(url, {
1144
+ headers: this.getAuthHeaders(token)
1145
+ });
1146
+ if (fetchRes.ok) {
1147
+ res.status(fetchRes.status).json(await fetchRes.json());
1148
+ } else {
1149
+ res.sendStatus(fetchRes.status);
1150
+ }
1151
+ }).catch(e => {
1152
+ res.status(500).send({
1153
+ error: {
1154
+ message: e === null || e === void 0 ? void 0 : e.message,
1155
+ status: 500
1156
+ }
1157
+ });
1158
+ });
1159
+ }
1160
+ async patchIds(req, res) {
1161
+ this.withAuth(req, res, async ({
1162
+ token
1163
+ }) => {
1164
+ // First validate that the body has the right structure
1165
+ const patchJSON = PatchJSON.safeParse(req.body);
1166
+ if (!patchJSON.success) {
1167
+ res.status(401).json(patchJSON.error.issues);
1168
+ return;
1169
+ }
1170
+ // Then parse/validate
1171
+ const patch = parsePatch(patchJSON.data);
1172
+ if (result.isErr(patch)) {
1173
+ res.status(401).json(patch.error);
1174
+ return;
1175
+ }
1176
+ const id = getPathFromParams(req.params);
1177
+ const url = new URL(`/api/val/modules/${encodeURIComponent(this.options.gitCommit)}${id}`, this.options.valBuildUrl);
1178
+ // Proxy patch to val.build
1179
+ const fetchRes = await fetch(url, {
1180
+ method: "PATCH",
1181
+ headers: this.getAuthHeaders(token, "application/json-patch+json"),
1182
+ body: JSON.stringify(patch)
1183
+ });
1184
+ if (fetchRes.ok) {
1185
+ res.status(fetchRes.status).json(await fetchRes.json());
1186
+ } else {
1187
+ res.sendStatus(fetchRes.status);
1188
+ }
1189
+ }).catch(e => {
1190
+ res.status(500).send({
1191
+ error: {
1192
+ message: e === null || e === void 0 ? void 0 : e.message,
1193
+ status: 500
1194
+ }
1195
+ });
1196
+ });
1197
+ }
1198
+ async commit(req, res) {
1199
+ this.withAuth(req, res, async ({
1200
+ token
1201
+ }) => {
1202
+ const url = new URL(`/api/val/commit/${encodeURIComponent(this.options.gitBranch)}`, this.options.valBuildUrl);
1203
+ const fetchRes = await fetch(url, {
1204
+ method: "POST",
1205
+ headers: this.getAuthHeaders(token)
1206
+ });
1207
+ if (fetchRes.ok) {
1208
+ res.status(fetchRes.status).json(await fetchRes.json());
1209
+ } else {
1210
+ res.sendStatus(fetchRes.status);
1211
+ }
1212
+ });
1213
+ }
1214
+ getAuthHeaders(token, type) {
1215
+ if (!type) {
1216
+ return {
1217
+ Authorization: `Bearer ${token}`
1218
+ };
1219
+ }
1220
+ return {
1221
+ "Content-Type": type,
1222
+ Authorization: `Bearer ${token}`
1223
+ };
1224
+ }
1225
+ async consumeCode(code) {
1226
+ const url = new URL("/api/val/auth/user/token", this.options.valBuildUrl);
1227
+ url.searchParams.set("code", encodeURIComponent(code));
1228
+ return fetch(url, {
1229
+ method: "POST",
1230
+ headers: this.getAuthHeaders(this.options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
1231
+ }).then(async res => {
1232
+ if (res.status === 200) {
1233
+ const token = await res.text();
1234
+ const verification = ValAppJwtPayload.safeParse(decodeJwt(token));
1235
+ if (!verification.success) {
1236
+ return null;
1237
+ }
1238
+ return {
1239
+ ...verification.data,
1240
+ token
1241
+ };
1242
+ } else {
1243
+ console.debug("Failed to get data from code: ", res.status);
1244
+ return null;
1245
+ }
1246
+ }).catch(err => {
1247
+ console.debug("Failed to get user from code: ", err);
1248
+ return null;
1249
+ });
1250
+ }
1251
+ getAuthorizeUrl(publicValApiRoute, token) {
1252
+ const url = new URL("/authorize", this.options.valBuildUrl);
1253
+ url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRoute}/callback`));
1254
+ url.searchParams.set("state", token);
1255
+ return url.toString();
1256
+ }
1257
+ getAppErrorUrl(error) {
1258
+ const url = new URL("/authorize", this.options.valBuildUrl);
1259
+ url.searchParams.set("error", encodeURIComponent(error));
1260
+ return url.toString();
1261
+ }
1262
+ }
1263
+ function verifyCallbackReq(stateCookie, queryParams) {
1264
+ if (typeof stateCookie !== "string") {
1265
+ return {
1266
+ success: false,
1267
+ error: "No state cookie"
1268
+ };
1269
+ }
1270
+ const {
1271
+ code,
1272
+ state: tokenFromQuery
1273
+ } = queryParams;
1274
+ if (typeof code !== "string") {
1275
+ return {
1276
+ success: false,
1277
+ error: "No code query param"
1278
+ };
1279
+ }
1280
+ if (typeof tokenFromQuery !== "string") {
1281
+ return {
1282
+ success: false,
1283
+ error: "No state query param"
1284
+ };
1285
+ }
1286
+ const {
1287
+ success: cookieStateSuccess,
1288
+ error: cookieStateError
1289
+ } = getStateFromCookie(stateCookie);
1290
+ if (cookieStateError !== null) {
1291
+ return {
1292
+ success: false,
1293
+ error: cookieStateError
1294
+ };
1295
+ }
1296
+ if (cookieStateSuccess.token !== tokenFromQuery) {
1297
+ return {
1298
+ success: false,
1299
+ error: "Invalid state token"
1300
+ };
1301
+ }
1302
+ return {
1303
+ success: {
1304
+ code,
1305
+ redirect_uri: cookieStateSuccess.redirect_to
1306
+ },
1307
+ error: null
1308
+ };
1309
+ }
1310
+ function getStateFromCookie(stateCookie) {
1311
+ try {
1312
+ const decoded = Buffer.from(stateCookie, "base64").toString("utf8");
1313
+ const parsed = JSON.parse(decoded);
1314
+ if (!parsed) {
1315
+ return {
1316
+ success: false,
1317
+ error: "Invalid state cookie: could not parse"
1318
+ };
1319
+ }
1320
+ if (typeof parsed !== "object") {
1321
+ return {
1322
+ success: false,
1323
+ error: "Invalid state cookie: parsed object is not an object"
1324
+ };
1325
+ }
1326
+ if ("token" in parsed && "redirect_to" in parsed) {
1327
+ const {
1328
+ token,
1329
+ redirect_to
1330
+ } = parsed;
1331
+ if (typeof token !== "string") {
1332
+ return {
1333
+ success: false,
1334
+ error: "Invalid state cookie: no token in parsed object"
1335
+ };
1336
+ }
1337
+ if (typeof redirect_to !== "string") {
1338
+ return {
1339
+ success: false,
1340
+ error: "Invalid state cookie: no redirect_to in parsed object"
1341
+ };
1342
+ }
1343
+ return {
1344
+ success: {
1345
+ token,
1346
+ redirect_to
1347
+ },
1348
+ error: null
1349
+ };
1350
+ } else {
1351
+ return {
1352
+ success: false,
1353
+ error: "Invalid state cookie: no token or redirect_to in parsed object"
1354
+ };
1355
+ }
1356
+ } catch (err) {
1357
+ return {
1358
+ success: false,
1359
+ error: "Invalid state cookie: could not parse"
1360
+ };
1361
+ }
1362
+ }
1363
+ function createStateCookie(state) {
1364
+ return Buffer.from(JSON.stringify(state), "utf8").toString("base64");
1365
+ }
1366
+ const ValAppJwtPayload = z$1.object({
1367
+ sub: z$1.string(),
1368
+ exp: z$1.number(),
1369
+ project: z$1.string(),
1370
+ org: z$1.string()
1371
+ });
1372
+ const IntegratedServerJwtPayload = z$1.object({
1373
+ sub: z$1.string(),
1374
+ exp: z$1.number(),
1375
+ token: z$1.string(),
1376
+ org: z$1.string(),
1377
+ project: z$1.string()
1378
+ });
1379
+
1380
+ async function _createRequestListener(route, opts) {
1381
+ const serverOpts = await initHandlerOptions(route, opts);
1382
+ let valServer;
1383
+ if (serverOpts.mode === "proxy") {
1384
+ valServer = new ProxyValServer(serverOpts);
1385
+ } else {
1386
+ valServer = new LocalValServer(serverOpts);
1387
+ }
1388
+ const reqHandler = createRequestHandler(valServer);
1389
+ return express().use(route, reqHandler);
1390
+ }
1391
+ async function initHandlerOptions(route, opts) {
1392
+ const maybeApiKey = opts.apiKey || process.env.VAL_API_KEY;
1393
+ const maybeValSecret = opts.valSecret || process.env.VAL_SECRET;
1394
+ const isProxyMode = opts.mode === "proxy" || opts.mode === undefined && (maybeApiKey || maybeValSecret);
1395
+ if (isProxyMode) {
1396
+ const valBuildUrl = opts.valBuildUrl || process.env.VAL_BUILD_URL || "https://app.val.build";
1397
+ if (!maybeApiKey || !maybeValSecret) {
1398
+ throw new Error("VAL_API_KEY and VAL_SECRET env vars must both be set in proxy mode");
1399
+ }
1400
+ const maybeGitCommit = opts.gitCommit || process.env.VAL_GIT_COMMIT;
1401
+ if (!maybeGitCommit) {
1402
+ throw new Error("VAL_GIT_COMMIT env var must be set in proxy mode");
1403
+ }
1404
+ const maybeGitBranch = opts.gitBranch || process.env.VAL_GIT_BRANCH;
1405
+ if (!maybeGitBranch) {
1406
+ throw new Error("VAL_GIT_BRANCH env var must be set in proxy mode");
1407
+ }
1408
+ return {
1409
+ mode: "proxy",
1410
+ route,
1411
+ apiKey: maybeApiKey,
1412
+ valSecret: maybeValSecret,
1413
+ valBuildUrl,
1414
+ gitCommit: maybeGitCommit,
1415
+ gitBranch: maybeGitBranch
1416
+ };
1417
+ } else {
1418
+ const service = await createService(process.cwd(), opts);
1419
+ return {
1420
+ mode: "local",
1421
+ service
1422
+ };
1423
+ }
1424
+ }
1425
+
1426
+ // TODO: rename to createValApiHandlers?
1427
+ function createRequestListener(route, opts) {
1428
+ const handler = _createRequestListener(route, opts);
1429
+ return async (req, res) => {
1430
+ try {
1431
+ return (await handler)(req, res);
1432
+ } catch (e) {
1433
+ res.statusCode = 500;
1434
+ res.write(e instanceof Error ? e.message : "Unknown error");
1435
+ res.end();
1436
+ return;
1437
+ }
1438
+ };
1439
+ }
1440
+
1441
+ /**
1442
+ * An implementation of methods in the various ts.*Host interfaces
1443
+ * that uses ValFS to resolve modules and read/write files.
1444
+ */
1445
+
1446
+ class ValFSHost {
1447
+ constructor(valFS, currentDirectory) {
1448
+ this.valFS = valFS;
1449
+ this.currentDirectory = currentDirectory;
1450
+ }
1451
+ useCaseSensitiveFileNames = true;
1452
+ readDirectory(rootDir, extensions, excludes, includes, depth) {
1453
+ return this.valFS.readDirectory(rootDir, extensions, excludes, includes, depth);
1454
+ }
1455
+ writeFile(fileName, text, encoding) {
1456
+ this.valFS.writeFile(fileName, text, encoding);
1457
+ }
1458
+ getCurrentDirectory() {
1459
+ return this.currentDirectory;
1460
+ }
1461
+ getCanonicalFileName(fileName) {
1462
+ if (path.isAbsolute(fileName)) {
1463
+ return path.normalize(fileName);
1464
+ }
1465
+ return path.resolve(this.getCurrentDirectory(), fileName);
1466
+ }
1467
+ fileExists(fileName) {
1468
+ return this.valFS.fileExists(fileName);
1469
+ }
1470
+ readFile(fileName) {
1471
+ return this.valFS.readFile(fileName);
1472
+ }
1473
+ realpath(path) {
1474
+ return this.valFS.realpath(path);
1475
+ }
1476
+ }
1477
+
1478
+ export { Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createRequestHandler, createRequestListener, createService, formatSyntaxErrorTree, getCompilerOptions, patchSourceFile };