@valbuild/server 0.58.0 → 0.59.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.
@@ -1,17 +1,19 @@
1
1
  import { newQuickJSWASMModule } from 'quickjs-emscripten';
2
- import ts from 'typescript';
2
+ import * as ts from 'typescript';
3
+ import ts__default from 'typescript';
3
4
  import { result, pipe } from '@valbuild/core/fp';
4
- import { FILE_REF_PROP, FILE_REF_SUBTYPE_TAG, RT_IMAGE_TAG, VAL_EXTENSION, derefPatch, Internal, Schema } from '@valbuild/core';
5
- import { deepEqual, isNotRoot, PatchError, parseAndValidateArrayIndex, applyPatch, parsePatch, sourceToPatchPath } from '@valbuild/core/patch';
5
+ import { FILE_REF_PROP, FILE_REF_SUBTYPE_TAG, RT_IMAGE_TAG, VAL_EXTENSION, derefPatch, Internal, Schema, deserializeSchema } from '@valbuild/core';
6
+ import { deepEqual, isNotRoot, PatchError, parseAndValidateArrayIndex, applyPatch, JSONOps, sourceToPatchPath } from '@valbuild/core/patch';
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
11
  import z, { z as z$1 } from 'zod';
11
- import { VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE as VAL_STATE_COOKIE$1, VAL_SESSION_COOKIE as VAL_SESSION_COOKIE$1, MIME_TYPES_TO_EXT, filenameToMimeType } from '@valbuild/shared/internal';
12
+ import { MIME_TYPES_TO_EXT, filenameToMimeType, 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 sizeOf from 'image-size';
12
14
  import crypto from 'crypto';
15
+ import minimatch from 'minimatch';
13
16
  import { createUIRequestHandler } from '@valbuild/ui/server';
14
- import sizeOf from 'image-size';
15
17
 
16
18
  class ValSyntaxError {
17
19
  constructor(message, node) {
@@ -41,12 +43,12 @@ function formatSyntaxErrorTree(tree, sourceFile) {
41
43
  return flatMapErrors(tree, error => formatSyntaxError(error, sourceFile));
42
44
  }
43
45
  function isLiteralPropertyName(name) {
44
- return ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name);
46
+ return ts__default.isIdentifier(name) || ts__default.isStringLiteral(name) || ts__default.isNumericLiteral(name);
45
47
  }
46
48
  function validateObjectProperties(nodes) {
47
49
  const errors = [];
48
50
  for (const node of nodes) {
49
- if (!ts.isPropertyAssignment(node)) {
51
+ if (!ts__default.isPropertyAssignment(node)) {
50
52
  errors.push(new ValSyntaxError("Object literal element must be property assignment", node));
51
53
  } else if (!isLiteralPropertyName(node.name)) {
52
54
  errors.push(new ValSyntaxError("Object literal element key must be an identifier or a literal", node));
@@ -63,7 +65,7 @@ function validateObjectProperties(nodes) {
63
65
  * validating its children.
64
66
  */
65
67
  function shallowValidateExpression(value) {
66
- 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 c.file", value);
68
+ return ts__default.isStringLiteralLike(value) || ts__default.isNumericLiteral(value) || value.kind === ts__default.SyntaxKind.TrueKeyword || value.kind === ts__default.SyntaxKind.FalseKeyword || value.kind === ts__default.SyntaxKind.NullKeyword || ts__default.isArrayLiteralExpression(value) || ts__default.isObjectLiteralExpression(value) || isValFileMethodCall(value) ? undefined : new ValSyntaxError("Expression must be a literal or call c.file", value);
67
69
  }
68
70
 
69
71
  /**
@@ -75,19 +77,19 @@ function evaluateExpression(value) {
75
77
  // 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".
76
78
  // https://github.com/microsoft/TypeScript/blob/4b794fe1dd0d184d3f8f17e94d8187eace57c91e/src/compiler/types.ts#L2127-L2131
77
79
 
78
- if (ts.isStringLiteralLike(value)) {
80
+ if (ts__default.isStringLiteralLike(value)) {
79
81
  return result.ok(value.text);
80
- } else if (ts.isNumericLiteral(value)) {
82
+ } else if (ts__default.isNumericLiteral(value)) {
81
83
  return result.ok(Number(value.text));
82
- } else if (value.kind === ts.SyntaxKind.TrueKeyword) {
84
+ } else if (value.kind === ts__default.SyntaxKind.TrueKeyword) {
83
85
  return result.ok(true);
84
- } else if (value.kind === ts.SyntaxKind.FalseKeyword) {
86
+ } else if (value.kind === ts__default.SyntaxKind.FalseKeyword) {
85
87
  return result.ok(false);
86
- } else if (value.kind === ts.SyntaxKind.NullKeyword) {
88
+ } else if (value.kind === ts__default.SyntaxKind.NullKeyword) {
87
89
  return result.ok(null);
88
- } else if (ts.isArrayLiteralExpression(value)) {
90
+ } else if (ts__default.isArrayLiteralExpression(value)) {
89
91
  return result.all(value.elements.map(evaluateExpression));
90
- } else if (ts.isObjectLiteralExpression(value)) {
92
+ } else if (ts__default.isObjectLiteralExpression(value)) {
91
93
  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));
92
94
  } else if (isValFileMethodCall(value)) {
93
95
  return pipe(findValFileNodeArg(value), result.flatMap(ref => {
@@ -120,14 +122,14 @@ function findObjectPropertyAssignment(value, key) {
120
122
  }));
121
123
  }
122
124
  function isValFileMethodCall(node) {
123
- return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "file";
125
+ return ts__default.isCallExpression(node) && ts__default.isPropertyAccessExpression(node.expression) && ts__default.isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "file";
124
126
  }
125
127
  function findValFileNodeArg(node) {
126
128
  if (node.arguments.length === 0) {
127
129
  return result.err(new ValSyntaxError(`Invalid c.file() call: missing ref argument`, node));
128
130
  } else if (node.arguments.length > 2) {
129
131
  return result.err(new ValSyntaxError(`Invalid c.file() call: too many arguments ${node.arguments.length}}`, node));
130
- } else if (!ts.isStringLiteral(node.arguments[0])) {
132
+ } else if (!ts__default.isStringLiteral(node.arguments[0])) {
131
133
  return result.err(new ValSyntaxError(`Invalid c.file() call: ref must be a string literal`, node));
132
134
  }
133
135
  const refNode = node.arguments[0];
@@ -139,7 +141,7 @@ function findValFileMetadataArg(node) {
139
141
  } else if (node.arguments.length > 2) {
140
142
  return result.err(new ValSyntaxError(`Invalid c.file() call: too many arguments ${node.arguments.length}}`, node));
141
143
  } else if (node.arguments.length === 2) {
142
- if (!ts.isObjectLiteralExpression(node.arguments[1])) {
144
+ if (!ts__default.isObjectLiteralExpression(node.arguments[1])) {
143
145
  return result.err(new ValSyntaxError(`Invalid c.file() call: metadata must be a object literal`, node));
144
146
  }
145
147
  return result.ok(node.arguments[1]);
@@ -154,7 +156,7 @@ function findValFileMetadataArg(node) {
154
156
  */
155
157
  function validateInitializers(nodes) {
156
158
  for (const node of nodes) {
157
- if (ts.isSpreadElement(node)) {
159
+ if (ts__default.isSpreadElement(node)) {
158
160
  return new ValSyntaxError("Unexpected spread element", node);
159
161
  }
160
162
  }
@@ -165,22 +167,22 @@ function isPath(node, path) {
165
167
  let currentNode = node;
166
168
  for (let i = path.length - 1; i > 0; --i) {
167
169
  const name = path[i];
168
- if (!ts.isPropertyAccessExpression(currentNode)) {
170
+ if (!ts__default.isPropertyAccessExpression(currentNode)) {
169
171
  return false;
170
172
  }
171
- if (!ts.isIdentifier(currentNode.name) || currentNode.name.text !== name) {
173
+ if (!ts__default.isIdentifier(currentNode.name) || currentNode.name.text !== name) {
172
174
  return false;
173
175
  }
174
176
  currentNode = currentNode.expression;
175
177
  }
176
- return ts.isIdentifier(currentNode) && currentNode.text === path[0];
178
+ return ts__default.isIdentifier(currentNode) && currentNode.text === path[0];
177
179
  }
178
180
  function validateArguments(node, validators) {
179
181
  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))]);
180
182
  }
181
183
  function analyzeDefaultExport(node) {
182
184
  const cDefine = node.expression;
183
- if (!ts.isCallExpression(cDefine)) {
185
+ if (!ts__default.isCallExpression(cDefine)) {
184
186
  return result.err(new ValSyntaxError("Expected default expression to be a call expression", cDefine));
185
187
  }
186
188
  if (!isPath(cDefine.expression, ["c", "define"])) {
@@ -188,7 +190,7 @@ function analyzeDefaultExport(node) {
188
190
  }
189
191
  return pipe(validateArguments(cDefine, [id => {
190
192
  // TODO: validate ID value here?
191
- if (!ts.isStringLiteralLike(id)) {
193
+ if (!ts__default.isStringLiteralLike(id)) {
192
194
  return result.err(new ValSyntaxError("Expected first argument to c.define to be a string literal", id));
193
195
  }
194
196
  return result.voidOk;
@@ -206,7 +208,7 @@ function analyzeDefaultExport(node) {
206
208
  }
207
209
  function analyzeValModule(sourceFile) {
208
210
  const analysis = sourceFile.forEachChild(node => {
209
- if (ts.isExportAssignment(node)) {
211
+ if (ts__default.isExportAssignment(node)) {
210
212
  return analyzeDefaultExport(node);
211
213
  }
212
214
  });
@@ -220,62 +222,62 @@ function isValidIdentifier(text) {
220
222
  if (text.length === 0) {
221
223
  return false;
222
224
  }
223
- if (!ts.isIdentifierStart(text.charCodeAt(0), ts.ScriptTarget.ES2020)) {
225
+ if (!ts__default.isIdentifierStart(text.charCodeAt(0), ts__default.ScriptTarget.ES2020)) {
224
226
  return false;
225
227
  }
226
228
  for (let i = 1; i < text.length; ++i) {
227
- if (!ts.isIdentifierPart(text.charCodeAt(i), ts.ScriptTarget.ES2020)) {
229
+ if (!ts__default.isIdentifierPart(text.charCodeAt(i), ts__default.ScriptTarget.ES2020)) {
228
230
  return false;
229
231
  }
230
232
  }
231
233
  return true;
232
234
  }
233
235
  function createPropertyAssignment(key, value) {
234
- return ts.factory.createPropertyAssignment(isValidIdentifier(key) ? ts.factory.createIdentifier(key) : ts.factory.createStringLiteral(key), toExpression(value));
236
+ return ts__default.factory.createPropertyAssignment(isValidIdentifier(key) ? ts__default.factory.createIdentifier(key) : ts__default.factory.createStringLiteral(key), toExpression(value));
235
237
  }
236
238
  function createValFileReference(value) {
237
- const args = [ts.factory.createStringLiteral(value[FILE_REF_PROP])];
239
+ const args = [ts__default.factory.createStringLiteral(value[FILE_REF_PROP])];
238
240
  if (value.metadata) {
239
241
  args.push(toExpression(value.metadata));
240
242
  }
241
- return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("c"), ts.factory.createIdentifier("file")), undefined, args);
243
+ return ts__default.factory.createCallExpression(ts__default.factory.createPropertyAccessExpression(ts__default.factory.createIdentifier("c"), ts__default.factory.createIdentifier("file")), undefined, args);
242
244
  }
243
245
  function createValRichTextImage(value) {
244
- const args = [ts.factory.createStringLiteral(value[FILE_REF_PROP])];
246
+ const args = [ts__default.factory.createStringLiteral(value[FILE_REF_PROP])];
245
247
  if (value.metadata) {
246
248
  args.push(toExpression(value.metadata));
247
249
  }
248
- return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("c"), "rt"), ts.factory.createIdentifier("image")), undefined, args);
250
+ return ts__default.factory.createCallExpression(ts__default.factory.createPropertyAccessExpression(ts__default.factory.createPropertyAccessExpression(ts__default.factory.createIdentifier("c"), "rt"), ts__default.factory.createIdentifier("image")), undefined, args);
249
251
  }
250
252
  function createValLink(value) {
251
- const args = [ts.factory.createStringLiteral(value.children[0]), toExpression({
253
+ const args = [ts__default.factory.createStringLiteral(value.children[0]), toExpression({
252
254
  href: value.href
253
255
  })];
254
- return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("c"), "rt"), ts.factory.createIdentifier("link")), undefined, args);
256
+ return ts__default.factory.createCallExpression(ts__default.factory.createPropertyAccessExpression(ts__default.factory.createPropertyAccessExpression(ts__default.factory.createIdentifier("c"), "rt"), ts__default.factory.createIdentifier("link")), undefined, args);
255
257
  }
256
258
  function createValRichTextTaggedStringTemplate(value) {
257
259
  const {
258
260
  templateStrings: [head, ...others],
259
261
  exprs
260
262
  } = value;
261
- const tag = ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("c"), ts.factory.createIdentifier("richtext"));
263
+ const tag = ts__default.factory.createPropertyAccessExpression(ts__default.factory.createIdentifier("c"), ts__default.factory.createIdentifier("richtext"));
262
264
  if (exprs.length > 0) {
263
- return ts.factory.createTaggedTemplateExpression(tag, undefined, ts.factory.createTemplateExpression(ts.factory.createTemplateHead(head, head), others.map((s, i) => ts.factory.createTemplateSpan(toExpression(exprs[i]), i < others.length - 1 ? ts.factory.createTemplateMiddle(s, s) : ts.factory.createTemplateTail(s, s)))));
265
+ return ts__default.factory.createTaggedTemplateExpression(tag, undefined, ts__default.factory.createTemplateExpression(ts__default.factory.createTemplateHead(head, head), others.map((s, i) => ts__default.factory.createTemplateSpan(toExpression(exprs[i]), i < others.length - 1 ? ts__default.factory.createTemplateMiddle(s, s) : ts__default.factory.createTemplateTail(s, s)))));
264
266
  }
265
- return ts.factory.createTaggedTemplateExpression(tag, undefined, ts.factory.createNoSubstitutionTemplateLiteral(head, head));
267
+ return ts__default.factory.createTaggedTemplateExpression(tag, undefined, ts__default.factory.createNoSubstitutionTemplateLiteral(head, head));
266
268
  }
267
269
  function toExpression(value) {
268
270
  if (typeof value === "string") {
269
271
  // TODO: Use configuration/heuristics to determine use of single quote or double quote
270
- return ts.factory.createStringLiteral(value);
272
+ return ts__default.factory.createStringLiteral(value);
271
273
  } else if (typeof value === "number") {
272
- return ts.factory.createNumericLiteral(value);
274
+ return ts__default.factory.createNumericLiteral(value);
273
275
  } else if (typeof value === "boolean") {
274
- return value ? ts.factory.createTrue() : ts.factory.createFalse();
276
+ return value ? ts__default.factory.createTrue() : ts__default.factory.createFalse();
275
277
  } else if (value === null) {
276
- return ts.factory.createNull();
278
+ return ts__default.factory.createNull();
277
279
  } else if (Array.isArray(value)) {
278
- return ts.factory.createArrayLiteralExpression(value.map(toExpression));
280
+ return ts__default.factory.createArrayLiteralExpression(value.map(toExpression));
279
281
  } else if (typeof value === "object") {
280
282
  if (isValFileValue(value)) {
281
283
  if (isValRichTextImageValue(value)) {
@@ -287,24 +289,24 @@ function toExpression(value) {
287
289
  } else if (isValRichTextValue(value)) {
288
290
  return createValRichTextTaggedStringTemplate(value);
289
291
  }
290
- return ts.factory.createObjectLiteralExpression(Object.entries(value).map(([key, value]) => createPropertyAssignment(key, value)));
292
+ return ts__default.factory.createObjectLiteralExpression(Object.entries(value).map(([key, value]) => createPropertyAssignment(key, value)));
291
293
  } else {
292
- return ts.factory.createStringLiteral(value);
294
+ return ts__default.factory.createStringLiteral(value);
293
295
  }
294
296
  }
295
297
 
296
298
  // TODO: Choose newline based on project settings/heuristics/system default?
297
- const newLine = ts.NewLineKind.LineFeed;
299
+ const newLine = ts__default.NewLineKind.LineFeed;
298
300
  // TODO: Handle indentation of printed code
299
- const printer = ts.createPrinter({
301
+ const printer = ts__default.createPrinter({
300
302
  newLine: newLine
301
303
  // neverAsciiEscape: true,
302
304
  });
303
305
  function replaceNodeValue(document, node, value) {
304
- const replacementText = printer.printNode(ts.EmitHint.Unspecified, toExpression(value), document);
305
- const span = ts.createTextSpanFromBounds(node.getStart(document, false), node.end);
306
- const newText = `${document.text.substring(0, span.start)}${replacementText}${document.text.substring(ts.textSpanEnd(span))}`;
307
- return [document.update(newText, ts.createTextChangeRange(span, replacementText.length)), node];
306
+ const replacementText = printer.printNode(ts__default.EmitHint.Unspecified, toExpression(value), document);
307
+ const span = ts__default.createTextSpanFromBounds(node.getStart(document, false), node.end);
308
+ const newText = `${document.text.substring(0, span.start)}${replacementText}${document.text.substring(ts__default.textSpanEnd(span))}`;
309
+ return [document.update(newText, ts__default.createTextChangeRange(span, replacementText.length)), node];
308
310
  }
309
311
  function isIndentation(s) {
310
312
  for (let i = 0; i < s.length; ++i) {
@@ -316,7 +318,7 @@ function isIndentation(s) {
316
318
  return true;
317
319
  }
318
320
  function newLineStr(kind) {
319
- if (kind === ts.NewLineKind.CarriageReturnLineFeed) {
321
+ if (kind === ts__default.NewLineKind.CarriageReturnLineFeed) {
320
322
  return "\r\n";
321
323
  } else {
322
324
  return "\n";
@@ -338,38 +340,38 @@ function insertAt(document, nodes, index, node) {
338
340
  let replacementText;
339
341
  if (nodes.length === 0) {
340
342
  // Replace entire range of nodes
341
- replacementText = printer.printNode(ts.EmitHint.Unspecified, node, document);
342
- span = ts.createTextSpanFromBounds(nodes.pos, nodes.end);
343
+ replacementText = printer.printNode(ts__default.EmitHint.Unspecified, node, document);
344
+ span = ts__default.createTextSpanFromBounds(nodes.pos, nodes.end);
343
345
  } else if (index === nodes.length) {
344
346
  // Insert after last node
345
347
  const neighbor = nodes[nodes.length - 1];
346
- replacementText = `${getSeparator(document, neighbor)}${printer.printNode(ts.EmitHint.Unspecified, node, document)}`;
347
- span = ts.createTextSpan(neighbor.end, 0);
348
+ replacementText = `${getSeparator(document, neighbor)}${printer.printNode(ts__default.EmitHint.Unspecified, node, document)}`;
349
+ span = ts__default.createTextSpan(neighbor.end, 0);
348
350
  } else {
349
351
  // Insert before node
350
352
  const neighbor = nodes[index];
351
- replacementText = `${printer.printNode(ts.EmitHint.Unspecified, node, document)}${getSeparator(document, neighbor)}`;
352
- span = ts.createTextSpan(neighbor.getStart(document, true), 0);
353
+ replacementText = `${printer.printNode(ts__default.EmitHint.Unspecified, node, document)}${getSeparator(document, neighbor)}`;
354
+ span = ts__default.createTextSpan(neighbor.getStart(document, true), 0);
353
355
  }
354
- const newText = `${document.text.substring(0, span.start)}${replacementText}${document.text.substring(ts.textSpanEnd(span))}`;
355
- return document.update(newText, ts.createTextChangeRange(span, replacementText.length));
356
+ const newText = `${document.text.substring(0, span.start)}${replacementText}${document.text.substring(ts__default.textSpanEnd(span))}`;
357
+ return document.update(newText, ts__default.createTextChangeRange(span, replacementText.length));
356
358
  }
357
359
  function removeAt(document, nodes, index) {
358
360
  const node = nodes[index];
359
361
  let span;
360
362
  if (nodes.length === 1) {
361
- span = ts.createTextSpanFromBounds(nodes.pos, nodes.end);
363
+ span = ts__default.createTextSpanFromBounds(nodes.pos, nodes.end);
362
364
  } else if (index === nodes.length - 1) {
363
365
  // Remove until previous node
364
366
  const neighbor = nodes[index - 1];
365
- span = ts.createTextSpanFromBounds(neighbor.end, node.end);
367
+ span = ts__default.createTextSpanFromBounds(neighbor.end, node.end);
366
368
  } else {
367
369
  // Remove before next node
368
370
  const neighbor = nodes[index + 1];
369
- span = ts.createTextSpanFromBounds(node.getStart(document, true), neighbor.getStart(document, true));
371
+ span = ts__default.createTextSpanFromBounds(node.getStart(document, true), neighbor.getStart(document, true));
370
372
  }
371
- const newText = `${document.text.substring(0, span.start)}${document.text.substring(ts.textSpanEnd(span))}`;
372
- return [document.update(newText, ts.createTextChangeRange(span, 0)), node];
373
+ const newText = `${document.text.substring(0, span.start)}${document.text.substring(ts__default.textSpanEnd(span))}`;
374
+ return [document.update(newText, ts__default.createTextChangeRange(span, 0)), node];
373
375
  }
374
376
  function parseAndValidateArrayInsertIndex(key, nodes) {
375
377
  if (key === "-") {
@@ -411,9 +413,9 @@ function parseAndValidateArrayInboundsIndex(key, nodes) {
411
413
  }));
412
414
  }
413
415
  function replaceInNode(document, node, key, value) {
414
- if (ts.isArrayLiteralExpression(node)) {
416
+ if (ts__default.isArrayLiteralExpression(node)) {
415
417
  return pipe(parseAndValidateArrayInboundsIndex(key, node.elements), result.map(index => replaceNodeValue(document, node.elements[index], value)));
416
- } else if (ts.isObjectLiteralExpression(node)) {
418
+ } else if (ts__default.isObjectLiteralExpression(node)) {
417
419
  return pipe(findObjectPropertyAssignment(node, key), result.flatMap(assignment => {
418
420
  if (!assignment) {
419
421
  return result.err(new PatchError("Cannot replace object element which does not exist"));
@@ -436,7 +438,7 @@ function replaceInNode(document, node, key, value) {
436
438
  }
437
439
  return replaceInNode(document,
438
440
  // TODO: creating a fake object here might not be right - seems to work though
439
- ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
441
+ ts__default.factory.createObjectLiteralExpression([ts__default.factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
440
442
  }));
441
443
  }
442
444
  } else {
@@ -451,9 +453,9 @@ function replaceAtPath(document, rootNode, path, value) {
451
453
  }
452
454
  }
453
455
  function getFromNode(node, key) {
454
- if (ts.isArrayLiteralExpression(node)) {
456
+ if (ts__default.isArrayLiteralExpression(node)) {
455
457
  return pipe(parseAndValidateArrayInboundsIndex(key, node.elements), result.map(index => node.elements[index]));
456
- } else if (ts.isObjectLiteralExpression(node)) {
458
+ } else if (ts__default.isObjectLiteralExpression(node)) {
457
459
  return pipe(findObjectPropertyAssignment(node, key), result.map(assignment => assignment === null || assignment === void 0 ? void 0 : assignment.initializer));
458
460
  } else if (isValFileMethodCall(node)) {
459
461
  if (key === FILE_REF_PROP) {
@@ -483,9 +485,9 @@ function getAtPath(rootNode, path) {
483
485
  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));
484
486
  }
485
487
  function removeFromNode(document, node, key) {
486
- if (ts.isArrayLiteralExpression(node)) {
488
+ if (ts__default.isArrayLiteralExpression(node)) {
487
489
  return pipe(parseAndValidateArrayInboundsIndex(key, node.elements), result.map(index => removeAt(document, node.elements, index)));
488
- } else if (ts.isObjectLiteralExpression(node)) {
490
+ } else if (ts__default.isObjectLiteralExpression(node)) {
489
491
  return pipe(findObjectPropertyAssignment(node, key), result.flatMap(assignment => {
490
492
  if (!assignment) {
491
493
  return result.err(new PatchError("Cannot replace object element which does not exist"));
@@ -531,9 +533,9 @@ function isValRichTextValue(value) {
531
533
  return !!(typeof value === "object" && value && VAL_EXTENSION in value && value[VAL_EXTENSION] === "richtext" && "templateStrings" in value && typeof value.templateStrings === "object" && Array.isArray(value.templateStrings));
532
534
  }
533
535
  function addToNode(document, node, key, value) {
534
- if (ts.isArrayLiteralExpression(node)) {
536
+ if (ts__default.isArrayLiteralExpression(node)) {
535
537
  return pipe(parseAndValidateArrayInsertIndex(key, node.elements), result.map(index => [insertAt(document, node.elements, index, toExpression(value))]));
536
- } else if (ts.isObjectLiteralExpression(node)) {
538
+ } else if (ts__default.isObjectLiteralExpression(node)) {
537
539
  if (key === FILE_REF_PROP) {
538
540
  return result.err(new PatchError("Cannot add a key ref to object"));
539
541
  }
@@ -602,7 +604,7 @@ class TSOps {
602
604
  }
603
605
  }
604
606
 
605
- const ops = new TSOps(document => {
607
+ const ops$1 = new TSOps(document => {
606
608
  return pipe(analyzeValModule(document), result.map(({
607
609
  source
608
610
  }) => source));
@@ -617,7 +619,7 @@ const patchValFile = async (id, valConfigPath, patch, sourceFileHandler, runtime
617
619
  if (!sourceFile) {
618
620
  throw Error(`Source file ${filePath} not found`);
619
621
  }
620
- const derefRes = derefPatch(patch, sourceFile, ops);
622
+ const derefRes = derefPatch(patch, sourceFile, ops$1);
621
623
  if (result.isErr(derefRes)) {
622
624
  throw derefRes.error;
623
625
  }
@@ -655,9 +657,9 @@ function convertDataUrlToBase64(dataUrl) {
655
657
  }
656
658
  const patchSourceFile = (sourceFile, patch) => {
657
659
  if (typeof sourceFile === "string") {
658
- return applyPatch(ts.createSourceFile("<val>", sourceFile, ts.ScriptTarget.ES2015), ops, patch);
660
+ return applyPatch(ts__default.createSourceFile("<val>", sourceFile, ts__default.ScriptTarget.ES2015), ops$1, patch);
659
661
  }
660
- return applyPatch(sourceFile, ops, patch);
662
+ return applyPatch(sourceFile, ops$1, patch);
661
663
  };
662
664
 
663
665
  const readValFile = async (id, valConfigPath, runtime) => {
@@ -771,7 +773,7 @@ const getCompilerOptions = (rootDir, parseConfigHost) => {
771
773
  const {
772
774
  config,
773
775
  error
774
- } = ts.readConfigFile(configFilePath, parseConfigHost.readFile.bind(parseConfigHost));
776
+ } = ts__default.readConfigFile(configFilePath, parseConfigHost.readFile.bind(parseConfigHost));
775
777
  if (error) {
776
778
  if (typeof error.messageText === "string") {
777
779
  throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText}`);
@@ -779,7 +781,7 @@ const getCompilerOptions = (rootDir, parseConfigHost) => {
779
781
  throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText.messageText}`);
780
782
  }
781
783
  const optionsOverrides = undefined;
782
- const parsedConfigFile = ts.parseJsonConfigFileContent(config, parseConfigHost, rootDir, optionsOverrides, configFilePath);
784
+ const parsedConfigFile = ts__default.parseJsonConfigFileContent(config, parseConfigHost, rootDir, optionsOverrides, configFilePath);
783
785
  if (parsedConfigFile.errors.length > 0) {
784
786
  throw Error(`Could not parse config file: ${configFilePath}. Errors: ${parsedConfigFile.errors.map(e => e.messageText).join("\n")}`);
785
787
  }
@@ -788,8 +790,14 @@ const getCompilerOptions = (rootDir, parseConfigHost) => {
788
790
 
789
791
  class ValSourceFileHandler {
790
792
  constructor(projectRoot, compilerOptions, host = {
791
- ...ts.sys,
792
- writeFile: fs.writeFileSync
793
+ ...ts__default.sys,
794
+ writeFile: (fileName, data, encoding) => {
795
+ fs.mkdirSync(path__default.dirname(fileName), {
796
+ recursive: true
797
+ });
798
+ fs.writeFileSync(fileName, data, encoding);
799
+ },
800
+ rmFile: fs.rmSync
793
801
  }) {
794
802
  this.projectRoot = projectRoot;
795
803
  this.compilerOptions = compilerOptions;
@@ -797,9 +805,9 @@ class ValSourceFileHandler {
797
805
  }
798
806
  getSourceFile(filePath) {
799
807
  const fileContent = this.host.readFile(filePath);
800
- const scriptTarget = this.compilerOptions.target ?? ts.ScriptTarget.ES2020;
808
+ const scriptTarget = this.compilerOptions.target ?? ts__default.ScriptTarget.ES2020;
801
809
  if (fileContent) {
802
- return ts.createSourceFile(filePath, fileContent, scriptTarget);
810
+ return ts__default.createSourceFile(filePath, fileContent, scriptTarget);
803
811
  }
804
812
  }
805
813
  writeSourceFile(sourceFile) {
@@ -811,7 +819,7 @@ class ValSourceFileHandler {
811
819
  this.host.writeFile(filePath, content, encoding);
812
820
  }
813
821
  resolveSourceModulePath(containingFilePath, requestedModuleName) {
814
- const resolutionRes = ts.resolveModuleName(requestedModuleName, path__default.isAbsolute(containingFilePath) ? containingFilePath : path__default.resolve(this.projectRoot, containingFilePath), this.compilerOptions, this.host, undefined, undefined, ts.ModuleKind.ESNext);
822
+ const resolutionRes = ts__default.resolveModuleName(requestedModuleName, path__default.isAbsolute(containingFilePath) ? containingFilePath : path__default.resolve(this.projectRoot, containingFilePath), this.compilerOptions, this.host, undefined, undefined, ts__default.ModuleKind.ESNext);
815
823
  const resolvedModule = resolutionRes.resolvedModule;
816
824
  if (!resolvedModule) {
817
825
  throw Error(`Could not resolve module "${requestedModuleName}", base: "${containingFilePath}": No resolved modules returned: ${JSON.stringify(resolutionRes)}`);
@@ -823,7 +831,7 @@ class ValSourceFileHandler {
823
831
  const JsFileLookupMapping = [
824
832
  // NOTE: first one matching will be used
825
833
  [".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js", ".esm.js", ".mjs.js"]]];
826
- const MAX_CACHE_SIZE = 10 * 1024 * 1024; // 10 mb
834
+ const MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100 mb
827
835
  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
828
836
 
829
837
  class ValModuleLoader {
@@ -832,8 +840,14 @@ class ValModuleLoader {
832
840
  compilerOptions,
833
841
  // TODO: remove this?
834
842
  sourceFileHandler, host = {
835
- ...ts.sys,
836
- writeFile: fs.writeFileSync
843
+ ...ts__default.sys,
844
+ writeFile: (fileName, data, encoding) => {
845
+ fs.mkdirSync(path__default.dirname(fileName), {
846
+ recursive: true
847
+ });
848
+ fs.writeFileSync(fileName, data, encoding);
849
+ },
850
+ rmFile: fs.rmSync
837
851
  }, disableCache = false) {
838
852
  this.projectRoot = projectRoot;
839
853
  this.compilerOptions = compilerOptions;
@@ -1086,8 +1100,14 @@ export const IS_DEV = false;2
1086
1100
  }
1087
1101
 
1088
1102
  async function createService(projectRoot, opts, host = {
1089
- ...ts.sys,
1090
- writeFile: fs.writeFileSync
1103
+ ...ts__default.sys,
1104
+ writeFile: (fileName, data, encoding) => {
1105
+ fs.mkdirSync(path__default.dirname(fileName), {
1106
+ recursive: true
1107
+ });
1108
+ fs.writeFileSync(fileName, data, encoding);
1109
+ },
1110
+ rmFile: fs.rmSync
1091
1111
  }, loader) {
1092
1112
  const compilerOptions = getCompilerOptions(projectRoot, host);
1093
1113
  const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
@@ -1103,7 +1123,7 @@ class Service {
1103
1123
  this.runtime = runtime;
1104
1124
  this.valConfigPath = valConfigPath || "./val.config";
1105
1125
  }
1106
- async get(moduleId, modulePath) {
1126
+ async get(moduleId, modulePath = "") {
1107
1127
  const valModule = await readValFile(moduleId, this.valConfigPath, this.runtime);
1108
1128
  if (valModule.source && valModule.schema) {
1109
1129
  const resolved = Internal.resolvePath(modulePath, valModule.source, valModule.schema);
@@ -1124,7 +1144,7 @@ class Service {
1124
1144
  }
1125
1145
  }
1126
1146
  async patch(moduleId, patch) {
1127
- return patchValFile(moduleId, this.valConfigPath, patch, this.sourceFileHandler, this.runtime);
1147
+ await patchValFile(moduleId, this.valConfigPath, patch, this.sourceFileHandler, this.runtime);
1128
1148
  }
1129
1149
  dispose() {
1130
1150
  this.runtime.dispose();
@@ -1172,92 +1192,157 @@ const OperationJSONT = z.discriminatedUnion("op", [z.object({
1172
1192
  value: z.string()
1173
1193
  }).strict()]);
1174
1194
  const PatchJSON = z.array(OperationJSONT);
1195
+ /**
1196
+ * Raw JSON patch operation.
1197
+ */
1198
+ const OperationT = z.discriminatedUnion("op", [z.object({
1199
+ op: z.literal("add"),
1200
+ path: z.array(z.string()),
1201
+ value: JSONValueT
1202
+ }).strict(), z.object({
1203
+ op: z.literal("remove"),
1204
+ path: z.array(z.string()).nonempty()
1205
+ }).strict(), z.object({
1206
+ op: z.literal("replace"),
1207
+ path: z.array(z.string()),
1208
+ value: JSONValueT
1209
+ }).strict(), z.object({
1210
+ op: z.literal("move"),
1211
+ from: z.array(z.string()).nonempty(),
1212
+ path: z.array(z.string())
1213
+ }).strict(), z.object({
1214
+ op: z.literal("copy"),
1215
+ from: z.array(z.string()),
1216
+ path: z.array(z.string())
1217
+ }).strict(), z.object({
1218
+ op: z.literal("test"),
1219
+ path: z.array(z.string()),
1220
+ value: JSONValueT
1221
+ }).strict(), z.object({
1222
+ op: z.literal("file"),
1223
+ path: z.array(z.string()),
1224
+ filePath: z.string(),
1225
+ value: z.union([z.string(), z.object({
1226
+ sha256: z.string(),
1227
+ mimeType: z.string()
1228
+ })])
1229
+ }).strict()]);
1230
+ const Patch = z.array(OperationT);
1175
1231
 
1176
- const ENABLE_COOKIE_VALUE = {
1177
- value: "true",
1178
- options: {
1179
- httpOnly: false,
1180
- sameSite: "lax"
1181
- }
1182
- };
1183
- function getRedirectUrl(query, overrideHost) {
1184
- if (typeof query.redirect_to !== "string") {
1185
- return {
1186
- status: 400,
1187
- json: {
1188
- message: "Missing redirect_to query param"
1189
- }
1190
- };
1232
+ function getValidationErrorFileRef(validationError) {
1233
+ const maybeRef = validationError.value && typeof validationError.value === "object" && FILE_REF_PROP in validationError.value && typeof validationError.value[FILE_REF_PROP] === "string" ? validationError.value[FILE_REF_PROP] : undefined;
1234
+ if (!maybeRef) {
1235
+ return null;
1191
1236
  }
1192
- if (overrideHost) {
1193
- return overrideHost + "?redirect_to=" + encodeURIComponent(query.redirect_to);
1237
+ return maybeRef;
1238
+ }
1239
+
1240
+ const textEncoder$1 = new TextEncoder();
1241
+ async function extractImageMetadata(filename, input) {
1242
+ const imageSize = sizeOf(input);
1243
+ let mimeType = null;
1244
+ if (imageSize.type) {
1245
+ const possibleMimeType = `image/${imageSize.type}`;
1246
+ if (MIME_TYPES_TO_EXT[possibleMimeType]) {
1247
+ mimeType = possibleMimeType;
1248
+ }
1249
+ const filenameBasedLookup = filenameToMimeType(filename);
1250
+ if (filenameBasedLookup) {
1251
+ mimeType = filenameBasedLookup;
1252
+ }
1253
+ }
1254
+ if (!mimeType) {
1255
+ mimeType = "application/octet-stream";
1256
+ }
1257
+ let {
1258
+ width,
1259
+ height
1260
+ } = imageSize;
1261
+ if (!width || !height) {
1262
+ width = 0;
1263
+ height = 0;
1264
+ }
1265
+ const sha256 = getSha256(mimeType, input);
1266
+ return {
1267
+ width,
1268
+ height,
1269
+ sha256,
1270
+ mimeType
1271
+ };
1272
+ }
1273
+ function getSha256(mimeType, input) {
1274
+ return Internal.getSHA256Hash(textEncoder$1.encode(
1275
+ // TODO: we should probably store the mimetype in the metadata and reuse it here
1276
+ `data:${mimeType};base64,${input.toString("base64")}`));
1277
+ }
1278
+ async function extractFileMetadata(filename, input) {
1279
+ let mimeType = filenameToMimeType(filename);
1280
+ if (!mimeType) {
1281
+ mimeType = "application/octet-stream";
1194
1282
  }
1195
- return query.redirect_to;
1283
+ const sha256 = getSha256(mimeType, input);
1284
+ return {
1285
+ sha256,
1286
+ mimeType
1287
+ };
1196
1288
  }
1197
1289
 
1198
- class LocalValServer {
1199
- constructor(options, callbacks) {
1200
- this.options = options;
1201
- this.callbacks = callbacks;
1290
+ function validateMetadata(actualMetadata, expectedMetadata) {
1291
+ const missingMetadata = [];
1292
+ const erroneousMetadata = {};
1293
+ if (typeof actualMetadata !== "object" || actualMetadata === null) {
1294
+ return {
1295
+ globalErrors: ["Metadata is wrong type: must be an object."]
1296
+ };
1202
1297
  }
1203
- async session() {
1298
+ if (Array.isArray(actualMetadata)) {
1204
1299
  return {
1205
- status: 200,
1206
- json: {
1207
- mode: "local",
1208
- enabled: await this.callbacks.isEnabled()
1209
- }
1300
+ globalErrors: ["Metadata is wrong type: cannot be an array."]
1210
1301
  };
1211
1302
  }
1212
- async getTree(treePath,
1213
- // TODO: use the params: patch, schema, source
1214
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1215
- query) {
1216
- const rootDir = process.cwd();
1217
- const moduleIds = [];
1218
- // iterate over all .val files in the root directory
1219
- const walk = async dir => {
1220
- const files = await promises.readdir(dir);
1221
- for (const file of files) {
1222
- if ((await promises.stat(path__default.join(dir, file))).isDirectory()) {
1223
- if (file === "node_modules") continue;
1224
- await walk(path__default.join(dir, file));
1225
- } else {
1226
- const isValFile = file.endsWith(".val.js") || file.endsWith(".val.ts");
1227
- if (!isValFile) {
1228
- continue;
1229
- }
1230
- if (treePath && !path__default.join(dir, file).replace(rootDir, "").startsWith(treePath)) {
1231
- continue;
1232
- }
1233
- moduleIds.push(path__default.join(dir, file).replace(rootDir, "").replace(".val.js", "").replace(".val.ts", "").split(path__default.sep).join("/"));
1303
+ const recordMetadata = actualMetadata;
1304
+ const globalErrors = [];
1305
+ for (const anyKey in expectedMetadata) {
1306
+ if (typeof anyKey !== "string") {
1307
+ globalErrors.push(`Expected metadata has key '${anyKey}' that is not typeof 'string', but: '${typeof anyKey}'. This is most likely a Val bug.`);
1308
+ } else {
1309
+ if (anyKey in actualMetadata) {
1310
+ const key = anyKey;
1311
+ if (expectedMetadata[key] !== recordMetadata[key]) {
1312
+ erroneousMetadata[key] = `Expected metadata '${key}' to be ${JSON.stringify(expectedMetadata[key])}, but got ${JSON.stringify(recordMetadata[key])}.`;
1234
1313
  }
1314
+ } else {
1315
+ missingMetadata.push(anyKey);
1235
1316
  }
1236
- };
1237
- const serializedModuleContent = await walk(rootDir).then(async () => {
1238
- return Promise.all(moduleIds.map(async moduleId => {
1239
- return await this.options.service.get(moduleId, "");
1240
- }));
1241
- });
1317
+ }
1318
+ }
1319
+ if (globalErrors.length === 0 && missingMetadata.length === 0 && Object.keys(erroneousMetadata).length === 0) {
1320
+ return false;
1321
+ }
1322
+ return {
1323
+ missingMetadata,
1324
+ erroneousMetadata
1325
+ };
1326
+ }
1242
1327
 
1243
- //
1244
- const modules = Object.fromEntries(serializedModuleContent.map(serializedModuleContent => {
1245
- const module = {
1246
- schema: serializedModuleContent.schema,
1247
- source: serializedModuleContent.source,
1248
- errors: serializedModuleContent.errors
1249
- };
1250
- return [serializedModuleContent.path, module];
1251
- }));
1252
- const apiTreeResponse = {
1253
- modules,
1254
- git: this.options.git
1255
- };
1256
- return {
1257
- status: 200,
1258
- json: apiTreeResponse
1259
- };
1328
+ function getValidationErrorMetadata(validationError) {
1329
+ const maybeMetadata = validationError.value && typeof validationError.value === "object" && "metadata" in validationError.value && validationError.value.metadata && validationError.value.metadata;
1330
+ if (!maybeMetadata) {
1331
+ return null;
1332
+ }
1333
+ return maybeMetadata;
1334
+ }
1335
+
1336
+ const ops = new JSONOps();
1337
+ class ValServer {
1338
+ constructor(cwd, host, options, callbacks) {
1339
+ this.cwd = cwd;
1340
+ this.host = host;
1341
+ this.options = options;
1342
+ this.callbacks = callbacks;
1260
1343
  }
1344
+
1345
+ /* Auth endpoints: */
1261
1346
  async enable(query) {
1262
1347
  const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
1263
1348
  if (typeof redirectToRes !== "string") {
@@ -1288,98 +1373,900 @@ class LocalValServer {
1288
1373
  redirectTo: redirectToRes
1289
1374
  };
1290
1375
  }
1291
- async postPatches(body) {
1292
- // First validate that the body has the right structure
1293
- const patchJSON = z$1.record(PatchJSON).safeParse(body);
1294
- if (!patchJSON.success) {
1376
+ getAllModules(treePath) {
1377
+ const moduleIds = this.host.readDirectory(this.cwd, ["ts", "js"], ["node_modules", ".*"], ["**/*.val.ts", "**/*.val.js"]).filter(file => {
1378
+ if (treePath) {
1379
+ return file.replace(this.cwd, "").startsWith(treePath);
1380
+ }
1381
+ return true;
1382
+ }).map(file => file.replace(this.cwd, "").replace(".val.js", "").replace(".val.ts", "").split(path__default.sep).join("/"));
1383
+ return moduleIds;
1384
+ }
1385
+ async getTree(treePath,
1386
+ // TODO: use the params: patch, schema, source now we return everything, every time
1387
+ query, cookies) {
1388
+ const ensureRes = await this.ensureRemoteFSInitialized("getTree", cookies);
1389
+ if (result.isErr(ensureRes)) {
1390
+ return ensureRes.error;
1391
+ }
1392
+ const moduleIds = this.getAllModules(treePath);
1393
+ const applyPatches = query.patch === "true";
1394
+ let {
1395
+ patchIdsByModuleId,
1396
+ patchesById,
1397
+ fileUpdates
1398
+ } = {
1399
+ patchIdsByModuleId: {},
1400
+ patchesById: {},
1401
+ fileUpdates: {}
1402
+ };
1403
+ if (applyPatches) {
1404
+ const res = await this.readPatches(cookies);
1405
+ if (result.isErr(res)) {
1406
+ return res.error;
1407
+ }
1408
+ patchIdsByModuleId = res.value.patchIdsByModuleId;
1409
+ patchesById = res.value.patchesById;
1410
+ fileUpdates = res.value.fileUpdates;
1411
+ }
1412
+ const possiblyPatchedContent = await Promise.all(moduleIds.map(async moduleId => {
1413
+ return this.applyAllPatchesThenValidate(moduleId, patchIdsByModuleId, patchesById, fileUpdates, applyPatches, cookies);
1414
+ }));
1415
+ const modules = Object.fromEntries(possiblyPatchedContent.map(serializedModuleContent => {
1416
+ const module = {
1417
+ schema: serializedModuleContent.schema,
1418
+ source: serializedModuleContent.source,
1419
+ errors: serializedModuleContent.errors
1420
+ };
1421
+ return [serializedModuleContent.path, module];
1422
+ }));
1423
+ const apiTreeResponse = {
1424
+ modules,
1425
+ git: this.options.git
1426
+ };
1427
+ return {
1428
+ status: 200,
1429
+ json: apiTreeResponse
1430
+ };
1431
+ }
1432
+ async postValidate(rawBody, cookies) {
1433
+ const ensureRes = await this.ensureRemoteFSInitialized("postValidate", cookies);
1434
+ if (result.isErr(ensureRes)) {
1435
+ return ensureRes.error;
1436
+ }
1437
+ return this.validateThenMaybeCommit(rawBody, false, cookies);
1438
+ }
1439
+ async postCommit(rawBody, cookies) {
1440
+ const ensureRes = await this.ensureRemoteFSInitialized("postCommit", cookies);
1441
+ if (result.isErr(ensureRes)) {
1442
+ return ensureRes.error;
1443
+ }
1444
+ const res = await this.validateThenMaybeCommit(rawBody, true, cookies);
1445
+ if (res.status === 200) {
1446
+ if (res.json.validationErrors) {
1447
+ return {
1448
+ status: 400,
1449
+ json: {
1450
+ ...res.json
1451
+ }
1452
+ };
1453
+ }
1295
1454
  return {
1296
- status: 404,
1455
+ status: 200,
1297
1456
  json: {
1298
- message: `Invalid patch: ${patchJSON.error.message}`,
1299
- details: patchJSON.error.issues
1457
+ ...res.json,
1458
+ git: this.options.git
1300
1459
  }
1301
1460
  };
1302
1461
  }
1462
+ return res;
1463
+ }
1303
1464
 
1304
- // const id = randomUUID();
1305
- // console.time("patching:" + id);
1306
- for (const moduleId in patchJSON.data) {
1307
- // Then parse/validate
1308
- // TODO: validate all and then fail instead:
1309
- const patch = parsePatch(patchJSON.data[moduleId]);
1310
- if (result.isErr(patch)) {
1311
- console.error("Unexpected error parsing patch", patch.error);
1312
- throw new Error("Unexpected error parsing patch");
1313
- }
1314
- await this.options.service.patch(moduleId, patch.value);
1315
- }
1316
- // console.timeEnd("patching:" + id);
1465
+ /* */
1317
1466
 
1318
- return {
1319
- status: 200,
1320
- json: {} // no patch ids created
1321
- };
1322
- }
1323
- badRequest() {
1324
- return {
1325
- status: 400,
1326
- json: {
1327
- message: "Local server does not handle this request"
1467
+ async applyAllPatchesThenValidate(moduleId, patchIdsByModuleId, patchesById, fileUpdates, applyPatches, cookies) {
1468
+ const serializedModuleContent = await this.getModule(moduleId);
1469
+ const schema = serializedModuleContent.schema;
1470
+ const maybeSource = serializedModuleContent.source;
1471
+ if (!applyPatches) {
1472
+ return serializedModuleContent;
1473
+ }
1474
+ if (serializedModuleContent.errors && (serializedModuleContent.errors.fatal || serializedModuleContent.errors.invalidModuleId)) {
1475
+ return serializedModuleContent;
1476
+ }
1477
+ if (!maybeSource || !schema) {
1478
+ return serializedModuleContent;
1479
+ }
1480
+ let source = maybeSource;
1481
+ for (const patchId of patchIdsByModuleId[moduleId] ?? []) {
1482
+ const patch = patchesById[patchId];
1483
+ if (!patch) {
1484
+ continue;
1485
+ }
1486
+ const patchRes = applyPatch(source, ops, patch.filter(Internal.notFileOp));
1487
+ if (result.isOk(patchRes)) {
1488
+ source = patchRes.value;
1489
+ } else {
1490
+ console.error("Val: got an unexpected error while applying patch. Is there a mismatch in Val versions? Perhaps Val is misconfigured?", {
1491
+ patchId,
1492
+ moduleId,
1493
+ patch: JSON.stringify(patch, null, 2),
1494
+ error: patchRes.error
1495
+ });
1496
+ return {
1497
+ path: moduleId,
1498
+ schema,
1499
+ source,
1500
+ errors: {
1501
+ fatal: [{
1502
+ message: "Unexpected error applying patch",
1503
+ type: "invalid-patch"
1504
+ }]
1505
+ }
1506
+ };
1328
1507
  }
1508
+ }
1509
+ const validationErrors = deserializeSchema(schema).validate(moduleId, source);
1510
+ if (validationErrors) {
1511
+ const revalidated = await this.revalidateImageAndFileValidation(validationErrors, fileUpdates, cookies);
1512
+ return {
1513
+ path: moduleId,
1514
+ schema,
1515
+ source,
1516
+ errors: revalidated && {
1517
+ validation: revalidated
1518
+ }
1519
+ };
1520
+ }
1521
+ return {
1522
+ path: moduleId,
1523
+ schema,
1524
+ source,
1525
+ errors: false
1329
1526
  };
1330
1527
  }
1331
1528
 
1332
- // eslint-disable-next-line @typescript-eslint/ban-types
1333
- async postCommit() {
1334
- return this.badRequest();
1335
- }
1336
- async authorize() {
1337
- return this.badRequest();
1338
- }
1339
- async callback() {
1340
- return this.badRequest();
1341
- }
1342
- async logout() {
1343
- return this.badRequest();
1344
- }
1345
- async getFiles() {
1346
- return this.badRequest();
1529
+ // TODO: name this better: we need to check for image and file validation errors
1530
+ // since they cannot be handled directly inside the validation function.
1531
+ // The reason is that validate will be called inside QuickJS (in the future, hopefully),
1532
+ // which does not have access to the filesystem, at least not at the time of writing this comment.
1533
+ // If you are reading this, and we still are not using QuickJS to validate, this assumption might be wrong.
1534
+ async revalidateImageAndFileValidation(validationErrors, fileUpdates, cookies) {
1535
+ const revalidatedValidationErrors = {};
1536
+ for (const pathStr in validationErrors) {
1537
+ const errorSourcePath = pathStr;
1538
+ const errors = validationErrors[errorSourcePath];
1539
+ revalidatedValidationErrors[errorSourcePath] = [];
1540
+ for (const error of errors) {
1541
+ var _error$fixes;
1542
+ if ((_error$fixes = error.fixes) !== null && _error$fixes !== void 0 && _error$fixes.every(fix => fix === "file:check-metadata" || fix === "image:replace-metadata" // TODO: rename fix to: image:check-metadata
1543
+ )) {
1544
+ const fileRef = getValidationErrorFileRef(error);
1545
+ if (fileRef) {
1546
+ const filePath = path__default.join(this.cwd, fileRef);
1547
+ let expectedMetadata;
1548
+
1549
+ // if this is a new file or we have an actual FS, we read the file and get the metadata
1550
+ if (!expectedMetadata) {
1551
+ let fileBuffer = undefined;
1552
+ const updatedFileMetadata = fileUpdates[fileRef];
1553
+ if (updatedFileMetadata) {
1554
+ const fileRes = await this.getFiles(fileRef, {
1555
+ sha256: updatedFileMetadata.sha256
1556
+ }, cookies);
1557
+ if (fileRes.status === 200 && fileRes.body) {
1558
+ const res = new Response(fileRes.body);
1559
+ fileBuffer = Buffer.from(await res.arrayBuffer());
1560
+ } else {
1561
+ console.error("Val: unexpected error while fetching image / file:", fileRef, {
1562
+ error: fileRes
1563
+ });
1564
+ }
1565
+ }
1566
+ if (!fileBuffer) {
1567
+ try {
1568
+ fileBuffer = await this.readStaticBinaryFile(filePath);
1569
+ } catch (err) {
1570
+ console.error("Val: unexpected error while reading image / file:", filePath, {
1571
+ error: err
1572
+ });
1573
+ }
1574
+ }
1575
+ if (!fileBuffer) {
1576
+ revalidatedValidationErrors[errorSourcePath].push({
1577
+ message: `Could not read file: ${filePath}`
1578
+ });
1579
+ continue;
1580
+ }
1581
+ if (error.fixes.some(fix => fix === "image:replace-metadata")) {
1582
+ expectedMetadata = await extractImageMetadata(filePath, fileBuffer);
1583
+ } else {
1584
+ expectedMetadata = await extractFileMetadata(filePath, fileBuffer);
1585
+ }
1586
+ }
1587
+ if (!expectedMetadata) {
1588
+ revalidatedValidationErrors[errorSourcePath].push({
1589
+ message: `Could not read file metadata. Is the reference to the file: ${fileRef} correct?`
1590
+ });
1591
+ } else {
1592
+ const actualMetadata = getValidationErrorMetadata(error);
1593
+ const revalidatedError = validateMetadata(actualMetadata, expectedMetadata);
1594
+ if (!revalidatedError) {
1595
+ // no errors anymore:
1596
+ continue;
1597
+ }
1598
+ const errorMsgs = (revalidatedError.globalErrors || []).concat(Object.values(revalidatedError.erroneousMetadata || {})).concat(Object.values(revalidatedError.missingMetadata || []).map(missingKey => {
1599
+ var _expectedMetadata;
1600
+ return `Required key: '${missingKey}' is not defined. Should be: '${JSON.stringify((_expectedMetadata = expectedMetadata) === null || _expectedMetadata === void 0 ? void 0 : _expectedMetadata[missingKey])}'`;
1601
+ }));
1602
+ revalidatedValidationErrors[errorSourcePath].push(...errorMsgs.map(message => ({
1603
+ message
1604
+ })));
1605
+ }
1606
+ } else {
1607
+ revalidatedValidationErrors[errorSourcePath].push(error);
1608
+ }
1609
+ } else {
1610
+ revalidatedValidationErrors[errorSourcePath].push(error);
1611
+ }
1612
+ }
1613
+ }
1614
+ const hasErrors = Object.values(revalidatedValidationErrors).some(errors => errors.length > 0);
1615
+ if (hasErrors) {
1616
+ return revalidatedValidationErrors;
1617
+ }
1618
+ return hasErrors;
1347
1619
  }
1348
- async getPatches() {
1349
- return this.badRequest();
1620
+ sortPatchIds(patchesByModule) {
1621
+ return Object.values(patchesByModule).flatMap(modulePatches => modulePatches).sort((a, b) => {
1622
+ return a.created_at.localeCompare(b.created_at);
1623
+ }).map(patchData => patchData.patch_id);
1350
1624
  }
1351
- }
1352
1625
 
1353
- function decodeJwt(token, secretKey) {
1354
- const [headerBase64, payloadBase64, signatureBase64, ...rest] = token.split(".");
1355
- if (!headerBase64 || !payloadBase64 || !signatureBase64 || rest.length > 0) {
1356
- console.debug("Invalid JWT: format is not exactly {header}.{payload}.{signature}", token);
1357
- return null;
1626
+ // can be overridden if FS cannot read from static assets / public folder (because of bundlers or what not)
1627
+ async readStaticBinaryFile(filePath) {
1628
+ return fs.promises.readFile(filePath);
1629
+ }
1630
+ async readPatches(cookies) {
1631
+ const res = await this.getPatches({},
1632
+ // {} means no ids, so get all patches
1633
+ cookies);
1634
+ if (res.status === 400 || res.status === 401 || res.status === 403 || res.status === 404 || res.status === 500 || res.status === 501) {
1635
+ return result.err(res);
1636
+ } else if (res.status === 200 || res.status === 201) {
1637
+ const patchesByModule = res.json;
1638
+ const patches = [];
1639
+ const patchIdsByModuleId = {};
1640
+ const patchesById = {};
1641
+ for (const [moduleIdS, modulePatchData] of Object.entries(patchesByModule)) {
1642
+ const moduleId = moduleIdS;
1643
+ patchIdsByModuleId[moduleId] = modulePatchData.map(patch => patch.patch_id);
1644
+ for (const patchData of modulePatchData) {
1645
+ patches.push([patchData.patch_id, moduleId, patchData.patch]);
1646
+ patchesById[patchData.patch_id] = patchData.patch;
1647
+ }
1648
+ }
1649
+ const fileUpdates = {};
1650
+ const sortedPatchIds = this.sortPatchIds(patchesByModule);
1651
+ for (const sortedPatchId of sortedPatchIds) {
1652
+ const patchId = sortedPatchId;
1653
+ for (const op of patchesById[patchId] || []) {
1654
+ if (op.op === "file") {
1655
+ const parsedFileOp = z$1.object({
1656
+ sha256: z$1.string(),
1657
+ mimeType: z$1.string()
1658
+ }).safeParse(op.value);
1659
+ if (!parsedFileOp.success) {
1660
+ return result.err({
1661
+ status: 500,
1662
+ json: {
1663
+ message: "Unexpected error: file op value must be transformed into object",
1664
+ details: {
1665
+ value: "First 200 chars: " + JSON.stringify(op.value).slice(0, 200),
1666
+ patchId
1667
+ }
1668
+ }
1669
+ });
1670
+ }
1671
+ fileUpdates[op.filePath] = {
1672
+ ...parsedFileOp.data
1673
+ };
1674
+ }
1675
+ }
1676
+ }
1677
+ return result.ok({
1678
+ patches,
1679
+ patchIdsByModuleId,
1680
+ patchesById,
1681
+ fileUpdates
1682
+ });
1683
+ } else {
1684
+ return result.err({
1685
+ status: 500,
1686
+ json: {
1687
+ message: "Unknown error"
1688
+ }
1689
+ });
1690
+ }
1358
1691
  }
1359
- try {
1360
- const parsedHeader = JSON.parse(Buffer.from(headerBase64, "base64").toString("utf8"));
1361
- const headerVerification = JwtHeaderSchema.safeParse(parsedHeader);
1362
- if (!headerVerification.success) {
1363
- console.debug("Invalid JWT: invalid header", parsedHeader);
1364
- return null;
1692
+ async validateThenMaybeCommit(rawBody, commit, cookies) {
1693
+ const filterPatchesByModuleIdRes = z$1.object({
1694
+ patches: z$1.record(z$1.array(z$1.string())).optional()
1695
+ }).safeParse(rawBody);
1696
+ if (!filterPatchesByModuleIdRes.success) {
1697
+ return {
1698
+ status: 404,
1699
+ json: {
1700
+ message: "Could not parse body",
1701
+ details: filterPatchesByModuleIdRes.error
1702
+ }
1703
+ };
1365
1704
  }
1366
- if (headerVerification.data.typ !== jwtHeader.typ) {
1367
- console.debug("Invalid JWT: invalid header typ", parsedHeader);
1368
- return null;
1705
+ const res = await this.readPatches(cookies);
1706
+ if (result.isErr(res)) {
1707
+ return res.error;
1369
1708
  }
1370
- if (headerVerification.data.alg !== jwtHeader.alg) {
1371
- console.debug("Invalid JWT: invalid header alg", parsedHeader);
1372
- return null;
1709
+ const {
1710
+ patchIdsByModuleId,
1711
+ patchesById,
1712
+ patches,
1713
+ fileUpdates
1714
+ } = res.value;
1715
+ const validationErrorsByModuleId = {};
1716
+ for (const moduleIdStr of this.getAllModules("/")) {
1717
+ const moduleId = moduleIdStr;
1718
+ const serializedModuleContent = await this.applyAllPatchesThenValidate(moduleId, filterPatchesByModuleIdRes.data.patches ||
1719
+ // TODO: refine to ModuleId and PatchId when parsing
1720
+ patchIdsByModuleId, patchesById, fileUpdates, true, cookies);
1721
+ if (serializedModuleContent.errors) {
1722
+ validationErrorsByModuleId[moduleId] = serializedModuleContent;
1723
+ }
1724
+ }
1725
+ if (Object.keys(validationErrorsByModuleId).length > 0) {
1726
+ const modules = {};
1727
+ for (const [patchId, moduleId] of patches) {
1728
+ if (!modules[moduleId]) {
1729
+ modules[moduleId] = {
1730
+ patches: {
1731
+ applied: []
1732
+ }
1733
+ };
1734
+ }
1735
+ if (validationErrorsByModuleId[moduleId]) {
1736
+ var _modules$moduleId$pat;
1737
+ if (!modules[moduleId].patches.failed) {
1738
+ modules[moduleId].patches.failed = [];
1739
+ }
1740
+ (_modules$moduleId$pat = modules[moduleId].patches.failed) === null || _modules$moduleId$pat === void 0 || _modules$moduleId$pat.push(patchId);
1741
+ } else {
1742
+ modules[moduleId].patches.applied.push(patchId);
1743
+ }
1744
+ }
1745
+ return {
1746
+ status: 200,
1747
+ json: {
1748
+ modules,
1749
+ validationErrors: validationErrorsByModuleId
1750
+ }
1751
+ };
1373
1752
  }
1374
- } catch (err) {
1375
- console.debug("Invalid JWT: could not parse header", err);
1376
- return null;
1377
- }
1378
- if (secretKey) {
1379
- const signature = crypto.createHmac("sha256", secretKey).update(`${headerBase64}.${payloadBase64}`).digest("base64");
1380
- if (signature !== signatureBase64) {
1381
- console.debug("Invalid JWT: invalid signature");
1382
- return null;
1753
+ let modules;
1754
+ if (commit) {
1755
+ modules = await this.execCommit(patches, cookies);
1756
+ } else {
1757
+ modules = await this.getPatchedModules(patches);
1758
+ }
1759
+ return {
1760
+ status: 200,
1761
+ json: {
1762
+ modules,
1763
+ validationErrors: false
1764
+ }
1765
+ };
1766
+ }
1767
+ async getPatchedModules(patches) {
1768
+ const modules = {};
1769
+ for (const [patchId, moduleId] of patches) {
1770
+ if (!modules[moduleId]) {
1771
+ modules[moduleId] = {
1772
+ patches: {
1773
+ applied: []
1774
+ }
1775
+ };
1776
+ }
1777
+ modules[moduleId].patches.applied.push(patchId);
1778
+ }
1779
+ return modules;
1780
+ }
1781
+
1782
+ /* Abstract methods */
1783
+
1784
+ /**
1785
+ * Runs before remoteFS dependent methods (e.g.getModule, ...) are called to make sure that:
1786
+ * 1) The remote FS, if applicable, is initialized
1787
+ * 2) The error is returned via API if the remote FS could not be initialized
1788
+ * */
1789
+
1790
+ /* Abstract endpoints */
1791
+
1792
+ /* Abstract auth endpoints: */
1793
+
1794
+ /* Abstract patch endpoints: */
1795
+ }
1796
+
1797
+ // From slightly modified ChatGPT generated
1798
+ function bufferToReadableStream(buffer) {
1799
+ const stream = new ReadableStream({
1800
+ start(controller) {
1801
+ const chunkSize = 1024; // Adjust the chunk size as needed
1802
+ let offset = 0;
1803
+ function push() {
1804
+ const chunk = buffer.subarray(offset, offset + chunkSize);
1805
+ offset += chunkSize;
1806
+ if (chunk.length > 0) {
1807
+ controller.enqueue(new Uint8Array(chunk));
1808
+ setTimeout(push, 0); // Enqueue the next chunk asynchronously
1809
+ } else {
1810
+ controller.close();
1811
+ }
1812
+ }
1813
+ push();
1814
+ }
1815
+ });
1816
+ return stream;
1817
+ }
1818
+ const ENABLE_COOKIE_VALUE = {
1819
+ value: "true",
1820
+ options: {
1821
+ httpOnly: false,
1822
+ sameSite: "lax"
1823
+ }
1824
+ };
1825
+ function getRedirectUrl(query, overrideHost) {
1826
+ if (typeof query.redirect_to !== "string") {
1827
+ return {
1828
+ status: 400,
1829
+ json: {
1830
+ message: "Missing redirect_to query param"
1831
+ }
1832
+ };
1833
+ }
1834
+ if (overrideHost) {
1835
+ return overrideHost + "?redirect_to=" + encodeURIComponent(query.redirect_to);
1836
+ }
1837
+ return query.redirect_to;
1838
+ }
1839
+ const base64DataAttr = "data:";
1840
+ function getMimeTypeFromBase64(content) {
1841
+ const dataIndex = content.indexOf(base64DataAttr);
1842
+ const base64Index = content.indexOf(";base64,");
1843
+ if (dataIndex > -1 || base64Index > -1) {
1844
+ const mimeType = content.slice(dataIndex + base64DataAttr.length, base64Index);
1845
+ return mimeType;
1846
+ }
1847
+ return null;
1848
+ }
1849
+ function bufferFromDataUrl(dataUrl, contentType) {
1850
+ let base64Data;
1851
+ if (!contentType) {
1852
+ const base64Index = dataUrl.indexOf(";base64,");
1853
+ if (base64Index > -1) {
1854
+ base64Data = dataUrl.slice(base64Index + ";base64,".length);
1855
+ }
1856
+ } else {
1857
+ const dataUrlEncodingHeader = `${base64DataAttr}${contentType};base64,`;
1858
+ if (dataUrl.slice(0, dataUrlEncodingHeader.length) === dataUrlEncodingHeader) {
1859
+ base64Data = dataUrl.slice(dataUrlEncodingHeader.length);
1860
+ }
1861
+ }
1862
+ if (base64Data) {
1863
+ return Buffer.from(base64Data, "base64" // TODO: why does it not work with base64url?
1864
+ );
1865
+ }
1866
+ }
1867
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
1868
+ const COMMON_MIME_TYPES = {
1869
+ aac: "audio/aac",
1870
+ abw: "application/x-abiword",
1871
+ arc: "application/x-freearc",
1872
+ avif: "image/avif",
1873
+ avi: "video/x-msvideo",
1874
+ azw: "application/vnd.amazon.ebook",
1875
+ bin: "application/octet-stream",
1876
+ bmp: "image/bmp",
1877
+ bz: "application/x-bzip",
1878
+ bz2: "application/x-bzip2",
1879
+ cda: "application/x-cdf",
1880
+ csh: "application/x-csh",
1881
+ css: "text/css",
1882
+ csv: "text/csv",
1883
+ doc: "application/msword",
1884
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1885
+ eot: "application/vnd.ms-fontobject",
1886
+ epub: "application/epub+zip",
1887
+ gz: "application/gzip",
1888
+ gif: "image/gif",
1889
+ htm: "text/html",
1890
+ html: "text/html",
1891
+ ico: "image/vnd.microsoft.icon",
1892
+ ics: "text/calendar",
1893
+ jar: "application/java-archive",
1894
+ jpeg: "image/jpeg",
1895
+ jpg: "image/jpeg",
1896
+ js: "text/javascript",
1897
+ json: "application/json",
1898
+ jsonld: "application/ld+json",
1899
+ mid: "audio/midi",
1900
+ midi: "audio/midi",
1901
+ mjs: "text/javascript",
1902
+ mp3: "audio/mpeg",
1903
+ mp4: "video/mp4",
1904
+ mpeg: "video/mpeg",
1905
+ mpkg: "application/vnd.apple.installer+xml",
1906
+ odp: "application/vnd.oasis.opendocument.presentation",
1907
+ ods: "application/vnd.oasis.opendocument.spreadsheet",
1908
+ odt: "application/vnd.oasis.opendocument.text",
1909
+ oga: "audio/ogg",
1910
+ ogv: "video/ogg",
1911
+ ogx: "application/ogg",
1912
+ opus: "audio/opus",
1913
+ otf: "font/otf",
1914
+ png: "image/png",
1915
+ pdf: "application/pdf",
1916
+ php: "application/x-httpd-php",
1917
+ ppt: "application/vnd.ms-powerpoint",
1918
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1919
+ rar: "application/vnd.rar",
1920
+ rtf: "application/rtf",
1921
+ sh: "application/x-sh",
1922
+ svg: "image/svg+xml",
1923
+ tar: "application/x-tar",
1924
+ tif: "image/tiff",
1925
+ tiff: "image/tiff",
1926
+ ts: "video/mp2t",
1927
+ ttf: "font/ttf",
1928
+ txt: "text/plain",
1929
+ vsd: "application/vnd.visio",
1930
+ wav: "audio/wav",
1931
+ weba: "audio/webm",
1932
+ webm: "video/webm",
1933
+ webp: "image/webp",
1934
+ woff: "font/woff",
1935
+ woff2: "font/woff2",
1936
+ xhtml: "application/xhtml+xml",
1937
+ xls: "application/vnd.ms-excel",
1938
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1939
+ xml: "application/xml",
1940
+ xul: "application/vnd.mozilla.xul+xml",
1941
+ zip: "application/zip",
1942
+ "3gp": "video/3gpp; audio/3gpp if it doesn't contain video",
1943
+ "3g2": "video/3gpp2; audio/3gpp2 if it doesn't contain video",
1944
+ "7z": "application/x-7z-compressed"
1945
+ };
1946
+ function guessMimeTypeFromPath(filePath) {
1947
+ const fileExt = filePath.split(".").pop();
1948
+ if (fileExt) {
1949
+ return COMMON_MIME_TYPES[fileExt.toLowerCase()] || null;
1950
+ }
1951
+ return null;
1952
+ }
1953
+
1954
+ const textEncoder = new TextEncoder();
1955
+ class LocalValServer extends ValServer {
1956
+ static PATCHES_DIR = "patches";
1957
+ static FILES_DIR = "files";
1958
+ constructor(options, callbacks) {
1959
+ super(options.service.sourceFileHandler.projectRoot, options.service.sourceFileHandler.host, options, callbacks);
1960
+ this.options = options;
1961
+ this.callbacks = callbacks;
1962
+ this.patchesRootPath = options.cacheDir || path__default.join(options.service.sourceFileHandler.projectRoot, ".val");
1963
+ }
1964
+ async session() {
1965
+ return {
1966
+ status: 200,
1967
+ json: {
1968
+ mode: "local",
1969
+ enabled: await this.callbacks.isEnabled()
1970
+ }
1971
+ };
1972
+ }
1973
+ async deletePatches(query) {
1974
+ const deletedPatches = [];
1975
+ for (const patchId of query.id ?? []) {
1976
+ const rawPatchFileContent = this.host.readFile(this.getPatchFilePath(patchId));
1977
+ if (!rawPatchFileContent) {
1978
+ console.warn("Val: Patch not found", patchId);
1979
+ continue;
1980
+ }
1981
+ const parsedPatchesRes = z$1.record(Patch).safeParse(JSON.parse(rawPatchFileContent));
1982
+ if (!parsedPatchesRes.success) {
1983
+ console.warn("Val: Could not parse patch file", patchId, parsedPatchesRes.error);
1984
+ continue;
1985
+ }
1986
+ const files = Object.values(parsedPatchesRes.data).flatMap(ops => ops.filter(isCachedPatchFileOp).map(op => ({
1987
+ filePath: op.filePath,
1988
+ sha256: op.value.sha256
1989
+ })));
1990
+ for (const file of files) {
1991
+ this.host.rmFile(this.getFilePath(file.filePath, file.sha256));
1992
+ this.host.rmFile(this.getFileMetadataPath(file.filePath, file.sha256));
1993
+ }
1994
+ this.host.rmFile(path__default.join(this.patchesRootPath, LocalValServer.PATCHES_DIR, patchId));
1995
+ deletedPatches.push(patchId);
1996
+ }
1997
+ return {
1998
+ status: 200,
1999
+ json: deletedPatches
2000
+ };
2001
+ }
2002
+ async postPatches(body) {
2003
+ const patches = z$1.record(Patch).safeParse(body);
2004
+ if (!patches.success) {
2005
+ return {
2006
+ status: 404,
2007
+ json: {
2008
+ message: `Invalid patch: ${patches.error.message}`,
2009
+ details: patches.error.issues
2010
+ }
2011
+ };
2012
+ }
2013
+ let fileId = Date.now();
2014
+ while (this.host.fileExists(this.getPatchFilePath(fileId.toString()))) {
2015
+ // ensure unique file / patch id
2016
+ fileId++;
2017
+ }
2018
+ const patchId = fileId.toString();
2019
+ const res = {};
2020
+ const parsedPatches = {};
2021
+ for (const moduleIdStr in patches.data) {
2022
+ const moduleId = moduleIdStr; // TODO: validate that this is a valid module id
2023
+ res[moduleId] = {
2024
+ patch_id: patchId
2025
+ };
2026
+ parsedPatches[moduleId] = [];
2027
+ for (const op of patches.data[moduleId]) {
2028
+ // We do not want to include value of a file op in the patch as they potentially contain a lot of data,
2029
+ // therefore we store the file in a separate file and only store the sha256 hash in the patch.
2030
+ // I.e. the patch that frontend sends is not the same as the one stored.
2031
+ // Large amount of text is one thing, but one could easily imagine a lot of patches being accumulated over time with a lot of images which would then consume a non-negligible amount of memory.
2032
+ //
2033
+ // This is potentially confusing for us working on Val internals, however, the alternative was expected to cause a lot of issues down the line: low performance, a lot of data moved, etc
2034
+ // In the worst scenario we imagine this being potentially crashing the server runtime, especially on smaller edge runtimes.
2035
+ // Potential crashes are bad enough to warrant this workaround.
2036
+ if (Internal.isFileOp(op)) {
2037
+ const sha256 = Internal.getSHA256Hash(textEncoder.encode(op.value));
2038
+ const mimeType = getMimeTypeFromBase64(op.value);
2039
+ if (!mimeType) {
2040
+ console.error("Val: Cannot determine mimeType from base64 data", op);
2041
+ throw Error("Cannot determine mimeType from base64 data: " + op.filePath);
2042
+ }
2043
+ const buffer = bufferFromDataUrl(op.value, mimeType);
2044
+ if (!buffer) {
2045
+ console.error("Val: Cannot parse base64 data", op);
2046
+ throw Error("Cannot parse base64 data: " + op.filePath);
2047
+ }
2048
+ this.host.writeFile(this.getFilePath(op.filePath, sha256), buffer, "binary");
2049
+ this.host.writeFile(this.getFileMetadataPath(op.filePath, sha256), JSON.stringify({
2050
+ mimeType,
2051
+ sha256,
2052
+ // useful for debugging / manual inspection
2053
+ patchId,
2054
+ createdAt: new Date().toISOString()
2055
+ }, null, 2), "utf8");
2056
+ parsedPatches[moduleId].push({
2057
+ ...op,
2058
+ value: {
2059
+ sha256,
2060
+ mimeType
2061
+ }
2062
+ });
2063
+ } else {
2064
+ parsedPatches[moduleId].push(op);
2065
+ }
2066
+ }
2067
+ }
2068
+ this.host.writeFile(this.getPatchFilePath(patchId), JSON.stringify(parsedPatches), "utf8");
2069
+ return {
2070
+ status: 200,
2071
+ json: res
2072
+ };
2073
+ }
2074
+ async getFiles(filePath, query) {
2075
+ if (query.sha256) {
2076
+ const fileExists = this.host.fileExists(this.getFilePath(filePath, query.sha256));
2077
+ if (fileExists) {
2078
+ const metadataFileContent = this.host.readFile(this.getFileMetadataPath(filePath, query.sha256));
2079
+ const fileContent = await this.readStaticBinaryFile(this.getFilePath(filePath, query.sha256));
2080
+ if (!fileContent) {
2081
+ throw Error("Could not read cached patch file / asset. Cache corrupted?");
2082
+ }
2083
+ if (!metadataFileContent) {
2084
+ throw Error("Missing metadata of cached patch file / asset. Cache corrupted?");
2085
+ }
2086
+ const metadata = JSON.parse(metadataFileContent);
2087
+ return {
2088
+ status: 200,
2089
+ headers: {
2090
+ "Content-Type": metadata.mimeType,
2091
+ "Content-Length": fileContent.byteLength.toString()
2092
+ },
2093
+ body: bufferToReadableStream(fileContent)
2094
+ };
2095
+ }
2096
+ }
2097
+ const buffer = await this.readStaticBinaryFile(path__default.join(this.cwd, filePath));
2098
+ const mimeType = guessMimeTypeFromPath(filePath) || "application/octet-stream";
2099
+ if (!buffer) {
2100
+ return {
2101
+ status: 404,
2102
+ json: {
2103
+ message: "File not found"
2104
+ }
2105
+ };
2106
+ }
2107
+ return {
2108
+ status: 200,
2109
+ headers: {
2110
+ "Content-Type": mimeType,
2111
+ "Content-Length": buffer.byteLength.toString()
2112
+ },
2113
+ body: bufferToReadableStream(buffer)
2114
+ };
2115
+ }
2116
+ async getPatches(query) {
2117
+ const patchesCacheDir = path__default.join(this.patchesRootPath, LocalValServer.PATCHES_DIR);
2118
+ let files = [];
2119
+ try {
2120
+ if (!this.host.directoryExists || this.host.directoryExists && this.host.directoryExists(patchesCacheDir)) {
2121
+ files = this.host.readDirectory(patchesCacheDir, [""], [], []);
2122
+ }
2123
+ } catch (e) {
2124
+ console.debug("Failed to read directory (no patches yet?)", e);
2125
+ }
2126
+ const res = {};
2127
+ const sortedPatchIds = files.map(file => parseInt(path__default.basename(file), 10)).sort();
2128
+ for (const patchIdStr of sortedPatchIds) {
2129
+ const patchId = patchIdStr.toString();
2130
+ if (query.id && query.id.length > 0 && !query.id.includes(patchId)) {
2131
+ continue;
2132
+ }
2133
+ try {
2134
+ const currentParsedPatches = z$1.record(Patch).safeParse(JSON.parse(this.host.readFile(path__default.join(this.patchesRootPath, LocalValServer.PATCHES_DIR, `${patchId}`)) || ""));
2135
+ if (!currentParsedPatches.success) {
2136
+ const msg = "Unexpected error reading patch. Patch did not parse correctly. Is there a mismatch in Val versions? Perhaps Val is misconfigured?";
2137
+ console.error(`Val: ${msg}`, {
2138
+ patchId,
2139
+ error: currentParsedPatches.error
2140
+ });
2141
+ return {
2142
+ status: 500,
2143
+ json: {
2144
+ message: msg,
2145
+ details: {
2146
+ patchId,
2147
+ error: currentParsedPatches.error
2148
+ }
2149
+ }
2150
+ };
2151
+ }
2152
+ const createdAt = patchId;
2153
+ for (const moduleIdStr in currentParsedPatches.data) {
2154
+ const moduleId = moduleIdStr;
2155
+ if (!res[moduleId]) {
2156
+ res[moduleId] = [];
2157
+ }
2158
+ res[moduleId].push({
2159
+ patch: currentParsedPatches.data[moduleId],
2160
+ patch_id: patchId,
2161
+ created_at: new Date(Number(createdAt)).toISOString()
2162
+ });
2163
+ }
2164
+ } catch (err) {
2165
+ const msg = `Unexpected error while reading patch file. The cache may be corrupted or Val may be misconfigured. Try deleting the cache directory.`;
2166
+ console.error(`Val: ${msg}`, {
2167
+ patchId,
2168
+ error: err,
2169
+ dir: this.patchesRootPath
2170
+ });
2171
+ return {
2172
+ status: 500,
2173
+ json: {
2174
+ message: msg,
2175
+ details: {
2176
+ patchId,
2177
+ error: err === null || err === void 0 ? void 0 : err.toString()
2178
+ }
2179
+ }
2180
+ };
2181
+ }
2182
+ }
2183
+ return {
2184
+ status: 200,
2185
+ json: res
2186
+ };
2187
+ }
2188
+ getFilePath(filename, sha256) {
2189
+ return path__default.join(this.patchesRootPath, LocalValServer.FILES_DIR, filename, sha256, "file");
2190
+ }
2191
+ getFileMetadataPath(filename, sha256) {
2192
+ return path__default.join(this.patchesRootPath, LocalValServer.FILES_DIR, filename, sha256, "metadata.json");
2193
+ }
2194
+ getPatchFilePath(patchId) {
2195
+ return path__default.join(this.patchesRootPath, LocalValServer.PATCHES_DIR, patchId.toString());
2196
+ }
2197
+ badRequest() {
2198
+ return {
2199
+ status: 400,
2200
+ json: {
2201
+ message: "Local server does not handle this request"
2202
+ }
2203
+ };
2204
+ }
2205
+ async ensureRemoteFSInitialized() {
2206
+ // No RemoteFS so nothing to ensure
2207
+ return result.ok(undefined);
2208
+ }
2209
+ getModule(moduleId) {
2210
+ return this.options.service.get(moduleId);
2211
+ }
2212
+ async execCommit(patches) {
2213
+ for (const [patchId, moduleId, patch] of patches) {
2214
+ // TODO: patch the entire module content directly by using a { path: "", op: "replace", value: patchedData }?
2215
+ // Reason: that would be more atomic? Not doing it now, because there are currently already too many moving pieces.
2216
+ // Other things we could do would be to patch in a temp directory and ONLY when all patches are applied we move back in.
2217
+ // This would improve reliability
2218
+ this.host.rmFile(this.getPatchFilePath(patchId));
2219
+ await this.options.service.patch(moduleId, patch);
2220
+ }
2221
+ return this.getPatchedModules(patches);
2222
+ }
2223
+
2224
+ /* Bad requests on Local Server: */
2225
+
2226
+ async authorize() {
2227
+ return this.badRequest();
2228
+ }
2229
+ async callback() {
2230
+ return this.badRequest();
2231
+ }
2232
+ async logout() {
2233
+ return this.badRequest();
2234
+ }
2235
+ }
2236
+ function isCachedPatchFileOp(op) {
2237
+ return !!(op.op === "file" && typeof op.filePath === "string" && op.value && typeof op.value === "object" && !Array.isArray(op.value) && "sha256" in op.value && typeof op.value.sha256 === "string");
2238
+ }
2239
+
2240
+ function decodeJwt(token, secretKey) {
2241
+ const [headerBase64, payloadBase64, signatureBase64, ...rest] = token.split(".");
2242
+ if (!headerBase64 || !payloadBase64 || !signatureBase64 || rest.length > 0) {
2243
+ console.debug("Invalid JWT: format is not exactly {header}.{payload}.{signature}", token);
2244
+ return null;
2245
+ }
2246
+ try {
2247
+ const parsedHeader = JSON.parse(Buffer.from(headerBase64, "base64").toString("utf8"));
2248
+ const headerVerification = JwtHeaderSchema.safeParse(parsedHeader);
2249
+ if (!headerVerification.success) {
2250
+ console.debug("Invalid JWT: invalid header", parsedHeader);
2251
+ return null;
2252
+ }
2253
+ if (headerVerification.data.typ !== jwtHeader.typ) {
2254
+ console.debug("Invalid JWT: invalid header typ", parsedHeader);
2255
+ return null;
2256
+ }
2257
+ if (headerVerification.data.alg !== jwtHeader.alg) {
2258
+ console.debug("Invalid JWT: invalid header alg", parsedHeader);
2259
+ return null;
2260
+ }
2261
+ } catch (err) {
2262
+ console.debug("Invalid JWT: could not parse header", err);
2263
+ return null;
2264
+ }
2265
+ if (secretKey) {
2266
+ const signature = crypto.createHmac("sha256", secretKey).update(`${headerBase64}.${payloadBase64}`).digest("base64");
2267
+ if (signature !== signatureBase64) {
2268
+ console.debug("Invalid JWT: invalid signature");
2269
+ return null;
1383
2270
  }
1384
2271
  }
1385
2272
  try {
@@ -1408,108 +2295,368 @@ function encodeJwt(payload, sessionKey) {
1408
2295
  return `${jwtHeaderBase64}.${payloadBase64}.${crypto.createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
1409
2296
  }
1410
2297
 
1411
- class ProxyValServer {
1412
- constructor(options, callbacks) {
2298
+ const SEPARATOR = "/";
2299
+ class RemoteFS {
2300
+ initialized = false;
2301
+ constructor() {
2302
+ this.data = {};
2303
+ this.modifiedFiles = [];
2304
+ this.deletedFiles = [];
2305
+ }
2306
+ useCaseSensitiveFileNames = true;
2307
+ isInitialized() {
2308
+ return this.initialized;
2309
+ }
2310
+ async initializeWith(data) {
2311
+ this.data = data;
2312
+ this.initialized = true;
2313
+ }
2314
+ async getPendingOperations() {
2315
+ const modified = {};
2316
+ for (const modifiedFile in this.modifiedFiles) {
2317
+ modified[modifiedFile] = this.data[modifiedFile].utf8Files[modifiedFile];
2318
+ }
2319
+ return {
2320
+ modified: modified,
2321
+ deleted: this.deletedFiles
2322
+ };
2323
+ }
2324
+ changedDirectories = {};
2325
+ readDirectory = (rootDir, extensions, excludes, includes, depth) => {
2326
+ // TODO: rewrite this! And make some tests! This is a mess!
2327
+ // Considered using glob which typescript seems to use, but that works on an entire typeof fs
2328
+ // glob uses minimatch internally, so using that instead
2329
+ const files = [];
2330
+ for (const dir in this.data) {
2331
+ const depthExceeded = depth ? dir.replace(rootDir, "").split(SEPARATOR).length > depth : false;
2332
+ if (dir.startsWith(rootDir) && !depthExceeded) {
2333
+ for (const file in this.data[dir].utf8Files) {
2334
+ for (const extension of extensions) {
2335
+ if (file.endsWith(extension)) {
2336
+ const path = `${dir}/${file}`;
2337
+ for (const include of includes ?? []) {
2338
+ // TODO: should default includes be ['**/*']?
2339
+ if (minimatch(path, include)) {
2340
+ let isExcluded = false;
2341
+ for (const exlude of excludes ?? []) {
2342
+ if (minimatch(path, exlude)) {
2343
+ isExcluded = true;
2344
+ break;
2345
+ }
2346
+ }
2347
+ if (!isExcluded) {
2348
+ files.push(path);
2349
+ }
2350
+ }
2351
+ }
2352
+ }
2353
+ }
2354
+ }
2355
+ }
2356
+ }
2357
+ return ts.sys.readDirectory(rootDir, extensions, excludes, includes, depth).concat(files);
2358
+ };
2359
+ writeFile = (filePath, data, encoding) => {
2360
+ // never write real fs
2361
+ const {
2362
+ directory,
2363
+ filename
2364
+ } = RemoteFS.parsePath(filePath);
2365
+ if (this.data[directory] === undefined) {
2366
+ throw new Error(`Directory not found: ${directory}`);
2367
+ }
2368
+ this.changedDirectories[directory] = this.changedDirectories[directory] ?? new Set();
2369
+
2370
+ // if it fails below this should not be added, so maybe a try/catch?
2371
+ this.changedDirectories[directory].add(filename);
2372
+ this.data[directory].utf8Files[filename] = data;
2373
+ this.modifiedFiles.push(filePath);
2374
+ };
2375
+ rmFile(filePath) {
2376
+ // never remove from real fs
2377
+ const {
2378
+ directory,
2379
+ filename
2380
+ } = RemoteFS.parsePath(filePath);
2381
+ if (this.data[directory] === undefined) {
2382
+ throw new Error(`Directory not found: ${directory}`);
2383
+ }
2384
+ this.changedDirectories[directory] = this.changedDirectories[directory] ?? new Set();
2385
+
2386
+ // if it fails below this should not be added, so maybe a try/catch?
2387
+ this.changedDirectories[directory].add(filename);
2388
+ delete this.data[directory].utf8Files[filename];
2389
+ delete this.data[directory].symlinks[filename];
2390
+ this.deletedFiles.push(filePath);
2391
+ }
2392
+ fileExists = filePath => {
2393
+ var _this$data$directory;
2394
+ if (ts.sys.fileExists(filePath)) {
2395
+ return true;
2396
+ }
2397
+ const {
2398
+ directory,
2399
+ filename
2400
+ } = RemoteFS.parsePath(this.realpath(filePath) // ts.sys seems to resolve symlinks while calling fileExists, i.e. a broken symlink (pointing to a non-existing file) is not considered to exist
2401
+ );
2402
+ return !!((_this$data$directory = this.data[directory]) !== null && _this$data$directory !== void 0 && _this$data$directory.utf8Files[filename]);
2403
+ };
2404
+ readFile = filePath => {
2405
+ const realFile = ts.sys.readFile(filePath);
2406
+ if (realFile !== undefined) {
2407
+ return realFile;
2408
+ }
2409
+ const {
2410
+ directory,
2411
+ filename
2412
+ } = RemoteFS.parsePath(filePath);
2413
+ const dirNode = this.data[directory];
2414
+ if (!dirNode) {
2415
+ return undefined;
2416
+ }
2417
+ const content = dirNode.utf8Files[filename];
2418
+ return content;
2419
+ };
2420
+ realpath(fullPath) {
2421
+ if (ts.sys.fileExists(fullPath) && ts.sys.realpath) {
2422
+ return ts.sys.realpath(fullPath);
2423
+ }
2424
+ // TODO: this only works in a very limited way.
2425
+ // It does not support symlinks to symlinks nor symlinked directories for instance.
2426
+ const {
2427
+ directory,
2428
+ filename
2429
+ } = RemoteFS.parsePath(fullPath);
2430
+ if (this.data[directory] === undefined) {
2431
+ return fullPath;
2432
+ }
2433
+ if (this.data[directory].utf8Files[filename] === undefined) {
2434
+ const link = this.data[directory].symlinks[filename];
2435
+ if (link === undefined) {
2436
+ return fullPath;
2437
+ } else {
2438
+ return link;
2439
+ }
2440
+ } else {
2441
+ return path__default.join(directory, filename);
2442
+ }
2443
+ }
2444
+
2445
+ /**
2446
+ *
2447
+ * @param path
2448
+ * @returns directory and filename. NOTE: directory might be empty string
2449
+ */
2450
+ static parsePath(path) {
2451
+ const pathParts = path.split(SEPARATOR);
2452
+ const filename = pathParts.pop();
2453
+ if (!filename) {
2454
+ throw new Error(`Invalid path: '${path}'. Node filename: '${filename}'`);
2455
+ }
2456
+ const directory = pathParts.join(SEPARATOR);
2457
+ return {
2458
+ directory,
2459
+ filename
2460
+ };
2461
+ }
2462
+ }
2463
+
2464
+ /**
2465
+ * Represents directories
2466
+ * NOTE: the keys of directory nodes are the "full" path, i.e. "foo/bar"
2467
+ * NOTE: the keys of file nodes are the "filename" only, i.e. "baz.txt"
2468
+ *
2469
+ * @example
2470
+ * {
2471
+ * "foo/bar": { // <- directory. NOTE: this is the "full" path
2472
+ * gitHubSha: "123",
2473
+ * files: {
2474
+ * "baz.txt": "hello world" // <- file. NOTE: this is the "filename" only
2475
+ * },
2476
+ * },
2477
+ * };
2478
+ */
2479
+ // TODO: a Map would be better here
2480
+
2481
+ class ProxyValServer extends ValServer {
2482
+ constructor(cwd, options, apiOptions, callbacks) {
2483
+ const remoteFS = new RemoteFS();
2484
+ super(cwd, remoteFS, options, callbacks);
2485
+ this.cwd = cwd;
1413
2486
  this.options = options;
2487
+ this.apiOptions = apiOptions;
1414
2488
  this.callbacks = callbacks;
2489
+ this.remoteFS = remoteFS;
1415
2490
  }
1416
- async getFiles(treePath, query, cookies) {
1417
- return this.withAuth(cookies, "getFiles", async data => {
1418
- const url = new URL(`/v1/files/${this.options.valName}${treePath}`, this.options.valContentUrl);
1419
- if (typeof query.sha256 === "string") {
1420
- url.searchParams.append("sha256", query.sha256);
1421
- } else {
1422
- console.warn("Missing sha256 query param");
2491
+
2492
+ /** Remote FS dependent methods: */
2493
+
2494
+ async getModule(moduleId) {
2495
+ if (!this.lazyService) {
2496
+ this.lazyService = await createService(this.cwd, this.apiOptions, this.remoteFS);
2497
+ }
2498
+ return this.lazyService.get(moduleId);
2499
+ }
2500
+ execCommit(patches, cookies) {
2501
+ return withAuth(this.options.valSecret, cookies, "execCommit", async ({
2502
+ token
2503
+ }) => {
2504
+ const commit = this.options.git.commit;
2505
+ if (!commit) {
2506
+ return {
2507
+ status: 400,
2508
+ json: {
2509
+ message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
2510
+ }
2511
+ };
1423
2512
  }
2513
+ const params = `commit=${encodeURIComponent(commit)}`;
2514
+ const url = new URL(`/v1/commit/${this.options.valName}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
2515
+
2516
+ // Creates a fresh copy of the fs. We cannot touch the existing fs, since we might still want to do
2517
+ // other operations on it.
2518
+ // We could perhaps free up the other fs while doing this operation, but uncertain if we can actually do that and if that would actually help on memory.
2519
+ // It is a concern we have, since we might be using quite a lot of memory when having the whole FS in memory. In particular because of images / files.
2520
+ const remoteFS = new RemoteFS();
2521
+ const initRes = await this.initRemoteFS(commit, remoteFS, token);
2522
+ if (initRes.status !== 200) {
2523
+ return initRes;
2524
+ }
2525
+ const service = await createService(this.cwd, this.apiOptions, remoteFS);
2526
+ for (const [, moduleId, patch] of patches) {
2527
+ await service.patch(moduleId, patch);
2528
+ }
2529
+ const fileOps = await remoteFS.getPendingOperations();
1424
2530
  const fetchRes = await fetch(url, {
1425
- headers: this.getAuthHeaders(data.token)
2531
+ method: "POST",
2532
+ headers: getAuthHeaders(token, "application/json"),
2533
+ body: JSON.stringify({
2534
+ fileOps
2535
+ })
1426
2536
  });
1427
2537
  if (fetchRes.status === 200) {
1428
- if (fetchRes.body) {
1429
- return {
1430
- status: fetchRes.status,
1431
- headers: {
1432
- "Content-Type": fetchRes.headers.get("Content-Type") || "",
1433
- "Content-Length": fetchRes.headers.get("Content-Length") || "0"
1434
- },
1435
- json: fetchRes.body
1436
- };
1437
- } else {
1438
- return {
1439
- status: 500,
1440
- json: {
1441
- message: "No body in response"
1442
- }
1443
- };
1444
- }
2538
+ return {
2539
+ status: fetchRes.status,
2540
+ json: await fetchRes.json()
2541
+ };
1445
2542
  } else {
2543
+ console.error("Failed to get patches", fetchRes.status, await fetchRes.text());
1446
2544
  return {
1447
2545
  status: fetchRes.status,
1448
2546
  json: {
1449
- message: "Failed to get files"
2547
+ message: "Failed to get patches"
1450
2548
  }
1451
2549
  };
1452
2550
  }
1453
2551
  });
1454
2552
  }
1455
- async authorize(query) {
1456
- if (typeof query.redirect_to !== "string") {
2553
+ async initRemoteFS(commit, remoteFS, token) {
2554
+ const params = new URLSearchParams(this.apiOptions.root ? {
2555
+ root: this.apiOptions.root,
2556
+ commit,
2557
+ cwd: this.cwd
2558
+ } : {
2559
+ commit,
2560
+ cwd: this.cwd
2561
+ });
2562
+ const url = new URL(`/v1/fs/${this.options.valName}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
2563
+ try {
2564
+ const fetchRes = await fetch(url, {
2565
+ headers: getAuthHeaders(token, "application/json")
2566
+ });
2567
+ if (fetchRes.status === 200) {
2568
+ const json = await fetchRes.json();
2569
+ remoteFS.initializeWith(json);
2570
+ return {
2571
+ status: 200
2572
+ };
2573
+ } else {
2574
+ try {
2575
+ var _fetchRes$headers$get;
2576
+ if ((_fetchRes$headers$get = fetchRes.headers.get("Content-Type")) !== null && _fetchRes$headers$get !== void 0 && _fetchRes$headers$get.includes("application/json")) {
2577
+ const json = await fetchRes.json();
2578
+ return {
2579
+ status: fetchRes.status,
2580
+ json: {
2581
+ message: "Failed to fetch remote files",
2582
+ details: json
2583
+ }
2584
+ };
2585
+ }
2586
+ } catch (err) {
2587
+ console.error(err);
2588
+ }
2589
+ return {
2590
+ status: fetchRes.status,
2591
+ json: {
2592
+ message: "Unknown failure while fetching remote files"
2593
+ }
2594
+ };
2595
+ }
2596
+ } catch (err) {
1457
2597
  return {
1458
- status: 400,
2598
+ status: 500,
1459
2599
  json: {
1460
- message: "Missing redirect_to query param"
2600
+ message: "Failed to fetch: check network connection"
1461
2601
  }
1462
2602
  };
1463
2603
  }
1464
- const token = crypto.randomUUID();
1465
- const redirectUrl = new URL(query.redirect_to);
1466
- const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
1467
- return {
1468
- cookies: {
1469
- [VAL_STATE_COOKIE$1]: {
1470
- value: createStateCookie({
1471
- redirect_to: query.redirect_to,
1472
- token
1473
- }),
1474
- options: {
1475
- httpOnly: true,
1476
- sameSite: "lax",
1477
- expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
1478
- }
1479
- }
1480
- },
1481
- status: 302,
1482
- redirectTo: appAuthorizeUrl
1483
- };
1484
2604
  }
1485
- async enable(query) {
1486
- const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
1487
- if (typeof redirectToRes !== "string") {
1488
- return redirectToRes;
2605
+ async ensureRemoteFSInitialized(errorMessageType, cookies) {
2606
+ const commit = this.options.git.commit;
2607
+ if (!commit) {
2608
+ return result.err({
2609
+ status: 400,
2610
+ json: {
2611
+ message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
2612
+ }
2613
+ });
2614
+ }
2615
+ const res = await withAuth(this.options.valSecret, cookies, errorMessageType, async data => {
2616
+ if (!this.remoteFS.isInitialized()) {
2617
+ return this.initRemoteFS(commit, this.remoteFS, data.token);
2618
+ } else {
2619
+ return {
2620
+ status: 200
2621
+ };
2622
+ }
2623
+ });
2624
+ if (res.status === 200) {
2625
+ return result.ok(undefined);
2626
+ } else {
2627
+ return result.err(res);
1489
2628
  }
1490
- await this.callbacks.onEnable(true);
1491
- return {
1492
- cookies: {
1493
- [VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
1494
- },
1495
- status: 302,
1496
- redirectTo: redirectToRes
1497
- };
1498
2629
  }
1499
- async disable(query) {
1500
- const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
1501
- if (typeof redirectToRes !== "string") {
1502
- return redirectToRes;
2630
+ /* Auth endpoints */
2631
+
2632
+ async authorize(query) {
2633
+ if (typeof query.redirect_to !== "string") {
2634
+ return {
2635
+ status: 400,
2636
+ json: {
2637
+ message: "Missing redirect_to query param"
2638
+ }
2639
+ };
1503
2640
  }
1504
- await this.callbacks.onDisable(true);
2641
+ const token = crypto.randomUUID();
2642
+ const redirectUrl = new URL(query.redirect_to);
2643
+ const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
1505
2644
  return {
1506
2645
  cookies: {
1507
- [VAL_ENABLE_COOKIE_NAME]: {
1508
- value: null
2646
+ [VAL_STATE_COOKIE$1]: {
2647
+ value: createStateCookie({
2648
+ redirect_to: query.redirect_to,
2649
+ token
2650
+ }),
2651
+ options: {
2652
+ httpOnly: true,
2653
+ sameSite: "lax",
2654
+ expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
2655
+ }
1509
2656
  }
1510
2657
  },
1511
2658
  status: 302,
1512
- redirectTo: redirectToRes
2659
+ redirectTo: appAuthorizeUrl
1513
2660
  };
1514
2661
  }
1515
2662
  async callback(query, cookies) {
@@ -1579,51 +2726,11 @@ class ProxyValServer {
1579
2726
  }
1580
2727
  };
1581
2728
  }
1582
- async withAuth(cookies, errorMessageType, handler) {
1583
- const cookie = cookies[VAL_SESSION_COOKIE$1];
1584
- if (typeof cookie === "string") {
1585
- const decodedToken = decodeJwt(cookie, this.options.valSecret);
1586
- if (!decodedToken) {
1587
- return {
1588
- status: 401,
1589
- json: {
1590
- message: "Invalid JWT token"
1591
- }
1592
- };
1593
- }
1594
- const verification = IntegratedServerJwtPayload.safeParse(decodedToken);
1595
- if (!verification.success) {
1596
- return {
1597
- status: 401,
1598
- json: {
1599
- message: "Could not parse JWT",
1600
- details: verification.error
1601
- }
1602
- };
1603
- }
1604
- return handler(verification.data).catch(err => {
1605
- console.error(`Failed while processing: ${errorMessageType}`, err);
1606
- return {
1607
- status: 500,
1608
- json: {
1609
- message: err.message
1610
- }
1611
- };
1612
- });
1613
- } else {
1614
- return {
1615
- status: 401,
1616
- json: {
1617
- message: "No token"
1618
- }
1619
- };
1620
- }
1621
- }
1622
2729
  async session(cookies) {
1623
- return this.withAuth(cookies, "session", async data => {
2730
+ return withAuth(this.options.valSecret, cookies, "session", async data => {
1624
2731
  const url = new URL(`/api/val/${this.options.valName}/auth/session`, this.options.valBuildUrl);
1625
2732
  const fetchRes = await fetch(url, {
1626
- headers: this.getAuthHeaders(data.token, "application/json")
2733
+ headers: getAuthHeaders(data.token, "application/json")
1627
2734
  });
1628
2735
  if (fetchRes.status === 200) {
1629
2736
  return {
@@ -1645,73 +2752,81 @@ class ProxyValServer {
1645
2752
  }
1646
2753
  });
1647
2754
  }
1648
- async getTree(treePath, query, cookies) {
1649
- return this.withAuth(cookies, "getTree", async data => {
1650
- const {
1651
- patch,
1652
- schema,
1653
- source
1654
- } = query;
1655
- const commit = this.options.gitCommit;
1656
- if (!commit) {
2755
+ async consumeCode(code) {
2756
+ const url = new URL(`/api/val/${this.options.valName}/auth/token`, this.options.valBuildUrl);
2757
+ url.searchParams.set("code", encodeURIComponent(code));
2758
+ return fetch(url, {
2759
+ method: "POST",
2760
+ headers: getAuthHeaders(this.options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
2761
+ }).then(async res => {
2762
+ if (res.status === 200) {
2763
+ const token = await res.text();
2764
+ const verification = ValAppJwtPayload.safeParse(decodeJwt(token));
2765
+ if (!verification.success) {
2766
+ return null;
2767
+ }
1657
2768
  return {
1658
- status: 400,
1659
- json: {
1660
- message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
1661
- }
2769
+ ...verification.data,
2770
+ token
1662
2771
  };
2772
+ } else {
2773
+ console.debug("Failed to get data from code: ", res.status);
2774
+ return null;
1663
2775
  }
1664
- const params = new URLSearchParams({
1665
- patch: (patch === "true").toString(),
1666
- schema: (schema === "true").toString(),
1667
- source: (source === "true").toString(),
1668
- commit
2776
+ }).catch(err => {
2777
+ console.debug("Failed to get user from code: ", err);
2778
+ return null;
2779
+ });
2780
+ }
2781
+ getAuthorizeUrl(publicValApiRoute, token) {
2782
+ const url = new URL(`/auth/${this.options.valName}/authorize`, this.options.valBuildUrl);
2783
+ url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRoute}/callback`));
2784
+ url.searchParams.set("state", token);
2785
+ return url.toString();
2786
+ }
2787
+ getAppErrorUrl(error) {
2788
+ const url = new URL(`/auth/${this.options.valName}/authorize`, this.options.valBuildUrl);
2789
+ url.searchParams.set("error", encodeURIComponent(error));
2790
+ return url.toString();
2791
+ }
2792
+
2793
+ /* Patch endpoints */
2794
+ async deletePatches(
2795
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2796
+ query,
2797
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2798
+ cookies) {
2799
+ return withAuth(this.options.valSecret, cookies, "deletePatches", async ({
2800
+ token
2801
+ }) => {
2802
+ const patchIds = query.id || [];
2803
+ const params = `${patchIds.map(id => `id=${encodeURIComponent(id)}`).join("&")}`;
2804
+ const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
2805
+ const fetchRes = await fetch(url, {
2806
+ method: "GET",
2807
+ headers: getAuthHeaders(token, "application/json")
1669
2808
  });
1670
- const url = new URL(`/v1/tree/${this.options.valName}/heads/${this.options.gitBranch}/~${treePath}/?${params}`, this.options.valContentUrl);
1671
- try {
1672
- const fetchRes = await fetch(url, {
1673
- headers: this.getAuthHeaders(data.token, "application/json")
1674
- });
1675
- if (fetchRes.status === 200) {
1676
- return {
1677
- status: fetchRes.status,
1678
- json: await fetchRes.json()
1679
- };
1680
- } else {
1681
- try {
1682
- var _fetchRes$headers$get;
1683
- if ((_fetchRes$headers$get = fetchRes.headers.get("Content-Type")) !== null && _fetchRes$headers$get !== void 0 && _fetchRes$headers$get.includes("application/json")) {
1684
- const json = await fetchRes.json();
1685
- return {
1686
- status: fetchRes.status,
1687
- json
1688
- };
1689
- }
1690
- } catch (err) {
1691
- console.error(err);
1692
- }
1693
- return {
1694
- status: fetchRes.status,
1695
- json: {
1696
- message: "Unknown failure while accessing Val"
1697
- }
1698
- };
1699
- }
1700
- } catch (err) {
2809
+ if (fetchRes.status === 200) {
1701
2810
  return {
1702
- status: 500,
2811
+ status: fetchRes.status,
2812
+ json: await fetchRes.json()
2813
+ };
2814
+ } else {
2815
+ console.error("Failed to delete patches", fetchRes.status, await fetchRes.text());
2816
+ return {
2817
+ status: fetchRes.status,
1703
2818
  json: {
1704
- message: "Failed to fetch: check network connection"
2819
+ message: "Failed to delete patches"
1705
2820
  }
1706
2821
  };
1707
2822
  }
1708
2823
  });
1709
2824
  }
1710
2825
  async getPatches(query, cookies) {
1711
- return this.withAuth(cookies, "getPatches", async ({
2826
+ return withAuth(this.options.valSecret, cookies, "getPatches", async ({
1712
2827
  token
1713
2828
  }) => {
1714
- const commit = this.options.gitCommit;
2829
+ const commit = this.options.git.commit;
1715
2830
  if (!commit) {
1716
2831
  return {
1717
2832
  status: 400,
@@ -1722,11 +2837,11 @@ class ProxyValServer {
1722
2837
  }
1723
2838
  const patchIds = query.id || [];
1724
2839
  const params = patchIds.length > 0 ? `commit=${encodeURIComponent(commit)}&${patchIds.map(id => `id=${encodeURIComponent(id)}`).join("&")}` : `commit=${encodeURIComponent(commit)}`;
1725
- const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.gitBranch}/~?${params}`, this.options.valContentUrl);
2840
+ const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
1726
2841
  // Proxy patch to val.build
1727
2842
  const fetchRes = await fetch(url, {
1728
2843
  method: "GET",
1729
- headers: this.getAuthHeaders(token, "application/json")
2844
+ headers: getAuthHeaders(token, "application/json")
1730
2845
  });
1731
2846
  if (fetchRes.status === 200) {
1732
2847
  return {
@@ -1734,6 +2849,7 @@ class ProxyValServer {
1734
2849
  json: await fetchRes.json()
1735
2850
  };
1736
2851
  } else {
2852
+ console.error("Failed to get patches", fetchRes.status, await fetchRes.text());
1737
2853
  return {
1738
2854
  status: fetchRes.status,
1739
2855
  json: {
@@ -1744,7 +2860,7 @@ class ProxyValServer {
1744
2860
  });
1745
2861
  }
1746
2862
  async postPatches(body, cookies) {
1747
- const commit = this.options.gitCommit;
2863
+ const commit = this.options.git.commit;
1748
2864
  if (!commit) {
1749
2865
  return {
1750
2866
  status: 401,
@@ -1756,34 +2872,27 @@ class ProxyValServer {
1756
2872
  const params = new URLSearchParams({
1757
2873
  commit
1758
2874
  });
1759
- return this.withAuth(cookies, "postPatches", async ({
2875
+ return withAuth(this.options.valSecret, cookies, "postPatches", async ({
1760
2876
  token
1761
2877
  }) => {
1762
2878
  // First validate that the body has the right structure
1763
- const patchJSON = z$1.record(PatchJSON).safeParse(body);
1764
- if (!patchJSON.success) {
2879
+ const parsedPatches = z$1.record(Patch).safeParse(body);
2880
+ if (!parsedPatches.success) {
1765
2881
  return {
1766
2882
  status: 400,
1767
2883
  json: {
1768
- message: "Invalid patch",
1769
- details: patchJSON.error.issues
2884
+ message: "Invalid patch(es)",
2885
+ details: parsedPatches.error.issues
1770
2886
  }
1771
2887
  };
1772
2888
  }
1773
- // Then parse/validate
1774
- // TODO:
1775
- const patch = patchJSON.data;
1776
- // const patch = parsePatch(patchJSON.data);
1777
- // if (result.isErr(patch)) {
1778
- // res.status(401).json(patch.error);
1779
- // return;
1780
- // }
1781
- const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.gitBranch}/~?${params}`, this.options.valContentUrl);
2889
+ const patches = parsedPatches.data;
2890
+ const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
1782
2891
  // Proxy patch to val.build
1783
2892
  const fetchRes = await fetch(url, {
1784
2893
  method: "POST",
1785
- headers: this.getAuthHeaders(token, "application/json"),
1786
- body: JSON.stringify(patch)
2894
+ headers: getAuthHeaders(token, "application/json"),
2895
+ body: JSON.stringify(patches)
1787
2896
  });
1788
2897
  if (fetchRes.status === 200) {
1789
2898
  return {
@@ -1797,89 +2906,62 @@ class ProxyValServer {
1797
2906
  }
1798
2907
  });
1799
2908
  }
1800
- async postCommit(rawBody, cookies) {
1801
- const commit = this.options.gitCommit;
1802
- if (!commit) {
1803
- return {
1804
- status: 401,
1805
- json: {
1806
- message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
1807
- }
1808
- };
1809
- }
1810
- const params = new URLSearchParams({
1811
- commit
1812
- });
1813
- return this.withAuth(cookies, "postCommit", async ({
1814
- token
1815
- }) => {
1816
- const url = new URL(`/v1/commit/${this.options.valName}/heads/${this.options.gitBranch}/~?${params}`, this.options.valContentUrl);
1817
- const body = JSON.stringify(rawBody);
2909
+ async getFiles(filePath, query, cookies) {
2910
+ return withAuth(this.options.valSecret, cookies, "getFiles", async data => {
2911
+ const url = new URL(`/v1/files/${this.options.valName}${filePath}`, this.options.valContentUrl);
2912
+ if (typeof query.sha256 === "string") {
2913
+ url.searchParams.append("sha256", query.sha256);
2914
+ } else {
2915
+ console.warn("Missing sha256 query param");
2916
+ }
1818
2917
  const fetchRes = await fetch(url, {
1819
- method: "POST",
1820
- headers: this.getAuthHeaders(token, "application/json"),
1821
- body
2918
+ headers: getAuthHeaders(data.token)
1822
2919
  });
1823
2920
  if (fetchRes.status === 200) {
1824
- return {
1825
- status: fetchRes.status,
1826
- json: await fetchRes.json()
1827
- };
2921
+ // TODO: does this stream data?
2922
+ if (fetchRes.body) {
2923
+ return {
2924
+ status: fetchRes.status,
2925
+ headers: {
2926
+ "Content-Type": fetchRes.headers.get("Content-Type") || "",
2927
+ "Content-Length": fetchRes.headers.get("Content-Length") || "0"
2928
+ },
2929
+ body: fetchRes.body
2930
+ };
2931
+ } else {
2932
+ return {
2933
+ status: 500,
2934
+ json: {
2935
+ message: "No body in response"
2936
+ }
2937
+ };
2938
+ }
1828
2939
  } else {
1829
- return {
1830
- status: fetchRes.status
1831
- };
1832
- }
1833
- });
1834
- }
1835
- getAuthHeaders(token, type) {
1836
- if (!type) {
1837
- return {
1838
- Authorization: `Bearer ${token}`
1839
- };
1840
- }
1841
- return {
1842
- "Content-Type": type,
1843
- Authorization: `Bearer ${token}`
1844
- };
1845
- }
1846
- async consumeCode(code) {
1847
- const url = new URL(`/api/val/${this.options.valName}/auth/token`, this.options.valBuildUrl);
1848
- url.searchParams.set("code", encodeURIComponent(code));
1849
- return fetch(url, {
1850
- method: "POST",
1851
- headers: this.getAuthHeaders(this.options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
1852
- }).then(async res => {
1853
- if (res.status === 200) {
1854
- const token = await res.text();
1855
- const verification = ValAppJwtPayload.safeParse(decodeJwt(token));
1856
- if (!verification.success) {
1857
- return null;
2940
+ const fileExists = this.remoteFS.fileExists(path__default.join(this.cwd, filePath));
2941
+ let buffer;
2942
+ if (fileExists) {
2943
+ buffer = await this.readStaticBinaryFile(path__default.join(this.cwd, filePath));
2944
+ }
2945
+ if (!buffer) {
2946
+ return {
2947
+ status: 404,
2948
+ json: {
2949
+ message: "File not found"
2950
+ }
2951
+ };
1858
2952
  }
2953
+ const mimeType = guessMimeTypeFromPath(filePath) || "application/octet-stream";
1859
2954
  return {
1860
- ...verification.data,
1861
- token
2955
+ status: 200,
2956
+ headers: {
2957
+ "Content-Type": mimeType,
2958
+ "Content-Length": buffer.byteLength.toString()
2959
+ },
2960
+ body: bufferToReadableStream(buffer)
1862
2961
  };
1863
- } else {
1864
- console.debug("Failed to get data from code: ", res.status);
1865
- return null;
1866
2962
  }
1867
- }).catch(err => {
1868
- console.debug("Failed to get user from code: ", err);
1869
- return null;
1870
2963
  });
1871
2964
  }
1872
- getAuthorizeUrl(publicValApiRoute, token) {
1873
- const url = new URL(`/auth/${this.options.valName}/authorize`, this.options.valBuildUrl);
1874
- url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRoute}/callback`));
1875
- url.searchParams.set("state", token);
1876
- return url.toString();
1877
- }
1878
- getAppErrorUrl(error) {
1879
- const url = new URL(`/auth/${this.options.valName}/authorize`, this.options.valBuildUrl);
1880
- url.searchParams.set("error", encodeURIComponent(error));
1881
- return url.toString();
1882
- }
1883
2965
  }
1884
2966
  function verifyCallbackReq(stateCookie, queryParams) {
1885
2967
  if (typeof stateCookie !== "string") {
@@ -1997,11 +3079,67 @@ const IntegratedServerJwtPayload = z$1.object({
1997
3079
  org: z$1.string(),
1998
3080
  project: z$1.string()
1999
3081
  });
3082
+ async function withAuth(secret, cookies, errorMessageType, handler) {
3083
+ const cookie = cookies[VAL_SESSION_COOKIE$1];
3084
+ if (typeof cookie === "string") {
3085
+ const decodedToken = decodeJwt(cookie, secret);
3086
+ if (!decodedToken) {
3087
+ return {
3088
+ status: 401,
3089
+ json: {
3090
+ message: "Could not verify session. You will need to login again.",
3091
+ details: "Invalid token"
3092
+ }
3093
+ };
3094
+ }
3095
+ const verification = IntegratedServerJwtPayload.safeParse(decodedToken);
3096
+ if (!verification.success) {
3097
+ return {
3098
+ status: 401,
3099
+ json: {
3100
+ message: "Session invalid or, most likely, expired. You will need to login again.",
3101
+ details: verification.error
3102
+ }
3103
+ };
3104
+ }
3105
+ return handler(verification.data).catch(err => {
3106
+ console.error(`Failed while processing: ${errorMessageType}`, err);
3107
+ return {
3108
+ status: 500,
3109
+ json: {
3110
+ message: err.message
3111
+ }
3112
+ };
3113
+ });
3114
+ } else {
3115
+ return {
3116
+ status: 401,
3117
+ json: {
3118
+ message: "Login required",
3119
+ details: {
3120
+ reason: "Cookie not found"
3121
+ }
3122
+ }
3123
+ };
3124
+ }
3125
+ }
3126
+ function getAuthHeaders(token, type) {
3127
+ if (!type) {
3128
+ return {
3129
+ Authorization: `Bearer ${token}`
3130
+ };
3131
+ }
3132
+ return {
3133
+ "Content-Type": type,
3134
+ Authorization: `Bearer ${token}`
3135
+ };
3136
+ }
2000
3137
 
2001
3138
  async function createValServer(route, opts, callbacks) {
2002
3139
  const serverOpts = await initHandlerOptions(route, opts);
2003
3140
  if (serverOpts.mode === "proxy") {
2004
- return new ProxyValServer(serverOpts, callbacks);
3141
+ const projectRoot = process.cwd(); //[process.cwd(), opts.root || ""] .filter((seg) => seg) .join("/");
3142
+ return new ProxyValServer(projectRoot, serverOpts, opts, callbacks);
2005
3143
  } else {
2006
3144
  return new LocalValServer(serverOpts, callbacks);
2007
3145
  }
@@ -2035,8 +3173,10 @@ async function initHandlerOptions(route, opts) {
2035
3173
  valSecret: maybeValSecret,
2036
3174
  valBuildUrl,
2037
3175
  valContentUrl,
2038
- gitCommit: maybeGitCommit,
2039
- gitBranch: maybeGitBranch,
3176
+ git: {
3177
+ commit: maybeGitCommit,
3178
+ branch: maybeGitBranch
3179
+ },
2040
3180
  valName: maybeValName,
2041
3181
  valEnableRedirectUrl: opts.valEnableRedirectUrl || process.env.VAL_ENABLE_REDIRECT_URL,
2042
3182
  valDisableRedirectUrl: opts.valDisableRedirectUrl || process.env.VAL_DISABLE_REDIRECT_URL
@@ -2189,6 +3329,9 @@ function createValApiRouter(route, valServerPromise, convert) {
2189
3329
  } else if (method === "POST" && path === "/commit") {
2190
3330
  const body = await req.json();
2191
3331
  return convert(await valServer.postCommit(body, getCookies(req, [VAL_SESSION_COOKIE])));
3332
+ } else if (method === "POST" && path === "/validate") {
3333
+ const body = await req.json();
3334
+ return convert(await valServer.postValidate(body, getCookies(req, [VAL_SESSION_COOKIE])));
2192
3335
  } else if (method === "GET" && path.startsWith(TREE_PATH_PREFIX)) {
2193
3336
  return withTreePath(path, TREE_PATH_PREFIX)(async treePath => convert(await valServer.getTree(treePath, {
2194
3337
  patch: url.searchParams.get("patch") || undefined,
@@ -2202,6 +3345,10 @@ function createValApiRouter(route, valServerPromise, convert) {
2202
3345
  } else if (method === "POST" && path.startsWith(PATCHES_PATH_PREFIX)) {
2203
3346
  const body = await req.json();
2204
3347
  return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.postPatches(body, getCookies(req, [VAL_SESSION_COOKIE]))));
3348
+ } else if (method === "DELETE" && path.startsWith(PATCHES_PATH_PREFIX)) {
3349
+ return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.deletePatches({
3350
+ id: url.searchParams.getAll("id")
3351
+ }, getCookies(req, [VAL_SESSION_COOKIE]))));
2205
3352
  } else if (path.startsWith(FILES_PATH_PREFIX)) {
2206
3353
  const treePath = path.slice(FILES_PATH_PREFIX.length);
2207
3354
  return convert(await valServer.getFiles(treePath, {
@@ -2248,6 +3395,9 @@ class ValFSHost {
2248
3395
  readDirectory(rootDir, extensions, excludes, includes, depth) {
2249
3396
  return this.valFS.readDirectory(rootDir, extensions, excludes, includes, depth);
2250
3397
  }
3398
+ rmFile(fileName) {
3399
+ this.valFS.rmFile(fileName);
3400
+ }
2251
3401
  writeFile(fileName, text, encoding) {
2252
3402
  this.valFS.writeFile(fileName, text, encoding);
2253
3403
  }
@@ -2272,67 +3422,26 @@ class ValFSHost {
2272
3422
  }
2273
3423
 
2274
3424
  // TODO: find a better name? transformFixesToPatch?
2275
- const textEncoder = new TextEncoder();
2276
3425
  async function createFixPatch(config, apply, sourcePath, validationError) {
2277
3426
  async function getImageMetadata() {
2278
- const maybeRef = validationError.value && typeof validationError.value === "object" && FILE_REF_PROP in validationError.value && typeof validationError.value[FILE_REF_PROP] === "string" ? validationError.value[FILE_REF_PROP] : undefined;
2279
- if (!maybeRef) {
3427
+ const fileRef = getValidationErrorFileRef(validationError);
3428
+ if (!fileRef) {
2280
3429
  // TODO:
2281
3430
  throw Error("Cannot fix image without a file reference");
2282
3431
  }
2283
- const filename = path__default.join(config.projectRoot, maybeRef);
3432
+ const filename = path__default.join(config.projectRoot, fileRef);
2284
3433
  const buffer = fs.readFileSync(filename);
2285
- const imageSize = sizeOf(buffer);
2286
- let mimeType = null;
2287
- if (imageSize.type) {
2288
- const possibleMimeType = `image/${imageSize.type}`;
2289
- if (MIME_TYPES_TO_EXT[possibleMimeType]) {
2290
- mimeType = possibleMimeType;
2291
- }
2292
- const filenameBasedLookup = filenameToMimeType(filename);
2293
- if (filenameBasedLookup) {
2294
- mimeType = filenameBasedLookup;
2295
- }
2296
- }
2297
- if (!mimeType) {
2298
- throw Error("Cannot determine mimetype of image");
2299
- }
2300
- const {
2301
- width,
2302
- height
2303
- } = imageSize;
2304
- if (!width || !height) {
2305
- throw Error("Cannot determine image size");
2306
- }
2307
- const sha256 = Internal.getSHA256Hash(textEncoder.encode(
2308
- // TODO: we should probably store the mimetype in the metadata and reuse it here
2309
- `data:${mimeType};base64,${buffer.toString("base64")}`));
2310
- return {
2311
- width,
2312
- height,
2313
- sha256,
2314
- mimeType
2315
- };
3434
+ return extractImageMetadata(filename, buffer);
2316
3435
  }
2317
3436
  async function getFileMetadata() {
2318
- const maybeRef = validationError.value && typeof validationError.value === "object" && FILE_REF_PROP in validationError.value && typeof validationError.value[FILE_REF_PROP] === "string" ? validationError.value[FILE_REF_PROP] : undefined;
2319
- if (!maybeRef) {
3437
+ const fileRef = getValidationErrorFileRef(validationError);
3438
+ if (!fileRef) {
2320
3439
  // TODO:
2321
- throw Error("Cannot fix image without a file reference");
3440
+ throw Error("Cannot fix file without a file reference");
2322
3441
  }
2323
- const filename = path__default.join(config.projectRoot, maybeRef);
3442
+ const filename = path__default.join(config.projectRoot, fileRef);
2324
3443
  const buffer = fs.readFileSync(filename);
2325
- let mimeType = filenameToMimeType(filename);
2326
- if (!mimeType) {
2327
- mimeType = "application/octet-stream";
2328
- }
2329
- const sha256 = Internal.getSHA256Hash(textEncoder.encode(
2330
- // TODO: we should probably store the mimetype in the metadata and reuse it here
2331
- `data:${mimeType};base64,${buffer.toString("base64")}`));
2332
- return {
2333
- sha256,
2334
- mimeType
2335
- };
3444
+ return extractFileMetadata(fileRef, buffer);
2336
3445
  }
2337
3446
  const remainingErrors = [];
2338
3447
  const patch = [];
@@ -2496,4 +3605,4 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
2496
3605
  };
2497
3606
  }
2498
3607
 
2499
- export { LocalValServer, PatchJSON, Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createFixPatch, createService, createValApiRouter, createValServer, decodeJwt, encodeJwt, formatSyntaxErrorTree, getCompilerOptions, getExpire, patchSourceFile, safeReadGit };
3608
+ export { LocalValServer, Patch, PatchJSON, Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createFixPatch, createService, createValApiRouter, createValServer, decodeJwt, encodeJwt, formatSyntaxErrorTree, getCompilerOptions, getExpire, patchSourceFile, safeReadGit };