@valbuild/server 0.58.0 → 0.60.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.
@@ -12,9 +12,10 @@ var fs = require('fs');
12
12
  var sucrase = require('sucrase');
13
13
  var z = require('zod');
14
14
  var internal = require('@valbuild/shared/internal');
15
+ var sizeOf = require('image-size');
15
16
  var crypto = require('crypto');
17
+ var minimatch = require('minimatch');
16
18
  var server = require('@valbuild/ui/server');
17
- var sizeOf = require('image-size');
18
19
 
19
20
  function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
20
21
 
@@ -36,12 +37,13 @@ function _interopNamespace(e) {
36
37
  return Object.freeze(n);
37
38
  }
38
39
 
39
- var ts__default = /*#__PURE__*/_interopDefault(ts);
40
+ var ts__namespace = /*#__PURE__*/_interopNamespace(ts);
40
41
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
41
42
  var fs__default = /*#__PURE__*/_interopDefault(fs);
42
43
  var z__default = /*#__PURE__*/_interopDefault(z);
43
- var crypto__default = /*#__PURE__*/_interopDefault(crypto);
44
44
  var sizeOf__default = /*#__PURE__*/_interopDefault(sizeOf);
45
+ var crypto__default = /*#__PURE__*/_interopDefault(crypto);
46
+ var minimatch__default = /*#__PURE__*/_interopDefault(minimatch);
45
47
 
46
48
  class ValSyntaxError {
47
49
  constructor(message, node) {
@@ -71,12 +73,12 @@ function formatSyntaxErrorTree(tree, sourceFile) {
71
73
  return flatMapErrors(tree, error => formatSyntaxError(error, sourceFile));
72
74
  }
73
75
  function isLiteralPropertyName(name) {
74
- return ts__default["default"].isIdentifier(name) || ts__default["default"].isStringLiteral(name) || ts__default["default"].isNumericLiteral(name);
76
+ return ts__namespace["default"].isIdentifier(name) || ts__namespace["default"].isStringLiteral(name) || ts__namespace["default"].isNumericLiteral(name);
75
77
  }
76
78
  function validateObjectProperties(nodes) {
77
79
  const errors = [];
78
80
  for (const node of nodes) {
79
- if (!ts__default["default"].isPropertyAssignment(node)) {
81
+ if (!ts__namespace["default"].isPropertyAssignment(node)) {
80
82
  errors.push(new ValSyntaxError("Object literal element must be property assignment", node));
81
83
  } else if (!isLiteralPropertyName(node.name)) {
82
84
  errors.push(new ValSyntaxError("Object literal element key must be an identifier or a literal", node));
@@ -93,7 +95,7 @@ function validateObjectProperties(nodes) {
93
95
  * validating its children.
94
96
  */
95
97
  function shallowValidateExpression(value) {
96
- return ts__default["default"].isStringLiteralLike(value) || ts__default["default"].isNumericLiteral(value) || value.kind === ts__default["default"].SyntaxKind.TrueKeyword || value.kind === ts__default["default"].SyntaxKind.FalseKeyword || value.kind === ts__default["default"].SyntaxKind.NullKeyword || ts__default["default"].isArrayLiteralExpression(value) || ts__default["default"].isObjectLiteralExpression(value) || isValFileMethodCall(value) ? undefined : new ValSyntaxError("Expression must be a literal or call c.file", value);
98
+ return ts__namespace["default"].isStringLiteralLike(value) || ts__namespace["default"].isNumericLiteral(value) || value.kind === ts__namespace["default"].SyntaxKind.TrueKeyword || value.kind === ts__namespace["default"].SyntaxKind.FalseKeyword || value.kind === ts__namespace["default"].SyntaxKind.NullKeyword || ts__namespace["default"].isArrayLiteralExpression(value) || ts__namespace["default"].isObjectLiteralExpression(value) || isValFileMethodCall(value) ? undefined : new ValSyntaxError("Expression must be a literal or call c.file", value);
97
99
  }
98
100
 
99
101
  /**
@@ -105,19 +107,19 @@ function evaluateExpression(value) {
105
107
  // 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".
106
108
  // https://github.com/microsoft/TypeScript/blob/4b794fe1dd0d184d3f8f17e94d8187eace57c91e/src/compiler/types.ts#L2127-L2131
107
109
 
108
- if (ts__default["default"].isStringLiteralLike(value)) {
110
+ if (ts__namespace["default"].isStringLiteralLike(value)) {
109
111
  return fp.result.ok(value.text);
110
- } else if (ts__default["default"].isNumericLiteral(value)) {
112
+ } else if (ts__namespace["default"].isNumericLiteral(value)) {
111
113
  return fp.result.ok(Number(value.text));
112
- } else if (value.kind === ts__default["default"].SyntaxKind.TrueKeyword) {
114
+ } else if (value.kind === ts__namespace["default"].SyntaxKind.TrueKeyword) {
113
115
  return fp.result.ok(true);
114
- } else if (value.kind === ts__default["default"].SyntaxKind.FalseKeyword) {
116
+ } else if (value.kind === ts__namespace["default"].SyntaxKind.FalseKeyword) {
115
117
  return fp.result.ok(false);
116
- } else if (value.kind === ts__default["default"].SyntaxKind.NullKeyword) {
118
+ } else if (value.kind === ts__namespace["default"].SyntaxKind.NullKeyword) {
117
119
  return fp.result.ok(null);
118
- } else if (ts__default["default"].isArrayLiteralExpression(value)) {
120
+ } else if (ts__namespace["default"].isArrayLiteralExpression(value)) {
119
121
  return fp.result.all(value.elements.map(evaluateExpression));
120
- } else if (ts__default["default"].isObjectLiteralExpression(value)) {
122
+ } else if (ts__namespace["default"].isObjectLiteralExpression(value)) {
121
123
  return fp.pipe(validateObjectProperties(value.properties), fp.result.flatMap(assignments => fp.pipe(assignments.map(assignment => fp.pipe(evaluateExpression(assignment.initializer), fp.result.map(value => [assignment.name.text, value]))), fp.result.all)), fp.result.map(Object.fromEntries));
122
124
  } else if (isValFileMethodCall(value)) {
123
125
  return fp.pipe(findValFileNodeArg(value), fp.result.flatMap(ref => {
@@ -150,14 +152,14 @@ function findObjectPropertyAssignment(value, key) {
150
152
  }));
151
153
  }
152
154
  function isValFileMethodCall(node) {
153
- return ts__default["default"].isCallExpression(node) && ts__default["default"].isPropertyAccessExpression(node.expression) && ts__default["default"].isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "file";
155
+ return ts__namespace["default"].isCallExpression(node) && ts__namespace["default"].isPropertyAccessExpression(node.expression) && ts__namespace["default"].isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "file";
154
156
  }
155
157
  function findValFileNodeArg(node) {
156
158
  if (node.arguments.length === 0) {
157
159
  return fp.result.err(new ValSyntaxError(`Invalid c.file() call: missing ref argument`, node));
158
160
  } else if (node.arguments.length > 2) {
159
161
  return fp.result.err(new ValSyntaxError(`Invalid c.file() call: too many arguments ${node.arguments.length}}`, node));
160
- } else if (!ts__default["default"].isStringLiteral(node.arguments[0])) {
162
+ } else if (!ts__namespace["default"].isStringLiteral(node.arguments[0])) {
161
163
  return fp.result.err(new ValSyntaxError(`Invalid c.file() call: ref must be a string literal`, node));
162
164
  }
163
165
  const refNode = node.arguments[0];
@@ -169,7 +171,7 @@ function findValFileMetadataArg(node) {
169
171
  } else if (node.arguments.length > 2) {
170
172
  return fp.result.err(new ValSyntaxError(`Invalid c.file() call: too many arguments ${node.arguments.length}}`, node));
171
173
  } else if (node.arguments.length === 2) {
172
- if (!ts__default["default"].isObjectLiteralExpression(node.arguments[1])) {
174
+ if (!ts__namespace["default"].isObjectLiteralExpression(node.arguments[1])) {
173
175
  return fp.result.err(new ValSyntaxError(`Invalid c.file() call: metadata must be a object literal`, node));
174
176
  }
175
177
  return fp.result.ok(node.arguments[1]);
@@ -184,7 +186,7 @@ function findValFileMetadataArg(node) {
184
186
  */
185
187
  function validateInitializers(nodes) {
186
188
  for (const node of nodes) {
187
- if (ts__default["default"].isSpreadElement(node)) {
189
+ if (ts__namespace["default"].isSpreadElement(node)) {
188
190
  return new ValSyntaxError("Unexpected spread element", node);
189
191
  }
190
192
  }
@@ -195,22 +197,22 @@ function isPath(node, path) {
195
197
  let currentNode = node;
196
198
  for (let i = path.length - 1; i > 0; --i) {
197
199
  const name = path[i];
198
- if (!ts__default["default"].isPropertyAccessExpression(currentNode)) {
200
+ if (!ts__namespace["default"].isPropertyAccessExpression(currentNode)) {
199
201
  return false;
200
202
  }
201
- if (!ts__default["default"].isIdentifier(currentNode.name) || currentNode.name.text !== name) {
203
+ if (!ts__namespace["default"].isIdentifier(currentNode.name) || currentNode.name.text !== name) {
202
204
  return false;
203
205
  }
204
206
  currentNode = currentNode.expression;
205
207
  }
206
- return ts__default["default"].isIdentifier(currentNode) && currentNode.text === path[0];
208
+ return ts__namespace["default"].isIdentifier(currentNode) && currentNode.text === path[0];
207
209
  }
208
210
  function validateArguments(node, validators) {
209
211
  return fp.result.allV([node.arguments.length === validators.length ? fp.result.voidOk : fp.result.err(new ValSyntaxError(`Expected ${validators.length} arguments`, node)), ...node.arguments.slice(0, validators.length).map((argument, index) => validators[index](argument))]);
210
212
  }
211
213
  function analyzeDefaultExport(node) {
212
214
  const cDefine = node.expression;
213
- if (!ts__default["default"].isCallExpression(cDefine)) {
215
+ if (!ts__namespace["default"].isCallExpression(cDefine)) {
214
216
  return fp.result.err(new ValSyntaxError("Expected default expression to be a call expression", cDefine));
215
217
  }
216
218
  if (!isPath(cDefine.expression, ["c", "define"])) {
@@ -218,7 +220,7 @@ function analyzeDefaultExport(node) {
218
220
  }
219
221
  return fp.pipe(validateArguments(cDefine, [id => {
220
222
  // TODO: validate ID value here?
221
- if (!ts__default["default"].isStringLiteralLike(id)) {
223
+ if (!ts__namespace["default"].isStringLiteralLike(id)) {
222
224
  return fp.result.err(new ValSyntaxError("Expected first argument to c.define to be a string literal", id));
223
225
  }
224
226
  return fp.result.voidOk;
@@ -236,7 +238,7 @@ function analyzeDefaultExport(node) {
236
238
  }
237
239
  function analyzeValModule(sourceFile) {
238
240
  const analysis = sourceFile.forEachChild(node => {
239
- if (ts__default["default"].isExportAssignment(node)) {
241
+ if (ts__namespace["default"].isExportAssignment(node)) {
240
242
  return analyzeDefaultExport(node);
241
243
  }
242
244
  });
@@ -250,62 +252,62 @@ function isValidIdentifier(text) {
250
252
  if (text.length === 0) {
251
253
  return false;
252
254
  }
253
- if (!ts__default["default"].isIdentifierStart(text.charCodeAt(0), ts__default["default"].ScriptTarget.ES2020)) {
255
+ if (!ts__namespace["default"].isIdentifierStart(text.charCodeAt(0), ts__namespace["default"].ScriptTarget.ES2020)) {
254
256
  return false;
255
257
  }
256
258
  for (let i = 1; i < text.length; ++i) {
257
- if (!ts__default["default"].isIdentifierPart(text.charCodeAt(i), ts__default["default"].ScriptTarget.ES2020)) {
259
+ if (!ts__namespace["default"].isIdentifierPart(text.charCodeAt(i), ts__namespace["default"].ScriptTarget.ES2020)) {
258
260
  return false;
259
261
  }
260
262
  }
261
263
  return true;
262
264
  }
263
265
  function createPropertyAssignment(key, value) {
264
- return ts__default["default"].factory.createPropertyAssignment(isValidIdentifier(key) ? ts__default["default"].factory.createIdentifier(key) : ts__default["default"].factory.createStringLiteral(key), toExpression(value));
266
+ return ts__namespace["default"].factory.createPropertyAssignment(isValidIdentifier(key) ? ts__namespace["default"].factory.createIdentifier(key) : ts__namespace["default"].factory.createStringLiteral(key), toExpression(value));
265
267
  }
266
268
  function createValFileReference(value) {
267
- const args = [ts__default["default"].factory.createStringLiteral(value[core.FILE_REF_PROP])];
269
+ const args = [ts__namespace["default"].factory.createStringLiteral(value[core.FILE_REF_PROP])];
268
270
  if (value.metadata) {
269
271
  args.push(toExpression(value.metadata));
270
272
  }
271
- return ts__default["default"].factory.createCallExpression(ts__default["default"].factory.createPropertyAccessExpression(ts__default["default"].factory.createIdentifier("c"), ts__default["default"].factory.createIdentifier("file")), undefined, args);
273
+ return ts__namespace["default"].factory.createCallExpression(ts__namespace["default"].factory.createPropertyAccessExpression(ts__namespace["default"].factory.createIdentifier("c"), ts__namespace["default"].factory.createIdentifier("file")), undefined, args);
272
274
  }
273
275
  function createValRichTextImage(value) {
274
- const args = [ts__default["default"].factory.createStringLiteral(value[core.FILE_REF_PROP])];
276
+ const args = [ts__namespace["default"].factory.createStringLiteral(value[core.FILE_REF_PROP])];
275
277
  if (value.metadata) {
276
278
  args.push(toExpression(value.metadata));
277
279
  }
278
- return ts__default["default"].factory.createCallExpression(ts__default["default"].factory.createPropertyAccessExpression(ts__default["default"].factory.createPropertyAccessExpression(ts__default["default"].factory.createIdentifier("c"), "rt"), ts__default["default"].factory.createIdentifier("image")), undefined, args);
280
+ return ts__namespace["default"].factory.createCallExpression(ts__namespace["default"].factory.createPropertyAccessExpression(ts__namespace["default"].factory.createPropertyAccessExpression(ts__namespace["default"].factory.createIdentifier("c"), "rt"), ts__namespace["default"].factory.createIdentifier("image")), undefined, args);
279
281
  }
280
282
  function createValLink(value) {
281
- const args = [ts__default["default"].factory.createStringLiteral(value.children[0]), toExpression({
283
+ const args = [ts__namespace["default"].factory.createStringLiteral(value.children[0]), toExpression({
282
284
  href: value.href
283
285
  })];
284
- return ts__default["default"].factory.createCallExpression(ts__default["default"].factory.createPropertyAccessExpression(ts__default["default"].factory.createPropertyAccessExpression(ts__default["default"].factory.createIdentifier("c"), "rt"), ts__default["default"].factory.createIdentifier("link")), undefined, args);
286
+ return ts__namespace["default"].factory.createCallExpression(ts__namespace["default"].factory.createPropertyAccessExpression(ts__namespace["default"].factory.createPropertyAccessExpression(ts__namespace["default"].factory.createIdentifier("c"), "rt"), ts__namespace["default"].factory.createIdentifier("link")), undefined, args);
285
287
  }
286
288
  function createValRichTextTaggedStringTemplate(value) {
287
289
  const {
288
290
  templateStrings: [head, ...others],
289
291
  exprs
290
292
  } = value;
291
- const tag = ts__default["default"].factory.createPropertyAccessExpression(ts__default["default"].factory.createIdentifier("c"), ts__default["default"].factory.createIdentifier("richtext"));
293
+ const tag = ts__namespace["default"].factory.createPropertyAccessExpression(ts__namespace["default"].factory.createIdentifier("c"), ts__namespace["default"].factory.createIdentifier("richtext"));
292
294
  if (exprs.length > 0) {
293
- return ts__default["default"].factory.createTaggedTemplateExpression(tag, undefined, ts__default["default"].factory.createTemplateExpression(ts__default["default"].factory.createTemplateHead(head, head), others.map((s, i) => ts__default["default"].factory.createTemplateSpan(toExpression(exprs[i]), i < others.length - 1 ? ts__default["default"].factory.createTemplateMiddle(s, s) : ts__default["default"].factory.createTemplateTail(s, s)))));
295
+ return ts__namespace["default"].factory.createTaggedTemplateExpression(tag, undefined, ts__namespace["default"].factory.createTemplateExpression(ts__namespace["default"].factory.createTemplateHead(head, head), others.map((s, i) => ts__namespace["default"].factory.createTemplateSpan(toExpression(exprs[i]), i < others.length - 1 ? ts__namespace["default"].factory.createTemplateMiddle(s, s) : ts__namespace["default"].factory.createTemplateTail(s, s)))));
294
296
  }
295
- return ts__default["default"].factory.createTaggedTemplateExpression(tag, undefined, ts__default["default"].factory.createNoSubstitutionTemplateLiteral(head, head));
297
+ return ts__namespace["default"].factory.createTaggedTemplateExpression(tag, undefined, ts__namespace["default"].factory.createNoSubstitutionTemplateLiteral(head, head));
296
298
  }
297
299
  function toExpression(value) {
298
300
  if (typeof value === "string") {
299
301
  // TODO: Use configuration/heuristics to determine use of single quote or double quote
300
- return ts__default["default"].factory.createStringLiteral(value);
302
+ return ts__namespace["default"].factory.createStringLiteral(value);
301
303
  } else if (typeof value === "number") {
302
- return ts__default["default"].factory.createNumericLiteral(value);
304
+ return ts__namespace["default"].factory.createNumericLiteral(value);
303
305
  } else if (typeof value === "boolean") {
304
- return value ? ts__default["default"].factory.createTrue() : ts__default["default"].factory.createFalse();
306
+ return value ? ts__namespace["default"].factory.createTrue() : ts__namespace["default"].factory.createFalse();
305
307
  } else if (value === null) {
306
- return ts__default["default"].factory.createNull();
308
+ return ts__namespace["default"].factory.createNull();
307
309
  } else if (Array.isArray(value)) {
308
- return ts__default["default"].factory.createArrayLiteralExpression(value.map(toExpression));
310
+ return ts__namespace["default"].factory.createArrayLiteralExpression(value.map(toExpression));
309
311
  } else if (typeof value === "object") {
310
312
  if (isValFileValue(value)) {
311
313
  if (isValRichTextImageValue(value)) {
@@ -317,24 +319,24 @@ function toExpression(value) {
317
319
  } else if (isValRichTextValue(value)) {
318
320
  return createValRichTextTaggedStringTemplate(value);
319
321
  }
320
- return ts__default["default"].factory.createObjectLiteralExpression(Object.entries(value).map(([key, value]) => createPropertyAssignment(key, value)));
322
+ return ts__namespace["default"].factory.createObjectLiteralExpression(Object.entries(value).map(([key, value]) => createPropertyAssignment(key, value)));
321
323
  } else {
322
- return ts__default["default"].factory.createStringLiteral(value);
324
+ return ts__namespace["default"].factory.createStringLiteral(value);
323
325
  }
324
326
  }
325
327
 
326
328
  // TODO: Choose newline based on project settings/heuristics/system default?
327
- const newLine = ts__default["default"].NewLineKind.LineFeed;
329
+ const newLine = ts__namespace["default"].NewLineKind.LineFeed;
328
330
  // TODO: Handle indentation of printed code
329
- const printer = ts__default["default"].createPrinter({
331
+ const printer = ts__namespace["default"].createPrinter({
330
332
  newLine: newLine
331
333
  // neverAsciiEscape: true,
332
334
  });
333
335
  function replaceNodeValue(document, node, value) {
334
- const replacementText = printer.printNode(ts__default["default"].EmitHint.Unspecified, toExpression(value), document);
335
- const span = ts__default["default"].createTextSpanFromBounds(node.getStart(document, false), node.end);
336
- const newText = `${document.text.substring(0, span.start)}${replacementText}${document.text.substring(ts__default["default"].textSpanEnd(span))}`;
337
- return [document.update(newText, ts__default["default"].createTextChangeRange(span, replacementText.length)), node];
336
+ const replacementText = printer.printNode(ts__namespace["default"].EmitHint.Unspecified, toExpression(value), document);
337
+ const span = ts__namespace["default"].createTextSpanFromBounds(node.getStart(document, false), node.end);
338
+ const newText = `${document.text.substring(0, span.start)}${replacementText}${document.text.substring(ts__namespace["default"].textSpanEnd(span))}`;
339
+ return [document.update(newText, ts__namespace["default"].createTextChangeRange(span, replacementText.length)), node];
338
340
  }
339
341
  function isIndentation(s) {
340
342
  for (let i = 0; i < s.length; ++i) {
@@ -346,7 +348,7 @@ function isIndentation(s) {
346
348
  return true;
347
349
  }
348
350
  function newLineStr(kind) {
349
- if (kind === ts__default["default"].NewLineKind.CarriageReturnLineFeed) {
351
+ if (kind === ts__namespace["default"].NewLineKind.CarriageReturnLineFeed) {
350
352
  return "\r\n";
351
353
  } else {
352
354
  return "\n";
@@ -368,38 +370,38 @@ function insertAt(document, nodes, index, node) {
368
370
  let replacementText;
369
371
  if (nodes.length === 0) {
370
372
  // Replace entire range of nodes
371
- replacementText = printer.printNode(ts__default["default"].EmitHint.Unspecified, node, document);
372
- span = ts__default["default"].createTextSpanFromBounds(nodes.pos, nodes.end);
373
+ replacementText = printer.printNode(ts__namespace["default"].EmitHint.Unspecified, node, document);
374
+ span = ts__namespace["default"].createTextSpanFromBounds(nodes.pos, nodes.end);
373
375
  } else if (index === nodes.length) {
374
376
  // Insert after last node
375
377
  const neighbor = nodes[nodes.length - 1];
376
- replacementText = `${getSeparator(document, neighbor)}${printer.printNode(ts__default["default"].EmitHint.Unspecified, node, document)}`;
377
- span = ts__default["default"].createTextSpan(neighbor.end, 0);
378
+ replacementText = `${getSeparator(document, neighbor)}${printer.printNode(ts__namespace["default"].EmitHint.Unspecified, node, document)}`;
379
+ span = ts__namespace["default"].createTextSpan(neighbor.end, 0);
378
380
  } else {
379
381
  // Insert before node
380
382
  const neighbor = nodes[index];
381
- replacementText = `${printer.printNode(ts__default["default"].EmitHint.Unspecified, node, document)}${getSeparator(document, neighbor)}`;
382
- span = ts__default["default"].createTextSpan(neighbor.getStart(document, true), 0);
383
+ replacementText = `${printer.printNode(ts__namespace["default"].EmitHint.Unspecified, node, document)}${getSeparator(document, neighbor)}`;
384
+ span = ts__namespace["default"].createTextSpan(neighbor.getStart(document, true), 0);
383
385
  }
384
- const newText = `${document.text.substring(0, span.start)}${replacementText}${document.text.substring(ts__default["default"].textSpanEnd(span))}`;
385
- return document.update(newText, ts__default["default"].createTextChangeRange(span, replacementText.length));
386
+ const newText = `${document.text.substring(0, span.start)}${replacementText}${document.text.substring(ts__namespace["default"].textSpanEnd(span))}`;
387
+ return document.update(newText, ts__namespace["default"].createTextChangeRange(span, replacementText.length));
386
388
  }
387
389
  function removeAt(document, nodes, index) {
388
390
  const node = nodes[index];
389
391
  let span;
390
392
  if (nodes.length === 1) {
391
- span = ts__default["default"].createTextSpanFromBounds(nodes.pos, nodes.end);
393
+ span = ts__namespace["default"].createTextSpanFromBounds(nodes.pos, nodes.end);
392
394
  } else if (index === nodes.length - 1) {
393
395
  // Remove until previous node
394
396
  const neighbor = nodes[index - 1];
395
- span = ts__default["default"].createTextSpanFromBounds(neighbor.end, node.end);
397
+ span = ts__namespace["default"].createTextSpanFromBounds(neighbor.end, node.end);
396
398
  } else {
397
399
  // Remove before next node
398
400
  const neighbor = nodes[index + 1];
399
- span = ts__default["default"].createTextSpanFromBounds(node.getStart(document, true), neighbor.getStart(document, true));
401
+ span = ts__namespace["default"].createTextSpanFromBounds(node.getStart(document, true), neighbor.getStart(document, true));
400
402
  }
401
- const newText = `${document.text.substring(0, span.start)}${document.text.substring(ts__default["default"].textSpanEnd(span))}`;
402
- return [document.update(newText, ts__default["default"].createTextChangeRange(span, 0)), node];
403
+ const newText = `${document.text.substring(0, span.start)}${document.text.substring(ts__namespace["default"].textSpanEnd(span))}`;
404
+ return [document.update(newText, ts__namespace["default"].createTextChangeRange(span, 0)), node];
403
405
  }
404
406
  function parseAndValidateArrayInsertIndex(key, nodes) {
405
407
  if (key === "-") {
@@ -441,9 +443,9 @@ function parseAndValidateArrayInboundsIndex(key, nodes) {
441
443
  }));
442
444
  }
443
445
  function replaceInNode(document, node, key, value) {
444
- if (ts__default["default"].isArrayLiteralExpression(node)) {
446
+ if (ts__namespace["default"].isArrayLiteralExpression(node)) {
445
447
  return fp.pipe(parseAndValidateArrayInboundsIndex(key, node.elements), fp.result.map(index => replaceNodeValue(document, node.elements[index], value)));
446
- } else if (ts__default["default"].isObjectLiteralExpression(node)) {
448
+ } else if (ts__namespace["default"].isObjectLiteralExpression(node)) {
447
449
  return fp.pipe(findObjectPropertyAssignment(node, key), fp.result.flatMap(assignment => {
448
450
  if (!assignment) {
449
451
  return fp.result.err(new patch.PatchError("Cannot replace object element which does not exist"));
@@ -466,7 +468,7 @@ function replaceInNode(document, node, key, value) {
466
468
  }
467
469
  return replaceInNode(document,
468
470
  // TODO: creating a fake object here might not be right - seems to work though
469
- ts__default["default"].factory.createObjectLiteralExpression([ts__default["default"].factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
471
+ ts__namespace["default"].factory.createObjectLiteralExpression([ts__namespace["default"].factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
470
472
  }));
471
473
  }
472
474
  } else {
@@ -481,9 +483,9 @@ function replaceAtPath(document, rootNode, path, value) {
481
483
  }
482
484
  }
483
485
  function getFromNode(node, key) {
484
- if (ts__default["default"].isArrayLiteralExpression(node)) {
486
+ if (ts__namespace["default"].isArrayLiteralExpression(node)) {
485
487
  return fp.pipe(parseAndValidateArrayInboundsIndex(key, node.elements), fp.result.map(index => node.elements[index]));
486
- } else if (ts__default["default"].isObjectLiteralExpression(node)) {
488
+ } else if (ts__namespace["default"].isObjectLiteralExpression(node)) {
487
489
  return fp.pipe(findObjectPropertyAssignment(node, key), fp.result.map(assignment => assignment === null || assignment === void 0 ? void 0 : assignment.initializer));
488
490
  } else if (isValFileMethodCall(node)) {
489
491
  if (key === core.FILE_REF_PROP) {
@@ -513,9 +515,9 @@ function getAtPath(rootNode, path) {
513
515
  return fp.pipe(path, fp.result.flatMapReduce((node, key) => fp.pipe(getFromNode(node, key), fp.result.filterOrElse(childNode => childNode !== undefined, () => new patch.PatchError("Path refers to non-existing object/array"))), rootNode));
514
516
  }
515
517
  function removeFromNode(document, node, key) {
516
- if (ts__default["default"].isArrayLiteralExpression(node)) {
518
+ if (ts__namespace["default"].isArrayLiteralExpression(node)) {
517
519
  return fp.pipe(parseAndValidateArrayInboundsIndex(key, node.elements), fp.result.map(index => removeAt(document, node.elements, index)));
518
- } else if (ts__default["default"].isObjectLiteralExpression(node)) {
520
+ } else if (ts__namespace["default"].isObjectLiteralExpression(node)) {
519
521
  return fp.pipe(findObjectPropertyAssignment(node, key), fp.result.flatMap(assignment => {
520
522
  if (!assignment) {
521
523
  return fp.result.err(new patch.PatchError("Cannot replace object element which does not exist"));
@@ -561,9 +563,9 @@ function isValRichTextValue(value) {
561
563
  return !!(typeof value === "object" && value && core.VAL_EXTENSION in value && value[core.VAL_EXTENSION] === "richtext" && "templateStrings" in value && typeof value.templateStrings === "object" && Array.isArray(value.templateStrings));
562
564
  }
563
565
  function addToNode(document, node, key, value) {
564
- if (ts__default["default"].isArrayLiteralExpression(node)) {
566
+ if (ts__namespace["default"].isArrayLiteralExpression(node)) {
565
567
  return fp.pipe(parseAndValidateArrayInsertIndex(key, node.elements), fp.result.map(index => [insertAt(document, node.elements, index, toExpression(value))]));
566
- } else if (ts__default["default"].isObjectLiteralExpression(node)) {
568
+ } else if (ts__namespace["default"].isObjectLiteralExpression(node)) {
567
569
  if (key === core.FILE_REF_PROP) {
568
570
  return fp.result.err(new patch.PatchError("Cannot add a key ref to object"));
569
571
  }
@@ -632,7 +634,7 @@ class TSOps {
632
634
  }
633
635
  }
634
636
 
635
- const ops = new TSOps(document => {
637
+ const ops$1 = new TSOps(document => {
636
638
  return fp.pipe(analyzeValModule(document), fp.result.map(({
637
639
  source
638
640
  }) => source));
@@ -647,7 +649,7 @@ const patchValFile = async (id, valConfigPath, patch$1, sourceFileHandler, runti
647
649
  if (!sourceFile) {
648
650
  throw Error(`Source file ${filePath} not found`);
649
651
  }
650
- const derefRes = core.derefPatch(patch$1, sourceFile, ops);
652
+ const derefRes = core.derefPatch(patch$1, sourceFile, ops$1);
651
653
  if (fp.result.isErr(derefRes)) {
652
654
  throw derefRes.error;
653
655
  }
@@ -685,9 +687,9 @@ function convertDataUrlToBase64(dataUrl) {
685
687
  }
686
688
  const patchSourceFile = (sourceFile, patch$1) => {
687
689
  if (typeof sourceFile === "string") {
688
- return patch.applyPatch(ts__default["default"].createSourceFile("<val>", sourceFile, ts__default["default"].ScriptTarget.ES2015), ops, patch$1);
690
+ return patch.applyPatch(ts__namespace["default"].createSourceFile("<val>", sourceFile, ts__namespace["default"].ScriptTarget.ES2015), ops$1, patch$1);
689
691
  }
690
- return patch.applyPatch(sourceFile, ops, patch$1);
692
+ return patch.applyPatch(sourceFile, ops$1, patch$1);
691
693
  };
692
694
 
693
695
  const readValFile = async (id, valConfigPath, runtime) => {
@@ -801,7 +803,7 @@ const getCompilerOptions = (rootDir, parseConfigHost) => {
801
803
  const {
802
804
  config,
803
805
  error
804
- } = ts__default["default"].readConfigFile(configFilePath, parseConfigHost.readFile.bind(parseConfigHost));
806
+ } = ts__namespace["default"].readConfigFile(configFilePath, parseConfigHost.readFile.bind(parseConfigHost));
805
807
  if (error) {
806
808
  if (typeof error.messageText === "string") {
807
809
  throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText}`);
@@ -809,7 +811,7 @@ const getCompilerOptions = (rootDir, parseConfigHost) => {
809
811
  throw Error(`Could not parse config file: ${configFilePath}. Error: ${error.messageText.messageText}`);
810
812
  }
811
813
  const optionsOverrides = undefined;
812
- const parsedConfigFile = ts__default["default"].parseJsonConfigFileContent(config, parseConfigHost, rootDir, optionsOverrides, configFilePath);
814
+ const parsedConfigFile = ts__namespace["default"].parseJsonConfigFileContent(config, parseConfigHost, rootDir, optionsOverrides, configFilePath);
813
815
  if (parsedConfigFile.errors.length > 0) {
814
816
  throw Error(`Could not parse config file: ${configFilePath}. Errors: ${parsedConfigFile.errors.map(e => e.messageText).join("\n")}`);
815
817
  }
@@ -818,8 +820,14 @@ const getCompilerOptions = (rootDir, parseConfigHost) => {
818
820
 
819
821
  class ValSourceFileHandler {
820
822
  constructor(projectRoot, compilerOptions, host = {
821
- ...ts__default["default"].sys,
822
- writeFile: fs__default["default"].writeFileSync
823
+ ...ts__namespace["default"].sys,
824
+ writeFile: (fileName, data, encoding) => {
825
+ fs__default["default"].mkdirSync(path__namespace["default"].dirname(fileName), {
826
+ recursive: true
827
+ });
828
+ fs__default["default"].writeFileSync(fileName, data, encoding);
829
+ },
830
+ rmFile: fs__default["default"].rmSync
823
831
  }) {
824
832
  this.projectRoot = projectRoot;
825
833
  this.compilerOptions = compilerOptions;
@@ -827,9 +835,9 @@ class ValSourceFileHandler {
827
835
  }
828
836
  getSourceFile(filePath) {
829
837
  const fileContent = this.host.readFile(filePath);
830
- const scriptTarget = this.compilerOptions.target ?? ts__default["default"].ScriptTarget.ES2020;
838
+ const scriptTarget = this.compilerOptions.target ?? ts__namespace["default"].ScriptTarget.ES2020;
831
839
  if (fileContent) {
832
- return ts__default["default"].createSourceFile(filePath, fileContent, scriptTarget);
840
+ return ts__namespace["default"].createSourceFile(filePath, fileContent, scriptTarget);
833
841
  }
834
842
  }
835
843
  writeSourceFile(sourceFile) {
@@ -841,7 +849,7 @@ class ValSourceFileHandler {
841
849
  this.host.writeFile(filePath, content, encoding);
842
850
  }
843
851
  resolveSourceModulePath(containingFilePath, requestedModuleName) {
844
- const resolutionRes = ts__default["default"].resolveModuleName(requestedModuleName, path__namespace["default"].isAbsolute(containingFilePath) ? containingFilePath : path__namespace["default"].resolve(this.projectRoot, containingFilePath), this.compilerOptions, this.host, undefined, undefined, ts__default["default"].ModuleKind.ESNext);
852
+ const resolutionRes = ts__namespace["default"].resolveModuleName(requestedModuleName, path__namespace["default"].isAbsolute(containingFilePath) ? containingFilePath : path__namespace["default"].resolve(this.projectRoot, containingFilePath), this.compilerOptions, this.host, undefined, undefined, ts__namespace["default"].ModuleKind.ESNext);
845
853
  const resolvedModule = resolutionRes.resolvedModule;
846
854
  if (!resolvedModule) {
847
855
  throw Error(`Could not resolve module "${requestedModuleName}", base: "${containingFilePath}": No resolved modules returned: ${JSON.stringify(resolutionRes)}`);
@@ -853,7 +861,7 @@ class ValSourceFileHandler {
853
861
  const JsFileLookupMapping = [
854
862
  // NOTE: first one matching will be used
855
863
  [".cjs.d.ts", [".esm.js", ".mjs.js"]], [".cjs.js", [".esm.js", ".mjs.js"]], [".cjs", [".mjs"]], [".d.ts", [".js", ".esm.js", ".mjs.js"]]];
856
- const MAX_CACHE_SIZE = 10 * 1024 * 1024; // 10 mb
864
+ const MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100 mb
857
865
  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
858
866
 
859
867
  class ValModuleLoader {
@@ -862,8 +870,14 @@ class ValModuleLoader {
862
870
  compilerOptions,
863
871
  // TODO: remove this?
864
872
  sourceFileHandler, host = {
865
- ...ts__default["default"].sys,
866
- writeFile: fs__default["default"].writeFileSync
873
+ ...ts__namespace["default"].sys,
874
+ writeFile: (fileName, data, encoding) => {
875
+ fs__default["default"].mkdirSync(path__namespace["default"].dirname(fileName), {
876
+ recursive: true
877
+ });
878
+ fs__default["default"].writeFileSync(fileName, data, encoding);
879
+ },
880
+ rmFile: fs__default["default"].rmSync
867
881
  }, disableCache = false) {
868
882
  this.projectRoot = projectRoot;
869
883
  this.compilerOptions = compilerOptions;
@@ -1116,8 +1130,14 @@ export const IS_DEV = false;2
1116
1130
  }
1117
1131
 
1118
1132
  async function createService(projectRoot, opts, host = {
1119
- ...ts__default["default"].sys,
1120
- writeFile: fs__default["default"].writeFileSync
1133
+ ...ts__namespace["default"].sys,
1134
+ writeFile: (fileName, data, encoding) => {
1135
+ fs__default["default"].mkdirSync(path__namespace["default"].dirname(fileName), {
1136
+ recursive: true
1137
+ });
1138
+ fs__default["default"].writeFileSync(fileName, data, encoding);
1139
+ },
1140
+ rmFile: fs__default["default"].rmSync
1121
1141
  }, loader) {
1122
1142
  const compilerOptions = getCompilerOptions(projectRoot, host);
1123
1143
  const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
@@ -1133,7 +1153,7 @@ class Service {
1133
1153
  this.runtime = runtime;
1134
1154
  this.valConfigPath = valConfigPath || "./val.config";
1135
1155
  }
1136
- async get(moduleId, modulePath) {
1156
+ async get(moduleId, modulePath = "") {
1137
1157
  const valModule = await readValFile(moduleId, this.valConfigPath, this.runtime);
1138
1158
  if (valModule.source && valModule.schema) {
1139
1159
  const resolved = core.Internal.resolvePath(modulePath, valModule.source, valModule.schema);
@@ -1154,7 +1174,7 @@ class Service {
1154
1174
  }
1155
1175
  }
1156
1176
  async patch(moduleId, patch) {
1157
- return patchValFile(moduleId, this.valConfigPath, patch, this.sourceFileHandler, this.runtime);
1177
+ await patchValFile(moduleId, this.valConfigPath, patch, this.sourceFileHandler, this.runtime);
1158
1178
  }
1159
1179
  dispose() {
1160
1180
  this.runtime.dispose();
@@ -1202,92 +1222,157 @@ const OperationJSONT = z__default["default"].discriminatedUnion("op", [z__defaul
1202
1222
  value: z__default["default"].string()
1203
1223
  }).strict()]);
1204
1224
  const PatchJSON = z__default["default"].array(OperationJSONT);
1225
+ /**
1226
+ * Raw JSON patch operation.
1227
+ */
1228
+ const OperationT = z__default["default"].discriminatedUnion("op", [z__default["default"].object({
1229
+ op: z__default["default"].literal("add"),
1230
+ path: z__default["default"].array(z__default["default"].string()),
1231
+ value: JSONValueT
1232
+ }).strict(), z__default["default"].object({
1233
+ op: z__default["default"].literal("remove"),
1234
+ path: z__default["default"].array(z__default["default"].string()).nonempty()
1235
+ }).strict(), z__default["default"].object({
1236
+ op: z__default["default"].literal("replace"),
1237
+ path: z__default["default"].array(z__default["default"].string()),
1238
+ value: JSONValueT
1239
+ }).strict(), z__default["default"].object({
1240
+ op: z__default["default"].literal("move"),
1241
+ from: z__default["default"].array(z__default["default"].string()).nonempty(),
1242
+ path: z__default["default"].array(z__default["default"].string())
1243
+ }).strict(), z__default["default"].object({
1244
+ op: z__default["default"].literal("copy"),
1245
+ from: z__default["default"].array(z__default["default"].string()),
1246
+ path: z__default["default"].array(z__default["default"].string())
1247
+ }).strict(), z__default["default"].object({
1248
+ op: z__default["default"].literal("test"),
1249
+ path: z__default["default"].array(z__default["default"].string()),
1250
+ value: JSONValueT
1251
+ }).strict(), z__default["default"].object({
1252
+ op: z__default["default"].literal("file"),
1253
+ path: z__default["default"].array(z__default["default"].string()),
1254
+ filePath: z__default["default"].string(),
1255
+ value: z__default["default"].union([z__default["default"].string(), z__default["default"].object({
1256
+ sha256: z__default["default"].string(),
1257
+ mimeType: z__default["default"].string()
1258
+ })])
1259
+ }).strict()]);
1260
+ const Patch = z__default["default"].array(OperationT);
1205
1261
 
1206
- const ENABLE_COOKIE_VALUE = {
1207
- value: "true",
1208
- options: {
1209
- httpOnly: false,
1210
- sameSite: "lax"
1211
- }
1212
- };
1213
- function getRedirectUrl(query, overrideHost) {
1214
- if (typeof query.redirect_to !== "string") {
1215
- return {
1216
- status: 400,
1217
- json: {
1218
- message: "Missing redirect_to query param"
1219
- }
1220
- };
1262
+ function getValidationErrorFileRef(validationError) {
1263
+ const maybeRef = validationError.value && typeof validationError.value === "object" && core.FILE_REF_PROP in validationError.value && typeof validationError.value[core.FILE_REF_PROP] === "string" ? validationError.value[core.FILE_REF_PROP] : undefined;
1264
+ if (!maybeRef) {
1265
+ return null;
1221
1266
  }
1222
- if (overrideHost) {
1223
- return overrideHost + "?redirect_to=" + encodeURIComponent(query.redirect_to);
1267
+ return maybeRef;
1268
+ }
1269
+
1270
+ const textEncoder$1 = new TextEncoder();
1271
+ async function extractImageMetadata(filename, input) {
1272
+ const imageSize = sizeOf__default["default"](input);
1273
+ let mimeType = null;
1274
+ if (imageSize.type) {
1275
+ const possibleMimeType = `image/${imageSize.type}`;
1276
+ if (internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
1277
+ mimeType = possibleMimeType;
1278
+ }
1279
+ const filenameBasedLookup = internal.filenameToMimeType(filename);
1280
+ if (filenameBasedLookup) {
1281
+ mimeType = filenameBasedLookup;
1282
+ }
1283
+ }
1284
+ if (!mimeType) {
1285
+ mimeType = "application/octet-stream";
1286
+ }
1287
+ let {
1288
+ width,
1289
+ height
1290
+ } = imageSize;
1291
+ if (!width || !height) {
1292
+ width = 0;
1293
+ height = 0;
1294
+ }
1295
+ const sha256 = getSha256(mimeType, input);
1296
+ return {
1297
+ width,
1298
+ height,
1299
+ sha256,
1300
+ mimeType
1301
+ };
1302
+ }
1303
+ function getSha256(mimeType, input) {
1304
+ return core.Internal.getSHA256Hash(textEncoder$1.encode(
1305
+ // TODO: we should probably store the mimetype in the metadata and reuse it here
1306
+ `data:${mimeType};base64,${input.toString("base64")}`));
1307
+ }
1308
+ async function extractFileMetadata(filename, input) {
1309
+ let mimeType = internal.filenameToMimeType(filename);
1310
+ if (!mimeType) {
1311
+ mimeType = "application/octet-stream";
1224
1312
  }
1225
- return query.redirect_to;
1313
+ const sha256 = getSha256(mimeType, input);
1314
+ return {
1315
+ sha256,
1316
+ mimeType
1317
+ };
1226
1318
  }
1227
1319
 
1228
- class LocalValServer {
1229
- constructor(options, callbacks) {
1230
- this.options = options;
1231
- this.callbacks = callbacks;
1320
+ function validateMetadata(actualMetadata, expectedMetadata) {
1321
+ const missingMetadata = [];
1322
+ const erroneousMetadata = {};
1323
+ if (typeof actualMetadata !== "object" || actualMetadata === null) {
1324
+ return {
1325
+ globalErrors: ["Metadata is wrong type: must be an object."]
1326
+ };
1232
1327
  }
1233
- async session() {
1328
+ if (Array.isArray(actualMetadata)) {
1234
1329
  return {
1235
- status: 200,
1236
- json: {
1237
- mode: "local",
1238
- enabled: await this.callbacks.isEnabled()
1239
- }
1330
+ globalErrors: ["Metadata is wrong type: cannot be an array."]
1240
1331
  };
1241
1332
  }
1242
- async getTree(treePath,
1243
- // TODO: use the params: patch, schema, source
1244
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1245
- query) {
1246
- const rootDir = process.cwd();
1247
- const moduleIds = [];
1248
- // iterate over all .val files in the root directory
1249
- const walk = async dir => {
1250
- const files = await fs.promises.readdir(dir);
1251
- for (const file of files) {
1252
- if ((await fs.promises.stat(path__namespace["default"].join(dir, file))).isDirectory()) {
1253
- if (file === "node_modules") continue;
1254
- await walk(path__namespace["default"].join(dir, file));
1255
- } else {
1256
- const isValFile = file.endsWith(".val.js") || file.endsWith(".val.ts");
1257
- if (!isValFile) {
1258
- continue;
1259
- }
1260
- if (treePath && !path__namespace["default"].join(dir, file).replace(rootDir, "").startsWith(treePath)) {
1261
- continue;
1262
- }
1263
- moduleIds.push(path__namespace["default"].join(dir, file).replace(rootDir, "").replace(".val.js", "").replace(".val.ts", "").split(path__namespace["default"].sep).join("/"));
1333
+ const recordMetadata = actualMetadata;
1334
+ const globalErrors = [];
1335
+ for (const anyKey in expectedMetadata) {
1336
+ if (typeof anyKey !== "string") {
1337
+ globalErrors.push(`Expected metadata has key '${anyKey}' that is not typeof 'string', but: '${typeof anyKey}'. This is most likely a Val bug.`);
1338
+ } else {
1339
+ if (anyKey in actualMetadata) {
1340
+ const key = anyKey;
1341
+ if (expectedMetadata[key] !== recordMetadata[key]) {
1342
+ erroneousMetadata[key] = `Expected metadata '${key}' to be ${JSON.stringify(expectedMetadata[key])}, but got ${JSON.stringify(recordMetadata[key])}.`;
1264
1343
  }
1344
+ } else {
1345
+ missingMetadata.push(anyKey);
1265
1346
  }
1266
- };
1267
- const serializedModuleContent = await walk(rootDir).then(async () => {
1268
- return Promise.all(moduleIds.map(async moduleId => {
1269
- return await this.options.service.get(moduleId, "");
1270
- }));
1271
- });
1347
+ }
1348
+ }
1349
+ if (globalErrors.length === 0 && missingMetadata.length === 0 && Object.keys(erroneousMetadata).length === 0) {
1350
+ return false;
1351
+ }
1352
+ return {
1353
+ missingMetadata,
1354
+ erroneousMetadata
1355
+ };
1356
+ }
1272
1357
 
1273
- //
1274
- const modules = Object.fromEntries(serializedModuleContent.map(serializedModuleContent => {
1275
- const module = {
1276
- schema: serializedModuleContent.schema,
1277
- source: serializedModuleContent.source,
1278
- errors: serializedModuleContent.errors
1279
- };
1280
- return [serializedModuleContent.path, module];
1281
- }));
1282
- const apiTreeResponse = {
1283
- modules,
1284
- git: this.options.git
1285
- };
1286
- return {
1287
- status: 200,
1288
- json: apiTreeResponse
1289
- };
1358
+ function getValidationErrorMetadata(validationError) {
1359
+ const maybeMetadata = validationError.value && typeof validationError.value === "object" && "metadata" in validationError.value && validationError.value.metadata && validationError.value.metadata;
1360
+ if (!maybeMetadata) {
1361
+ return null;
1362
+ }
1363
+ return maybeMetadata;
1364
+ }
1365
+
1366
+ const ops = new patch.JSONOps();
1367
+ class ValServer {
1368
+ constructor(cwd, host, options, callbacks) {
1369
+ this.cwd = cwd;
1370
+ this.host = host;
1371
+ this.options = options;
1372
+ this.callbacks = callbacks;
1290
1373
  }
1374
+
1375
+ /* Auth endpoints: */
1291
1376
  async enable(query) {
1292
1377
  const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
1293
1378
  if (typeof redirectToRes !== "string") {
@@ -1318,51 +1403,856 @@ class LocalValServer {
1318
1403
  redirectTo: redirectToRes
1319
1404
  };
1320
1405
  }
1321
- async postPatches(body) {
1322
- // First validate that the body has the right structure
1323
- const patchJSON = z.z.record(PatchJSON).safeParse(body);
1324
- if (!patchJSON.success) {
1406
+ getAllModules(treePath) {
1407
+ const moduleIds = this.host.readDirectory(this.cwd, ["ts", "js"], ["node_modules", ".*"], ["**/*.val.ts", "**/*.val.js"]).filter(file => {
1408
+ if (treePath) {
1409
+ return file.replace(this.cwd, "").startsWith(treePath);
1410
+ }
1411
+ return true;
1412
+ }).map(file => file.replace(this.cwd, "").replace(".val.js", "").replace(".val.ts", "").split(path__namespace["default"].sep).join("/"));
1413
+ return moduleIds;
1414
+ }
1415
+ async getTree(treePath,
1416
+ // TODO: use the params: patch, schema, source now we return everything, every time
1417
+ query, cookies) {
1418
+ const ensureRes = await this.ensureRemoteFSInitialized("getTree", cookies);
1419
+ if (fp.result.isErr(ensureRes)) {
1420
+ return ensureRes.error;
1421
+ }
1422
+ const moduleIds = this.getAllModules(treePath);
1423
+ const applyPatches = query.patch === "true";
1424
+ let {
1425
+ patchIdsByModuleId,
1426
+ patchesById,
1427
+ fileUpdates
1428
+ } = {
1429
+ patchIdsByModuleId: {},
1430
+ patchesById: {},
1431
+ fileUpdates: {}
1432
+ };
1433
+ if (applyPatches) {
1434
+ const res = await this.readPatches(cookies);
1435
+ if (fp.result.isErr(res)) {
1436
+ return res.error;
1437
+ }
1438
+ patchIdsByModuleId = res.value.patchIdsByModuleId;
1439
+ patchesById = res.value.patchesById;
1440
+ fileUpdates = res.value.fileUpdates;
1441
+ }
1442
+ const possiblyPatchedContent = await Promise.all(moduleIds.map(async moduleId => {
1443
+ return this.applyAllPatchesThenValidate(moduleId, patchIdsByModuleId, patchesById, fileUpdates, applyPatches, cookies);
1444
+ }));
1445
+ const modules = Object.fromEntries(possiblyPatchedContent.map(serializedModuleContent => {
1446
+ const module = {
1447
+ schema: serializedModuleContent.schema,
1448
+ source: serializedModuleContent.source,
1449
+ errors: serializedModuleContent.errors
1450
+ };
1451
+ return [serializedModuleContent.path, module];
1452
+ }));
1453
+ const apiTreeResponse = {
1454
+ modules,
1455
+ git: this.options.git
1456
+ };
1457
+ return {
1458
+ status: 200,
1459
+ json: apiTreeResponse
1460
+ };
1461
+ }
1462
+ async postValidate(rawBody, cookies) {
1463
+ const ensureRes = await this.ensureRemoteFSInitialized("postValidate", cookies);
1464
+ if (fp.result.isErr(ensureRes)) {
1465
+ return ensureRes.error;
1466
+ }
1467
+ return this.validateThenMaybeCommit(rawBody, false, cookies);
1468
+ }
1469
+ async postCommit(rawBody, cookies) {
1470
+ const ensureRes = await this.ensureRemoteFSInitialized("postCommit", cookies);
1471
+ if (fp.result.isErr(ensureRes)) {
1472
+ return ensureRes.error;
1473
+ }
1474
+ const res = await this.validateThenMaybeCommit(rawBody, true, cookies);
1475
+ if (res.status === 200) {
1476
+ if (res.json.validationErrors) {
1477
+ return {
1478
+ status: 400,
1479
+ json: {
1480
+ ...res.json
1481
+ }
1482
+ };
1483
+ }
1325
1484
  return {
1326
- status: 404,
1485
+ status: 200,
1327
1486
  json: {
1328
- message: `Invalid patch: ${patchJSON.error.message}`,
1329
- details: patchJSON.error.issues
1487
+ ...res.json,
1488
+ git: this.options.git
1330
1489
  }
1331
1490
  };
1332
1491
  }
1492
+ return res;
1493
+ }
1494
+
1495
+ /* */
1496
+
1497
+ async applyAllPatchesThenValidate(moduleId, patchIdsByModuleId, patchesById, fileUpdates, applyPatches, cookies) {
1498
+ const serializedModuleContent = await this.getModule(moduleId);
1499
+ const schema = serializedModuleContent.schema;
1500
+ const maybeSource = serializedModuleContent.source;
1501
+ if (!applyPatches) {
1502
+ return serializedModuleContent;
1503
+ }
1504
+ if (serializedModuleContent.errors && (serializedModuleContent.errors.fatal || serializedModuleContent.errors.invalidModuleId)) {
1505
+ return serializedModuleContent;
1506
+ }
1507
+ if (!maybeSource || !schema) {
1508
+ return serializedModuleContent;
1509
+ }
1510
+ let source = maybeSource;
1511
+ for (const patchId of patchIdsByModuleId[moduleId] ?? []) {
1512
+ const patch$1 = patchesById[patchId];
1513
+ if (!patch$1) {
1514
+ continue;
1515
+ }
1516
+ const patchRes = patch.applyPatch(source, ops, patch$1.filter(core.Internal.notFileOp));
1517
+ if (fp.result.isOk(patchRes)) {
1518
+ source = patchRes.value;
1519
+ } else {
1520
+ console.error("Val: got an unexpected error while applying patch. Is there a mismatch in Val versions? Perhaps Val is misconfigured?", {
1521
+ patchId,
1522
+ moduleId,
1523
+ patch: JSON.stringify(patch$1, null, 2),
1524
+ error: patchRes.error
1525
+ });
1526
+ return {
1527
+ path: moduleId,
1528
+ schema,
1529
+ source,
1530
+ errors: {
1531
+ fatal: [{
1532
+ message: "Unexpected error applying patch",
1533
+ type: "invalid-patch"
1534
+ }]
1535
+ }
1536
+ };
1537
+ }
1538
+ }
1539
+ const validationErrors = core.deserializeSchema(schema).validate(moduleId, source);
1540
+ if (validationErrors) {
1541
+ const revalidated = await this.revalidateImageAndFileValidation(validationErrors, fileUpdates, cookies);
1542
+ return {
1543
+ path: moduleId,
1544
+ schema,
1545
+ source,
1546
+ errors: revalidated && {
1547
+ validation: revalidated
1548
+ }
1549
+ };
1550
+ }
1551
+ return {
1552
+ path: moduleId,
1553
+ schema,
1554
+ source,
1555
+ errors: false
1556
+ };
1557
+ }
1558
+
1559
+ // TODO: name this better: we need to check for image and file validation errors
1560
+ // since they cannot be handled directly inside the validation function.
1561
+ // The reason is that validate will be called inside QuickJS (in the future, hopefully),
1562
+ // which does not have access to the filesystem, at least not at the time of writing this comment.
1563
+ // If you are reading this, and we still are not using QuickJS to validate, this assumption might be wrong.
1564
+ async revalidateImageAndFileValidation(validationErrors, fileUpdates, cookies) {
1565
+ const revalidatedValidationErrors = {};
1566
+ for (const pathStr in validationErrors) {
1567
+ const errorSourcePath = pathStr;
1568
+ const errors = validationErrors[errorSourcePath];
1569
+ revalidatedValidationErrors[errorSourcePath] = [];
1570
+ for (const error of errors) {
1571
+ var _error$fixes;
1572
+ 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
1573
+ )) {
1574
+ const fileRef = getValidationErrorFileRef(error);
1575
+ if (fileRef) {
1576
+ const filePath = path__namespace["default"].join(this.cwd, fileRef);
1577
+ let expectedMetadata;
1333
1578
 
1334
- // const id = randomUUID();
1335
- // console.time("patching:" + id);
1336
- for (const moduleId in patchJSON.data) {
1337
- // Then parse/validate
1338
- // TODO: validate all and then fail instead:
1339
- const patch$1 = patch.parsePatch(patchJSON.data[moduleId]);
1340
- if (fp.result.isErr(patch$1)) {
1341
- console.error("Unexpected error parsing patch", patch$1.error);
1342
- throw new Error("Unexpected error parsing patch");
1579
+ // if this is a new file or we have an actual FS, we read the file and get the metadata
1580
+ if (!expectedMetadata) {
1581
+ let fileBuffer = undefined;
1582
+ const updatedFileMetadata = fileUpdates[fileRef];
1583
+ if (updatedFileMetadata) {
1584
+ const fileRes = await this.getFiles(fileRef, {
1585
+ sha256: updatedFileMetadata.sha256
1586
+ }, cookies);
1587
+ if (fileRes.status === 200 && fileRes.body) {
1588
+ const res = new Response(fileRes.body);
1589
+ fileBuffer = Buffer.from(await res.arrayBuffer());
1590
+ } else {
1591
+ console.error("Val: unexpected error while fetching image / file:", fileRef, {
1592
+ error: fileRes
1593
+ });
1594
+ }
1595
+ }
1596
+ if (!fileBuffer) {
1597
+ try {
1598
+ fileBuffer = await this.readStaticBinaryFile(filePath);
1599
+ } catch (err) {
1600
+ console.error("Val: unexpected error while reading image / file:", filePath, {
1601
+ error: err
1602
+ });
1603
+ }
1604
+ }
1605
+ if (!fileBuffer) {
1606
+ revalidatedValidationErrors[errorSourcePath].push({
1607
+ message: `Could not read file: ${filePath}`
1608
+ });
1609
+ continue;
1610
+ }
1611
+ if (error.fixes.some(fix => fix === "image:replace-metadata")) {
1612
+ expectedMetadata = await extractImageMetadata(filePath, fileBuffer);
1613
+ } else {
1614
+ expectedMetadata = await extractFileMetadata(filePath, fileBuffer);
1615
+ }
1616
+ }
1617
+ if (!expectedMetadata) {
1618
+ revalidatedValidationErrors[errorSourcePath].push({
1619
+ message: `Could not read file metadata. Is the reference to the file: ${fileRef} correct?`
1620
+ });
1621
+ } else {
1622
+ const actualMetadata = getValidationErrorMetadata(error);
1623
+ const revalidatedError = validateMetadata(actualMetadata, expectedMetadata);
1624
+ if (!revalidatedError) {
1625
+ // no errors anymore:
1626
+ continue;
1627
+ }
1628
+ const errorMsgs = (revalidatedError.globalErrors || []).concat(Object.values(revalidatedError.erroneousMetadata || {})).concat(Object.values(revalidatedError.missingMetadata || []).map(missingKey => {
1629
+ var _expectedMetadata;
1630
+ return `Required key: '${missingKey}' is not defined. Should be: '${JSON.stringify((_expectedMetadata = expectedMetadata) === null || _expectedMetadata === void 0 ? void 0 : _expectedMetadata[missingKey])}'`;
1631
+ }));
1632
+ revalidatedValidationErrors[errorSourcePath].push(...errorMsgs.map(message => ({
1633
+ message
1634
+ })));
1635
+ }
1636
+ } else {
1637
+ revalidatedValidationErrors[errorSourcePath].push(error);
1638
+ }
1639
+ } else {
1640
+ revalidatedValidationErrors[errorSourcePath].push(error);
1641
+ }
1343
1642
  }
1344
- await this.options.service.patch(moduleId, patch$1.value);
1345
1643
  }
1346
- // console.timeEnd("patching:" + id);
1644
+ const hasErrors = Object.values(revalidatedValidationErrors).some(errors => errors.length > 0);
1645
+ if (hasErrors) {
1646
+ return revalidatedValidationErrors;
1647
+ }
1648
+ return hasErrors;
1649
+ }
1650
+ sortPatchIds(patchesByModule) {
1651
+ return Object.values(patchesByModule).flatMap(modulePatches => modulePatches).sort((a, b) => {
1652
+ return a.created_at.localeCompare(b.created_at);
1653
+ }).map(patchData => patchData.patch_id);
1654
+ }
1347
1655
 
1656
+ // can be overridden if FS cannot read from static assets / public folder (because of bundlers or what not)
1657
+ async readStaticBinaryFile(filePath) {
1658
+ return fs__default["default"].promises.readFile(filePath);
1659
+ }
1660
+ async readPatches(cookies) {
1661
+ const res = await this.getPatches({},
1662
+ // {} means no ids, so get all patches
1663
+ cookies);
1664
+ if (res.status === 400 || res.status === 401 || res.status === 403 || res.status === 404 || res.status === 500 || res.status === 501) {
1665
+ return fp.result.err(res);
1666
+ } else if (res.status === 200 || res.status === 201) {
1667
+ const patchesByModule = res.json;
1668
+ const patches = [];
1669
+ const patchIdsByModuleId = {};
1670
+ const patchesById = {};
1671
+ for (const [moduleIdS, modulePatchData] of Object.entries(patchesByModule)) {
1672
+ const moduleId = moduleIdS;
1673
+ patchIdsByModuleId[moduleId] = modulePatchData.map(patch => patch.patch_id);
1674
+ for (const patchData of modulePatchData) {
1675
+ patches.push([patchData.patch_id, moduleId, patchData.patch]);
1676
+ patchesById[patchData.patch_id] = patchData.patch;
1677
+ }
1678
+ }
1679
+ const fileUpdates = {};
1680
+ const sortedPatchIds = this.sortPatchIds(patchesByModule);
1681
+ for (const sortedPatchId of sortedPatchIds) {
1682
+ const patchId = sortedPatchId;
1683
+ for (const op of patchesById[patchId] || []) {
1684
+ if (op.op === "file") {
1685
+ const parsedFileOp = z.z.object({
1686
+ sha256: z.z.string(),
1687
+ mimeType: z.z.string()
1688
+ }).safeParse(op.value);
1689
+ if (!parsedFileOp.success) {
1690
+ return fp.result.err({
1691
+ status: 500,
1692
+ json: {
1693
+ message: "Unexpected error: file op value must be transformed into object",
1694
+ details: {
1695
+ value: "First 200 chars: " + JSON.stringify(op.value).slice(0, 200),
1696
+ patchId
1697
+ }
1698
+ }
1699
+ });
1700
+ }
1701
+ fileUpdates[op.filePath] = {
1702
+ ...parsedFileOp.data
1703
+ };
1704
+ }
1705
+ }
1706
+ }
1707
+ return fp.result.ok({
1708
+ patches,
1709
+ patchIdsByModuleId,
1710
+ patchesById,
1711
+ fileUpdates
1712
+ });
1713
+ } else {
1714
+ return fp.result.err({
1715
+ status: 500,
1716
+ json: {
1717
+ message: "Unknown error"
1718
+ }
1719
+ });
1720
+ }
1721
+ }
1722
+ async validateThenMaybeCommit(rawBody, commit, cookies) {
1723
+ const filterPatchesByModuleIdRes = z.z.object({
1724
+ patches: z.z.record(z.z.array(z.z.string())).optional()
1725
+ }).safeParse(rawBody);
1726
+ if (!filterPatchesByModuleIdRes.success) {
1727
+ return {
1728
+ status: 404,
1729
+ json: {
1730
+ message: "Could not parse body",
1731
+ details: filterPatchesByModuleIdRes.error
1732
+ }
1733
+ };
1734
+ }
1735
+ const res = await this.readPatches(cookies);
1736
+ if (fp.result.isErr(res)) {
1737
+ return res.error;
1738
+ }
1739
+ const {
1740
+ patchIdsByModuleId,
1741
+ patchesById,
1742
+ patches,
1743
+ fileUpdates
1744
+ } = res.value;
1745
+ const validationErrorsByModuleId = {};
1746
+ for (const moduleIdStr of this.getAllModules("/")) {
1747
+ const moduleId = moduleIdStr;
1748
+ const serializedModuleContent = await this.applyAllPatchesThenValidate(moduleId, filterPatchesByModuleIdRes.data.patches ||
1749
+ // TODO: refine to ModuleId and PatchId when parsing
1750
+ patchIdsByModuleId, patchesById, fileUpdates, true, cookies);
1751
+ if (serializedModuleContent.errors) {
1752
+ validationErrorsByModuleId[moduleId] = serializedModuleContent;
1753
+ }
1754
+ }
1755
+ if (Object.keys(validationErrorsByModuleId).length > 0) {
1756
+ const modules = {};
1757
+ for (const [patchId, moduleId] of patches) {
1758
+ if (!modules[moduleId]) {
1759
+ modules[moduleId] = {
1760
+ patches: {
1761
+ applied: []
1762
+ }
1763
+ };
1764
+ }
1765
+ if (validationErrorsByModuleId[moduleId]) {
1766
+ var _modules$moduleId$pat;
1767
+ if (!modules[moduleId].patches.failed) {
1768
+ modules[moduleId].patches.failed = [];
1769
+ }
1770
+ (_modules$moduleId$pat = modules[moduleId].patches.failed) === null || _modules$moduleId$pat === void 0 || _modules$moduleId$pat.push(patchId);
1771
+ } else {
1772
+ modules[moduleId].patches.applied.push(patchId);
1773
+ }
1774
+ }
1775
+ return {
1776
+ status: 200,
1777
+ json: {
1778
+ modules,
1779
+ validationErrors: validationErrorsByModuleId
1780
+ }
1781
+ };
1782
+ }
1783
+ let modules;
1784
+ if (commit) {
1785
+ modules = await this.execCommit(patches, cookies);
1786
+ } else {
1787
+ modules = await this.getPatchedModules(patches);
1788
+ }
1348
1789
  return {
1349
1790
  status: 200,
1350
- json: {} // no patch ids created
1791
+ json: {
1792
+ modules,
1793
+ validationErrors: false
1794
+ }
1351
1795
  };
1352
1796
  }
1353
- badRequest() {
1797
+ async getPatchedModules(patches) {
1798
+ const modules = {};
1799
+ for (const [patchId, moduleId] of patches) {
1800
+ if (!modules[moduleId]) {
1801
+ modules[moduleId] = {
1802
+ patches: {
1803
+ applied: []
1804
+ }
1805
+ };
1806
+ }
1807
+ modules[moduleId].patches.applied.push(patchId);
1808
+ }
1809
+ return modules;
1810
+ }
1811
+
1812
+ /* Abstract methods */
1813
+
1814
+ /**
1815
+ * Runs before remoteFS dependent methods (e.g.getModule, ...) are called to make sure that:
1816
+ * 1) The remote FS, if applicable, is initialized
1817
+ * 2) The error is returned via API if the remote FS could not be initialized
1818
+ * */
1819
+
1820
+ /* Abstract endpoints */
1821
+
1822
+ /* Abstract auth endpoints: */
1823
+
1824
+ /* Abstract patch endpoints: */
1825
+ }
1826
+
1827
+ // From slightly modified ChatGPT generated
1828
+ function bufferToReadableStream(buffer) {
1829
+ const stream = new ReadableStream({
1830
+ start(controller) {
1831
+ const chunkSize = 1024; // Adjust the chunk size as needed
1832
+ let offset = 0;
1833
+ function push() {
1834
+ const chunk = buffer.subarray(offset, offset + chunkSize);
1835
+ offset += chunkSize;
1836
+ if (chunk.length > 0) {
1837
+ controller.enqueue(new Uint8Array(chunk));
1838
+ setTimeout(push, 0); // Enqueue the next chunk asynchronously
1839
+ } else {
1840
+ controller.close();
1841
+ }
1842
+ }
1843
+ push();
1844
+ }
1845
+ });
1846
+ return stream;
1847
+ }
1848
+ const ENABLE_COOKIE_VALUE = {
1849
+ value: "true",
1850
+ options: {
1851
+ httpOnly: false,
1852
+ sameSite: "lax"
1853
+ }
1854
+ };
1855
+ function getRedirectUrl(query, overrideHost) {
1856
+ if (typeof query.redirect_to !== "string") {
1354
1857
  return {
1355
1858
  status: 400,
1356
1859
  json: {
1357
- message: "Local server does not handle this request"
1860
+ message: "Missing redirect_to query param"
1358
1861
  }
1359
1862
  };
1360
1863
  }
1361
-
1362
- // eslint-disable-next-line @typescript-eslint/ban-types
1363
- async postCommit() {
1364
- return this.badRequest();
1864
+ if (overrideHost) {
1865
+ return overrideHost + "?redirect_to=" + encodeURIComponent(query.redirect_to);
1365
1866
  }
1867
+ return query.redirect_to;
1868
+ }
1869
+ const base64DataAttr = "data:";
1870
+ function getMimeTypeFromBase64(content) {
1871
+ const dataIndex = content.indexOf(base64DataAttr);
1872
+ const base64Index = content.indexOf(";base64,");
1873
+ if (dataIndex > -1 || base64Index > -1) {
1874
+ const mimeType = content.slice(dataIndex + base64DataAttr.length, base64Index);
1875
+ return mimeType;
1876
+ }
1877
+ return null;
1878
+ }
1879
+ function bufferFromDataUrl(dataUrl, contentType) {
1880
+ let base64Data;
1881
+ if (!contentType) {
1882
+ const base64Index = dataUrl.indexOf(";base64,");
1883
+ if (base64Index > -1) {
1884
+ base64Data = dataUrl.slice(base64Index + ";base64,".length);
1885
+ }
1886
+ } else {
1887
+ const dataUrlEncodingHeader = `${base64DataAttr}${contentType};base64,`;
1888
+ if (dataUrl.slice(0, dataUrlEncodingHeader.length) === dataUrlEncodingHeader) {
1889
+ base64Data = dataUrl.slice(dataUrlEncodingHeader.length);
1890
+ }
1891
+ }
1892
+ if (base64Data) {
1893
+ return Buffer.from(base64Data, "base64" // TODO: why does it not work with base64url?
1894
+ );
1895
+ }
1896
+ }
1897
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
1898
+ const COMMON_MIME_TYPES = {
1899
+ aac: "audio/aac",
1900
+ abw: "application/x-abiword",
1901
+ arc: "application/x-freearc",
1902
+ avif: "image/avif",
1903
+ avi: "video/x-msvideo",
1904
+ azw: "application/vnd.amazon.ebook",
1905
+ bin: "application/octet-stream",
1906
+ bmp: "image/bmp",
1907
+ bz: "application/x-bzip",
1908
+ bz2: "application/x-bzip2",
1909
+ cda: "application/x-cdf",
1910
+ csh: "application/x-csh",
1911
+ css: "text/css",
1912
+ csv: "text/csv",
1913
+ doc: "application/msword",
1914
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1915
+ eot: "application/vnd.ms-fontobject",
1916
+ epub: "application/epub+zip",
1917
+ gz: "application/gzip",
1918
+ gif: "image/gif",
1919
+ htm: "text/html",
1920
+ html: "text/html",
1921
+ ico: "image/vnd.microsoft.icon",
1922
+ ics: "text/calendar",
1923
+ jar: "application/java-archive",
1924
+ jpeg: "image/jpeg",
1925
+ jpg: "image/jpeg",
1926
+ js: "text/javascript",
1927
+ json: "application/json",
1928
+ jsonld: "application/ld+json",
1929
+ mid: "audio/midi",
1930
+ midi: "audio/midi",
1931
+ mjs: "text/javascript",
1932
+ mp3: "audio/mpeg",
1933
+ mp4: "video/mp4",
1934
+ mpeg: "video/mpeg",
1935
+ mpkg: "application/vnd.apple.installer+xml",
1936
+ odp: "application/vnd.oasis.opendocument.presentation",
1937
+ ods: "application/vnd.oasis.opendocument.spreadsheet",
1938
+ odt: "application/vnd.oasis.opendocument.text",
1939
+ oga: "audio/ogg",
1940
+ ogv: "video/ogg",
1941
+ ogx: "application/ogg",
1942
+ opus: "audio/opus",
1943
+ otf: "font/otf",
1944
+ png: "image/png",
1945
+ pdf: "application/pdf",
1946
+ php: "application/x-httpd-php",
1947
+ ppt: "application/vnd.ms-powerpoint",
1948
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1949
+ rar: "application/vnd.rar",
1950
+ rtf: "application/rtf",
1951
+ sh: "application/x-sh",
1952
+ svg: "image/svg+xml",
1953
+ tar: "application/x-tar",
1954
+ tif: "image/tiff",
1955
+ tiff: "image/tiff",
1956
+ ts: "video/mp2t",
1957
+ ttf: "font/ttf",
1958
+ txt: "text/plain",
1959
+ vsd: "application/vnd.visio",
1960
+ wav: "audio/wav",
1961
+ weba: "audio/webm",
1962
+ webm: "video/webm",
1963
+ webp: "image/webp",
1964
+ woff: "font/woff",
1965
+ woff2: "font/woff2",
1966
+ xhtml: "application/xhtml+xml",
1967
+ xls: "application/vnd.ms-excel",
1968
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1969
+ xml: "application/xml",
1970
+ xul: "application/vnd.mozilla.xul+xml",
1971
+ zip: "application/zip",
1972
+ "3gp": "video/3gpp; audio/3gpp if it doesn't contain video",
1973
+ "3g2": "video/3gpp2; audio/3gpp2 if it doesn't contain video",
1974
+ "7z": "application/x-7z-compressed"
1975
+ };
1976
+ function guessMimeTypeFromPath(filePath) {
1977
+ const fileExt = filePath.split(".").pop();
1978
+ if (fileExt) {
1979
+ return COMMON_MIME_TYPES[fileExt.toLowerCase()] || null;
1980
+ }
1981
+ return null;
1982
+ }
1983
+
1984
+ const textEncoder = new TextEncoder();
1985
+ class LocalValServer extends ValServer {
1986
+ static PATCHES_DIR = "patches";
1987
+ static FILES_DIR = "files";
1988
+ constructor(options, callbacks) {
1989
+ super(options.service.sourceFileHandler.projectRoot, options.service.sourceFileHandler.host, options, callbacks);
1990
+ this.options = options;
1991
+ this.callbacks = callbacks;
1992
+ this.patchesRootPath = options.cacheDir || path__namespace["default"].join(options.service.sourceFileHandler.projectRoot, ".val");
1993
+ }
1994
+ async session() {
1995
+ return {
1996
+ status: 200,
1997
+ json: {
1998
+ mode: "local",
1999
+ enabled: await this.callbacks.isEnabled()
2000
+ }
2001
+ };
2002
+ }
2003
+ async deletePatches(query) {
2004
+ const deletedPatches = [];
2005
+ for (const patchId of query.id ?? []) {
2006
+ const rawPatchFileContent = this.host.readFile(this.getPatchFilePath(patchId));
2007
+ if (!rawPatchFileContent) {
2008
+ console.warn("Val: Patch not found", patchId);
2009
+ continue;
2010
+ }
2011
+ const parsedPatchesRes = z.z.record(Patch).safeParse(JSON.parse(rawPatchFileContent));
2012
+ if (!parsedPatchesRes.success) {
2013
+ console.warn("Val: Could not parse patch file", patchId, parsedPatchesRes.error);
2014
+ continue;
2015
+ }
2016
+ const files = Object.values(parsedPatchesRes.data).flatMap(ops => ops.filter(isCachedPatchFileOp).map(op => ({
2017
+ filePath: op.filePath,
2018
+ sha256: op.value.sha256
2019
+ })));
2020
+ for (const file of files) {
2021
+ this.host.rmFile(this.getFilePath(file.filePath, file.sha256));
2022
+ this.host.rmFile(this.getFileMetadataPath(file.filePath, file.sha256));
2023
+ }
2024
+ this.host.rmFile(path__namespace["default"].join(this.patchesRootPath, LocalValServer.PATCHES_DIR, patchId));
2025
+ deletedPatches.push(patchId);
2026
+ }
2027
+ return {
2028
+ status: 200,
2029
+ json: deletedPatches
2030
+ };
2031
+ }
2032
+ async postPatches(body) {
2033
+ const patches = z.z.record(Patch).safeParse(body);
2034
+ if (!patches.success) {
2035
+ return {
2036
+ status: 404,
2037
+ json: {
2038
+ message: `Invalid patch: ${patches.error.message}`,
2039
+ details: patches.error.issues
2040
+ }
2041
+ };
2042
+ }
2043
+ let fileId = Date.now();
2044
+ while (this.host.fileExists(this.getPatchFilePath(fileId.toString()))) {
2045
+ // ensure unique file / patch id
2046
+ fileId++;
2047
+ }
2048
+ const patchId = fileId.toString();
2049
+ const res = {};
2050
+ const parsedPatches = {};
2051
+ for (const moduleIdStr in patches.data) {
2052
+ const moduleId = moduleIdStr; // TODO: validate that this is a valid module id
2053
+ res[moduleId] = {
2054
+ patch_id: patchId
2055
+ };
2056
+ parsedPatches[moduleId] = [];
2057
+ for (const op of patches.data[moduleId]) {
2058
+ // We do not want to include value of a file op in the patch as they potentially contain a lot of data,
2059
+ // therefore we store the file in a separate file and only store the sha256 hash in the patch.
2060
+ // I.e. the patch that frontend sends is not the same as the one stored.
2061
+ // 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.
2062
+ //
2063
+ // 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
2064
+ // In the worst scenario we imagine this being potentially crashing the server runtime, especially on smaller edge runtimes.
2065
+ // Potential crashes are bad enough to warrant this workaround.
2066
+ if (core.Internal.isFileOp(op)) {
2067
+ const sha256 = core.Internal.getSHA256Hash(textEncoder.encode(op.value));
2068
+ const mimeType = getMimeTypeFromBase64(op.value);
2069
+ if (!mimeType) {
2070
+ console.error("Val: Cannot determine mimeType from base64 data", op);
2071
+ throw Error("Cannot determine mimeType from base64 data: " + op.filePath);
2072
+ }
2073
+ const buffer = bufferFromDataUrl(op.value, mimeType);
2074
+ if (!buffer) {
2075
+ console.error("Val: Cannot parse base64 data", op);
2076
+ throw Error("Cannot parse base64 data: " + op.filePath);
2077
+ }
2078
+ this.host.writeFile(this.getFilePath(op.filePath, sha256), buffer, "binary");
2079
+ this.host.writeFile(this.getFileMetadataPath(op.filePath, sha256), JSON.stringify({
2080
+ mimeType,
2081
+ sha256,
2082
+ // useful for debugging / manual inspection
2083
+ patchId,
2084
+ createdAt: new Date().toISOString()
2085
+ }, null, 2), "utf8");
2086
+ parsedPatches[moduleId].push({
2087
+ ...op,
2088
+ value: {
2089
+ sha256,
2090
+ mimeType
2091
+ }
2092
+ });
2093
+ } else {
2094
+ parsedPatches[moduleId].push(op);
2095
+ }
2096
+ }
2097
+ }
2098
+ this.host.writeFile(this.getPatchFilePath(patchId), JSON.stringify(parsedPatches), "utf8");
2099
+ return {
2100
+ status: 200,
2101
+ json: res
2102
+ };
2103
+ }
2104
+ async getFiles(filePath, query) {
2105
+ if (query.sha256) {
2106
+ const fileExists = this.host.fileExists(this.getFilePath(filePath, query.sha256));
2107
+ if (fileExists) {
2108
+ const metadataFileContent = this.host.readFile(this.getFileMetadataPath(filePath, query.sha256));
2109
+ const fileContent = await this.readStaticBinaryFile(this.getFilePath(filePath, query.sha256));
2110
+ if (!fileContent) {
2111
+ throw Error("Could not read cached patch file / asset. Cache corrupted?");
2112
+ }
2113
+ if (!metadataFileContent) {
2114
+ throw Error("Missing metadata of cached patch file / asset. Cache corrupted?");
2115
+ }
2116
+ const metadata = JSON.parse(metadataFileContent);
2117
+ return {
2118
+ status: 200,
2119
+ headers: {
2120
+ "Content-Type": metadata.mimeType,
2121
+ "Content-Length": fileContent.byteLength.toString()
2122
+ },
2123
+ body: bufferToReadableStream(fileContent)
2124
+ };
2125
+ }
2126
+ }
2127
+ const buffer = await this.readStaticBinaryFile(path__namespace["default"].join(this.cwd, filePath));
2128
+ const mimeType = guessMimeTypeFromPath(filePath) || "application/octet-stream";
2129
+ if (!buffer) {
2130
+ return {
2131
+ status: 404,
2132
+ json: {
2133
+ message: "File not found"
2134
+ }
2135
+ };
2136
+ }
2137
+ return {
2138
+ status: 200,
2139
+ headers: {
2140
+ "Content-Type": mimeType,
2141
+ "Content-Length": buffer.byteLength.toString()
2142
+ },
2143
+ body: bufferToReadableStream(buffer)
2144
+ };
2145
+ }
2146
+ async getPatches(query) {
2147
+ const patchesCacheDir = path__namespace["default"].join(this.patchesRootPath, LocalValServer.PATCHES_DIR);
2148
+ let files = [];
2149
+ try {
2150
+ if (!this.host.directoryExists || this.host.directoryExists && this.host.directoryExists(patchesCacheDir)) {
2151
+ files = this.host.readDirectory(patchesCacheDir, [""], [], []);
2152
+ }
2153
+ } catch (e) {
2154
+ console.debug("Failed to read directory (no patches yet?)", e);
2155
+ }
2156
+ const res = {};
2157
+ const sortedPatchIds = files.map(file => parseInt(path__namespace["default"].basename(file), 10)).sort();
2158
+ for (const patchIdStr of sortedPatchIds) {
2159
+ const patchId = patchIdStr.toString();
2160
+ if (query.id && query.id.length > 0 && !query.id.includes(patchId)) {
2161
+ continue;
2162
+ }
2163
+ try {
2164
+ const currentParsedPatches = z.z.record(Patch).safeParse(JSON.parse(this.host.readFile(path__namespace["default"].join(this.patchesRootPath, LocalValServer.PATCHES_DIR, `${patchId}`)) || ""));
2165
+ if (!currentParsedPatches.success) {
2166
+ const msg = "Unexpected error reading patch. Patch did not parse correctly. Is there a mismatch in Val versions? Perhaps Val is misconfigured?";
2167
+ console.error(`Val: ${msg}`, {
2168
+ patchId,
2169
+ error: currentParsedPatches.error
2170
+ });
2171
+ return {
2172
+ status: 500,
2173
+ json: {
2174
+ message: msg,
2175
+ details: {
2176
+ patchId,
2177
+ error: currentParsedPatches.error
2178
+ }
2179
+ }
2180
+ };
2181
+ }
2182
+ const createdAt = patchId;
2183
+ for (const moduleIdStr in currentParsedPatches.data) {
2184
+ const moduleId = moduleIdStr;
2185
+ if (!res[moduleId]) {
2186
+ res[moduleId] = [];
2187
+ }
2188
+ res[moduleId].push({
2189
+ patch: currentParsedPatches.data[moduleId],
2190
+ patch_id: patchId,
2191
+ created_at: new Date(Number(createdAt)).toISOString()
2192
+ });
2193
+ }
2194
+ } catch (err) {
2195
+ const msg = `Unexpected error while reading patch file. The cache may be corrupted or Val may be misconfigured. Try deleting the cache directory.`;
2196
+ console.error(`Val: ${msg}`, {
2197
+ patchId,
2198
+ error: err,
2199
+ dir: this.patchesRootPath
2200
+ });
2201
+ return {
2202
+ status: 500,
2203
+ json: {
2204
+ message: msg,
2205
+ details: {
2206
+ patchId,
2207
+ error: err === null || err === void 0 ? void 0 : err.toString()
2208
+ }
2209
+ }
2210
+ };
2211
+ }
2212
+ }
2213
+ return {
2214
+ status: 200,
2215
+ json: res
2216
+ };
2217
+ }
2218
+ getFilePath(filename, sha256) {
2219
+ return path__namespace["default"].join(this.patchesRootPath, LocalValServer.FILES_DIR, filename, sha256, "file");
2220
+ }
2221
+ getFileMetadataPath(filename, sha256) {
2222
+ return path__namespace["default"].join(this.patchesRootPath, LocalValServer.FILES_DIR, filename, sha256, "metadata.json");
2223
+ }
2224
+ getPatchFilePath(patchId) {
2225
+ return path__namespace["default"].join(this.patchesRootPath, LocalValServer.PATCHES_DIR, patchId.toString());
2226
+ }
2227
+ badRequest() {
2228
+ return {
2229
+ status: 400,
2230
+ json: {
2231
+ message: "Local server does not handle this request"
2232
+ }
2233
+ };
2234
+ }
2235
+ async ensureRemoteFSInitialized() {
2236
+ // No RemoteFS so nothing to ensure
2237
+ return fp.result.ok(undefined);
2238
+ }
2239
+ getModule(moduleId) {
2240
+ return this.options.service.get(moduleId);
2241
+ }
2242
+ async execCommit(patches) {
2243
+ for (const [patchId, moduleId, patch] of patches) {
2244
+ // TODO: patch the entire module content directly by using a { path: "", op: "replace", value: patchedData }?
2245
+ // Reason: that would be more atomic? Not doing it now, because there are currently already too many moving pieces.
2246
+ // Other things we could do would be to patch in a temp directory and ONLY when all patches are applied we move back in.
2247
+ // This would improve reliability
2248
+ this.host.rmFile(this.getPatchFilePath(patchId));
2249
+ await this.options.service.patch(moduleId, patch);
2250
+ }
2251
+ return this.getPatchedModules(patches);
2252
+ }
2253
+
2254
+ /* Bad requests on Local Server: */
2255
+
1366
2256
  async authorize() {
1367
2257
  return this.badRequest();
1368
2258
  }
@@ -1372,12 +2262,9 @@ class LocalValServer {
1372
2262
  async logout() {
1373
2263
  return this.badRequest();
1374
2264
  }
1375
- async getFiles() {
1376
- return this.badRequest();
1377
- }
1378
- async getPatches() {
1379
- return this.badRequest();
1380
- }
2265
+ }
2266
+ function isCachedPatchFileOp(op) {
2267
+ 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");
1381
2268
  }
1382
2269
 
1383
2270
  function decodeJwt(token, secretKey) {
@@ -1438,108 +2325,368 @@ function encodeJwt(payload, sessionKey) {
1438
2325
  return `${jwtHeaderBase64}.${payloadBase64}.${crypto__default["default"].createHmac("sha256", sessionKey).update(`${jwtHeaderBase64}.${payloadBase64}`).digest("base64")}`;
1439
2326
  }
1440
2327
 
1441
- class ProxyValServer {
1442
- constructor(options, callbacks) {
2328
+ const SEPARATOR = "/";
2329
+ class RemoteFS {
2330
+ initialized = false;
2331
+ constructor() {
2332
+ this.data = {};
2333
+ this.modifiedFiles = [];
2334
+ this.deletedFiles = [];
2335
+ }
2336
+ useCaseSensitiveFileNames = true;
2337
+ isInitialized() {
2338
+ return this.initialized;
2339
+ }
2340
+ async initializeWith(data) {
2341
+ this.data = data;
2342
+ this.initialized = true;
2343
+ }
2344
+ async getPendingOperations() {
2345
+ const modified = {};
2346
+ for (const modifiedFile in this.modifiedFiles) {
2347
+ modified[modifiedFile] = this.data[modifiedFile].utf8Files[modifiedFile];
2348
+ }
2349
+ return {
2350
+ modified: modified,
2351
+ deleted: this.deletedFiles
2352
+ };
2353
+ }
2354
+ changedDirectories = {};
2355
+ readDirectory = (rootDir, extensions, excludes, includes, depth) => {
2356
+ // TODO: rewrite this! And make some tests! This is a mess!
2357
+ // Considered using glob which typescript seems to use, but that works on an entire typeof fs
2358
+ // glob uses minimatch internally, so using that instead
2359
+ const files = [];
2360
+ for (const dir in this.data) {
2361
+ const depthExceeded = depth ? dir.replace(rootDir, "").split(SEPARATOR).length > depth : false;
2362
+ if (dir.startsWith(rootDir) && !depthExceeded) {
2363
+ for (const file in this.data[dir].utf8Files) {
2364
+ for (const extension of extensions) {
2365
+ if (file.endsWith(extension)) {
2366
+ const path = `${dir}/${file}`;
2367
+ for (const include of includes ?? []) {
2368
+ // TODO: should default includes be ['**/*']?
2369
+ if (minimatch__default["default"](path, include)) {
2370
+ let isExcluded = false;
2371
+ for (const exlude of excludes ?? []) {
2372
+ if (minimatch__default["default"](path, exlude)) {
2373
+ isExcluded = true;
2374
+ break;
2375
+ }
2376
+ }
2377
+ if (!isExcluded) {
2378
+ files.push(path);
2379
+ }
2380
+ }
2381
+ }
2382
+ }
2383
+ }
2384
+ }
2385
+ }
2386
+ }
2387
+ return ts__namespace.sys.readDirectory(rootDir, extensions, excludes, includes, depth).concat(files);
2388
+ };
2389
+ writeFile = (filePath, data, encoding) => {
2390
+ // never write real fs
2391
+ const {
2392
+ directory,
2393
+ filename
2394
+ } = RemoteFS.parsePath(filePath);
2395
+ if (this.data[directory] === undefined) {
2396
+ throw new Error(`Directory not found: ${directory}`);
2397
+ }
2398
+ this.changedDirectories[directory] = this.changedDirectories[directory] ?? new Set();
2399
+
2400
+ // if it fails below this should not be added, so maybe a try/catch?
2401
+ this.changedDirectories[directory].add(filename);
2402
+ this.data[directory].utf8Files[filename] = data;
2403
+ this.modifiedFiles.push(filePath);
2404
+ };
2405
+ rmFile(filePath) {
2406
+ // never remove from real fs
2407
+ const {
2408
+ directory,
2409
+ filename
2410
+ } = RemoteFS.parsePath(filePath);
2411
+ if (this.data[directory] === undefined) {
2412
+ throw new Error(`Directory not found: ${directory}`);
2413
+ }
2414
+ this.changedDirectories[directory] = this.changedDirectories[directory] ?? new Set();
2415
+
2416
+ // if it fails below this should not be added, so maybe a try/catch?
2417
+ this.changedDirectories[directory].add(filename);
2418
+ delete this.data[directory].utf8Files[filename];
2419
+ delete this.data[directory].symlinks[filename];
2420
+ this.deletedFiles.push(filePath);
2421
+ }
2422
+ fileExists = filePath => {
2423
+ var _this$data$directory;
2424
+ if (ts__namespace.sys.fileExists(filePath)) {
2425
+ return true;
2426
+ }
2427
+ const {
2428
+ directory,
2429
+ filename
2430
+ } = 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
2431
+ );
2432
+ return !!((_this$data$directory = this.data[directory]) !== null && _this$data$directory !== void 0 && _this$data$directory.utf8Files[filename]);
2433
+ };
2434
+ readFile = filePath => {
2435
+ const realFile = ts__namespace.sys.readFile(filePath);
2436
+ if (realFile !== undefined) {
2437
+ return realFile;
2438
+ }
2439
+ const {
2440
+ directory,
2441
+ filename
2442
+ } = RemoteFS.parsePath(filePath);
2443
+ const dirNode = this.data[directory];
2444
+ if (!dirNode) {
2445
+ return undefined;
2446
+ }
2447
+ const content = dirNode.utf8Files[filename];
2448
+ return content;
2449
+ };
2450
+ realpath(fullPath) {
2451
+ if (ts__namespace.sys.fileExists(fullPath) && ts__namespace.sys.realpath) {
2452
+ return ts__namespace.sys.realpath(fullPath);
2453
+ }
2454
+ // TODO: this only works in a very limited way.
2455
+ // It does not support symlinks to symlinks nor symlinked directories for instance.
2456
+ const {
2457
+ directory,
2458
+ filename
2459
+ } = RemoteFS.parsePath(fullPath);
2460
+ if (this.data[directory] === undefined) {
2461
+ return fullPath;
2462
+ }
2463
+ if (this.data[directory].utf8Files[filename] === undefined) {
2464
+ const link = this.data[directory].symlinks[filename];
2465
+ if (link === undefined) {
2466
+ return fullPath;
2467
+ } else {
2468
+ return link;
2469
+ }
2470
+ } else {
2471
+ return path__namespace["default"].join(directory, filename);
2472
+ }
2473
+ }
2474
+
2475
+ /**
2476
+ *
2477
+ * @param path
2478
+ * @returns directory and filename. NOTE: directory might be empty string
2479
+ */
2480
+ static parsePath(path) {
2481
+ const pathParts = path.split(SEPARATOR);
2482
+ const filename = pathParts.pop();
2483
+ if (!filename) {
2484
+ throw new Error(`Invalid path: '${path}'. Node filename: '${filename}'`);
2485
+ }
2486
+ const directory = pathParts.join(SEPARATOR);
2487
+ return {
2488
+ directory,
2489
+ filename
2490
+ };
2491
+ }
2492
+ }
2493
+
2494
+ /**
2495
+ * Represents directories
2496
+ * NOTE: the keys of directory nodes are the "full" path, i.e. "foo/bar"
2497
+ * NOTE: the keys of file nodes are the "filename" only, i.e. "baz.txt"
2498
+ *
2499
+ * @example
2500
+ * {
2501
+ * "foo/bar": { // <- directory. NOTE: this is the "full" path
2502
+ * gitHubSha: "123",
2503
+ * files: {
2504
+ * "baz.txt": "hello world" // <- file. NOTE: this is the "filename" only
2505
+ * },
2506
+ * },
2507
+ * };
2508
+ */
2509
+ // TODO: a Map would be better here
2510
+
2511
+ class ProxyValServer extends ValServer {
2512
+ constructor(cwd, options, apiOptions, callbacks) {
2513
+ const remoteFS = new RemoteFS();
2514
+ super(cwd, remoteFS, options, callbacks);
2515
+ this.cwd = cwd;
1443
2516
  this.options = options;
2517
+ this.apiOptions = apiOptions;
1444
2518
  this.callbacks = callbacks;
2519
+ this.remoteFS = remoteFS;
1445
2520
  }
1446
- async getFiles(treePath, query, cookies) {
1447
- return this.withAuth(cookies, "getFiles", async data => {
1448
- const url = new URL(`/v1/files/${this.options.valName}${treePath}`, this.options.valContentUrl);
1449
- if (typeof query.sha256 === "string") {
1450
- url.searchParams.append("sha256", query.sha256);
1451
- } else {
1452
- console.warn("Missing sha256 query param");
2521
+
2522
+ /** Remote FS dependent methods: */
2523
+
2524
+ async getModule(moduleId) {
2525
+ if (!this.lazyService) {
2526
+ this.lazyService = await createService(this.cwd, this.apiOptions, this.remoteFS);
2527
+ }
2528
+ return this.lazyService.get(moduleId);
2529
+ }
2530
+ execCommit(patches, cookies) {
2531
+ return withAuth(this.options.valSecret, cookies, "execCommit", async ({
2532
+ token
2533
+ }) => {
2534
+ const commit = this.options.git.commit;
2535
+ if (!commit) {
2536
+ return {
2537
+ status: 400,
2538
+ json: {
2539
+ message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
2540
+ }
2541
+ };
1453
2542
  }
2543
+ const params = `commit=${encodeURIComponent(commit)}`;
2544
+ const url = new URL(`/v1/commit/${this.options.valName}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
2545
+
2546
+ // Creates a fresh copy of the fs. We cannot touch the existing fs, since we might still want to do
2547
+ // other operations on it.
2548
+ // 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.
2549
+ // 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.
2550
+ const remoteFS = new RemoteFS();
2551
+ const initRes = await this.initRemoteFS(commit, remoteFS, token);
2552
+ if (initRes.status !== 200) {
2553
+ return initRes;
2554
+ }
2555
+ const service = await createService(this.cwd, this.apiOptions, remoteFS);
2556
+ for (const [, moduleId, patch] of patches) {
2557
+ await service.patch(moduleId, patch);
2558
+ }
2559
+ const fileOps = await remoteFS.getPendingOperations();
1454
2560
  const fetchRes = await fetch(url, {
1455
- headers: this.getAuthHeaders(data.token)
2561
+ method: "POST",
2562
+ headers: getAuthHeaders(token, "application/json"),
2563
+ body: JSON.stringify({
2564
+ fileOps
2565
+ })
1456
2566
  });
1457
2567
  if (fetchRes.status === 200) {
1458
- if (fetchRes.body) {
1459
- return {
1460
- status: fetchRes.status,
1461
- headers: {
1462
- "Content-Type": fetchRes.headers.get("Content-Type") || "",
1463
- "Content-Length": fetchRes.headers.get("Content-Length") || "0"
1464
- },
1465
- json: fetchRes.body
1466
- };
1467
- } else {
1468
- return {
1469
- status: 500,
1470
- json: {
1471
- message: "No body in response"
1472
- }
1473
- };
1474
- }
2568
+ return {
2569
+ status: fetchRes.status,
2570
+ json: await fetchRes.json()
2571
+ };
1475
2572
  } else {
2573
+ console.error("Failed to get patches", fetchRes.status, await fetchRes.text());
1476
2574
  return {
1477
2575
  status: fetchRes.status,
1478
2576
  json: {
1479
- message: "Failed to get files"
2577
+ message: "Failed to get patches"
1480
2578
  }
1481
2579
  };
1482
2580
  }
1483
2581
  });
1484
2582
  }
1485
- async authorize(query) {
1486
- if (typeof query.redirect_to !== "string") {
2583
+ async initRemoteFS(commit, remoteFS, token) {
2584
+ const params = new URLSearchParams(this.apiOptions.root ? {
2585
+ root: this.apiOptions.root,
2586
+ commit,
2587
+ cwd: this.cwd
2588
+ } : {
2589
+ commit,
2590
+ cwd: this.cwd
2591
+ });
2592
+ const url = new URL(`/v1/fs/${this.options.valName}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
2593
+ try {
2594
+ const fetchRes = await fetch(url, {
2595
+ headers: getAuthHeaders(token, "application/json")
2596
+ });
2597
+ if (fetchRes.status === 200) {
2598
+ const json = await fetchRes.json();
2599
+ remoteFS.initializeWith(json);
2600
+ return {
2601
+ status: 200
2602
+ };
2603
+ } else {
2604
+ try {
2605
+ var _fetchRes$headers$get;
2606
+ if ((_fetchRes$headers$get = fetchRes.headers.get("Content-Type")) !== null && _fetchRes$headers$get !== void 0 && _fetchRes$headers$get.includes("application/json")) {
2607
+ const json = await fetchRes.json();
2608
+ return {
2609
+ status: fetchRes.status,
2610
+ json: {
2611
+ message: "Failed to fetch remote files",
2612
+ details: json
2613
+ }
2614
+ };
2615
+ }
2616
+ } catch (err) {
2617
+ console.error(err);
2618
+ }
2619
+ return {
2620
+ status: fetchRes.status,
2621
+ json: {
2622
+ message: "Unknown failure while fetching remote files"
2623
+ }
2624
+ };
2625
+ }
2626
+ } catch (err) {
1487
2627
  return {
1488
- status: 400,
2628
+ status: 500,
1489
2629
  json: {
1490
- message: "Missing redirect_to query param"
2630
+ message: "Failed to fetch: check network connection"
1491
2631
  }
1492
2632
  };
1493
2633
  }
1494
- const token = crypto__default["default"].randomUUID();
1495
- const redirectUrl = new URL(query.redirect_to);
1496
- const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
1497
- return {
1498
- cookies: {
1499
- [internal.VAL_STATE_COOKIE]: {
1500
- value: createStateCookie({
1501
- redirect_to: query.redirect_to,
1502
- token
1503
- }),
1504
- options: {
1505
- httpOnly: true,
1506
- sameSite: "lax",
1507
- expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
1508
- }
1509
- }
1510
- },
1511
- status: 302,
1512
- redirectTo: appAuthorizeUrl
1513
- };
1514
2634
  }
1515
- async enable(query) {
1516
- const redirectToRes = getRedirectUrl(query, this.options.valEnableRedirectUrl);
1517
- if (typeof redirectToRes !== "string") {
1518
- return redirectToRes;
2635
+ async ensureRemoteFSInitialized(errorMessageType, cookies) {
2636
+ const commit = this.options.git.commit;
2637
+ if (!commit) {
2638
+ return fp.result.err({
2639
+ status: 400,
2640
+ json: {
2641
+ message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
2642
+ }
2643
+ });
2644
+ }
2645
+ const res = await withAuth(this.options.valSecret, cookies, errorMessageType, async data => {
2646
+ if (!this.remoteFS.isInitialized()) {
2647
+ return this.initRemoteFS(commit, this.remoteFS, data.token);
2648
+ } else {
2649
+ return {
2650
+ status: 200
2651
+ };
2652
+ }
2653
+ });
2654
+ if (res.status === 200) {
2655
+ return fp.result.ok(undefined);
2656
+ } else {
2657
+ return fp.result.err(res);
1519
2658
  }
1520
- await this.callbacks.onEnable(true);
1521
- return {
1522
- cookies: {
1523
- [internal.VAL_ENABLE_COOKIE_NAME]: ENABLE_COOKIE_VALUE
1524
- },
1525
- status: 302,
1526
- redirectTo: redirectToRes
1527
- };
1528
2659
  }
1529
- async disable(query) {
1530
- const redirectToRes = getRedirectUrl(query, this.options.valDisableRedirectUrl);
1531
- if (typeof redirectToRes !== "string") {
1532
- return redirectToRes;
2660
+ /* Auth endpoints */
2661
+
2662
+ async authorize(query) {
2663
+ if (typeof query.redirect_to !== "string") {
2664
+ return {
2665
+ status: 400,
2666
+ json: {
2667
+ message: "Missing redirect_to query param"
2668
+ }
2669
+ };
1533
2670
  }
1534
- await this.callbacks.onDisable(true);
2671
+ const token = crypto__default["default"].randomUUID();
2672
+ const redirectUrl = new URL(query.redirect_to);
2673
+ const appAuthorizeUrl = this.getAuthorizeUrl(`${redirectUrl.origin}/${this.options.route}`, token);
1535
2674
  return {
1536
2675
  cookies: {
1537
- [internal.VAL_ENABLE_COOKIE_NAME]: {
1538
- value: null
2676
+ [internal.VAL_STATE_COOKIE]: {
2677
+ value: createStateCookie({
2678
+ redirect_to: query.redirect_to,
2679
+ token
2680
+ }),
2681
+ options: {
2682
+ httpOnly: true,
2683
+ sameSite: "lax",
2684
+ expires: new Date(Date.now() + 1000 * 60 * 60) // 1 hour
2685
+ }
1539
2686
  }
1540
2687
  },
1541
2688
  status: 302,
1542
- redirectTo: redirectToRes
2689
+ redirectTo: appAuthorizeUrl
1543
2690
  };
1544
2691
  }
1545
2692
  async callback(query, cookies) {
@@ -1609,51 +2756,11 @@ class ProxyValServer {
1609
2756
  }
1610
2757
  };
1611
2758
  }
1612
- async withAuth(cookies, errorMessageType, handler) {
1613
- const cookie = cookies[internal.VAL_SESSION_COOKIE];
1614
- if (typeof cookie === "string") {
1615
- const decodedToken = decodeJwt(cookie, this.options.valSecret);
1616
- if (!decodedToken) {
1617
- return {
1618
- status: 401,
1619
- json: {
1620
- message: "Invalid JWT token"
1621
- }
1622
- };
1623
- }
1624
- const verification = IntegratedServerJwtPayload.safeParse(decodedToken);
1625
- if (!verification.success) {
1626
- return {
1627
- status: 401,
1628
- json: {
1629
- message: "Could not parse JWT",
1630
- details: verification.error
1631
- }
1632
- };
1633
- }
1634
- return handler(verification.data).catch(err => {
1635
- console.error(`Failed while processing: ${errorMessageType}`, err);
1636
- return {
1637
- status: 500,
1638
- json: {
1639
- message: err.message
1640
- }
1641
- };
1642
- });
1643
- } else {
1644
- return {
1645
- status: 401,
1646
- json: {
1647
- message: "No token"
1648
- }
1649
- };
1650
- }
1651
- }
1652
2759
  async session(cookies) {
1653
- return this.withAuth(cookies, "session", async data => {
2760
+ return withAuth(this.options.valSecret, cookies, "session", async data => {
1654
2761
  const url = new URL(`/api/val/${this.options.valName}/auth/session`, this.options.valBuildUrl);
1655
2762
  const fetchRes = await fetch(url, {
1656
- headers: this.getAuthHeaders(data.token, "application/json")
2763
+ headers: getAuthHeaders(data.token, "application/json")
1657
2764
  });
1658
2765
  if (fetchRes.status === 200) {
1659
2766
  return {
@@ -1675,73 +2782,81 @@ class ProxyValServer {
1675
2782
  }
1676
2783
  });
1677
2784
  }
1678
- async getTree(treePath, query, cookies) {
1679
- return this.withAuth(cookies, "getTree", async data => {
1680
- const {
1681
- patch,
1682
- schema,
1683
- source
1684
- } = query;
1685
- const commit = this.options.gitCommit;
1686
- if (!commit) {
2785
+ async consumeCode(code) {
2786
+ const url = new URL(`/api/val/${this.options.valName}/auth/token`, this.options.valBuildUrl);
2787
+ url.searchParams.set("code", encodeURIComponent(code));
2788
+ return fetch(url, {
2789
+ method: "POST",
2790
+ headers: getAuthHeaders(this.options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
2791
+ }).then(async res => {
2792
+ if (res.status === 200) {
2793
+ const token = await res.text();
2794
+ const verification = ValAppJwtPayload.safeParse(decodeJwt(token));
2795
+ if (!verification.success) {
2796
+ return null;
2797
+ }
1687
2798
  return {
1688
- status: 400,
1689
- json: {
1690
- message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
1691
- }
2799
+ ...verification.data,
2800
+ token
1692
2801
  };
2802
+ } else {
2803
+ console.debug("Failed to get data from code: ", res.status);
2804
+ return null;
1693
2805
  }
1694
- const params = new URLSearchParams({
1695
- patch: (patch === "true").toString(),
1696
- schema: (schema === "true").toString(),
1697
- source: (source === "true").toString(),
1698
- commit
2806
+ }).catch(err => {
2807
+ console.debug("Failed to get user from code: ", err);
2808
+ return null;
2809
+ });
2810
+ }
2811
+ getAuthorizeUrl(publicValApiRoute, token) {
2812
+ const url = new URL(`/auth/${this.options.valName}/authorize`, this.options.valBuildUrl);
2813
+ url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRoute}/callback`));
2814
+ url.searchParams.set("state", token);
2815
+ return url.toString();
2816
+ }
2817
+ getAppErrorUrl(error) {
2818
+ const url = new URL(`/auth/${this.options.valName}/authorize`, this.options.valBuildUrl);
2819
+ url.searchParams.set("error", encodeURIComponent(error));
2820
+ return url.toString();
2821
+ }
2822
+
2823
+ /* Patch endpoints */
2824
+ async deletePatches(
2825
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2826
+ query,
2827
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2828
+ cookies) {
2829
+ return withAuth(this.options.valSecret, cookies, "deletePatches", async ({
2830
+ token
2831
+ }) => {
2832
+ const patchIds = query.id || [];
2833
+ const params = `${patchIds.map(id => `id=${encodeURIComponent(id)}`).join("&")}`;
2834
+ const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
2835
+ const fetchRes = await fetch(url, {
2836
+ method: "GET",
2837
+ headers: getAuthHeaders(token, "application/json")
1699
2838
  });
1700
- const url = new URL(`/v1/tree/${this.options.valName}/heads/${this.options.gitBranch}/~${treePath}/?${params}`, this.options.valContentUrl);
1701
- try {
1702
- const fetchRes = await fetch(url, {
1703
- headers: this.getAuthHeaders(data.token, "application/json")
1704
- });
1705
- if (fetchRes.status === 200) {
1706
- return {
1707
- status: fetchRes.status,
1708
- json: await fetchRes.json()
1709
- };
1710
- } else {
1711
- try {
1712
- var _fetchRes$headers$get;
1713
- if ((_fetchRes$headers$get = fetchRes.headers.get("Content-Type")) !== null && _fetchRes$headers$get !== void 0 && _fetchRes$headers$get.includes("application/json")) {
1714
- const json = await fetchRes.json();
1715
- return {
1716
- status: fetchRes.status,
1717
- json
1718
- };
1719
- }
1720
- } catch (err) {
1721
- console.error(err);
1722
- }
1723
- return {
1724
- status: fetchRes.status,
1725
- json: {
1726
- message: "Unknown failure while accessing Val"
1727
- }
1728
- };
1729
- }
1730
- } catch (err) {
2839
+ if (fetchRes.status === 200) {
1731
2840
  return {
1732
- status: 500,
2841
+ status: fetchRes.status,
2842
+ json: await fetchRes.json()
2843
+ };
2844
+ } else {
2845
+ console.error("Failed to delete patches", fetchRes.status, await fetchRes.text());
2846
+ return {
2847
+ status: fetchRes.status,
1733
2848
  json: {
1734
- message: "Failed to fetch: check network connection"
2849
+ message: "Failed to delete patches"
1735
2850
  }
1736
2851
  };
1737
2852
  }
1738
2853
  });
1739
2854
  }
1740
2855
  async getPatches(query, cookies) {
1741
- return this.withAuth(cookies, "getPatches", async ({
2856
+ return withAuth(this.options.valSecret, cookies, "getPatches", async ({
1742
2857
  token
1743
2858
  }) => {
1744
- const commit = this.options.gitCommit;
2859
+ const commit = this.options.git.commit;
1745
2860
  if (!commit) {
1746
2861
  return {
1747
2862
  status: 400,
@@ -1752,11 +2867,11 @@ class ProxyValServer {
1752
2867
  }
1753
2868
  const patchIds = query.id || [];
1754
2869
  const params = patchIds.length > 0 ? `commit=${encodeURIComponent(commit)}&${patchIds.map(id => `id=${encodeURIComponent(id)}`).join("&")}` : `commit=${encodeURIComponent(commit)}`;
1755
- const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.gitBranch}/~?${params}`, this.options.valContentUrl);
2870
+ const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
1756
2871
  // Proxy patch to val.build
1757
2872
  const fetchRes = await fetch(url, {
1758
2873
  method: "GET",
1759
- headers: this.getAuthHeaders(token, "application/json")
2874
+ headers: getAuthHeaders(token, "application/json")
1760
2875
  });
1761
2876
  if (fetchRes.status === 200) {
1762
2877
  return {
@@ -1764,6 +2879,7 @@ class ProxyValServer {
1764
2879
  json: await fetchRes.json()
1765
2880
  };
1766
2881
  } else {
2882
+ console.error("Failed to get patches", fetchRes.status, await fetchRes.text());
1767
2883
  return {
1768
2884
  status: fetchRes.status,
1769
2885
  json: {
@@ -1774,7 +2890,7 @@ class ProxyValServer {
1774
2890
  });
1775
2891
  }
1776
2892
  async postPatches(body, cookies) {
1777
- const commit = this.options.gitCommit;
2893
+ const commit = this.options.git.commit;
1778
2894
  if (!commit) {
1779
2895
  return {
1780
2896
  status: 401,
@@ -1786,34 +2902,27 @@ class ProxyValServer {
1786
2902
  const params = new URLSearchParams({
1787
2903
  commit
1788
2904
  });
1789
- return this.withAuth(cookies, "postPatches", async ({
2905
+ return withAuth(this.options.valSecret, cookies, "postPatches", async ({
1790
2906
  token
1791
2907
  }) => {
1792
2908
  // First validate that the body has the right structure
1793
- const patchJSON = z.z.record(PatchJSON).safeParse(body);
1794
- if (!patchJSON.success) {
2909
+ const parsedPatches = z.z.record(Patch).safeParse(body);
2910
+ if (!parsedPatches.success) {
1795
2911
  return {
1796
2912
  status: 400,
1797
2913
  json: {
1798
- message: "Invalid patch",
1799
- details: patchJSON.error.issues
2914
+ message: "Invalid patch(es)",
2915
+ details: parsedPatches.error.issues
1800
2916
  }
1801
2917
  };
1802
2918
  }
1803
- // Then parse/validate
1804
- // TODO:
1805
- const patch = patchJSON.data;
1806
- // const patch = parsePatch(patchJSON.data);
1807
- // if (result.isErr(patch)) {
1808
- // res.status(401).json(patch.error);
1809
- // return;
1810
- // }
1811
- const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.gitBranch}/~?${params}`, this.options.valContentUrl);
2919
+ const patches = parsedPatches.data;
2920
+ const url = new URL(`/v1/patches/${this.options.valName}/heads/${this.options.git.branch}/~?${params}`, this.options.valContentUrl);
1812
2921
  // Proxy patch to val.build
1813
2922
  const fetchRes = await fetch(url, {
1814
2923
  method: "POST",
1815
- headers: this.getAuthHeaders(token, "application/json"),
1816
- body: JSON.stringify(patch)
2924
+ headers: getAuthHeaders(token, "application/json"),
2925
+ body: JSON.stringify(patches)
1817
2926
  });
1818
2927
  if (fetchRes.status === 200) {
1819
2928
  return {
@@ -1827,89 +2936,62 @@ class ProxyValServer {
1827
2936
  }
1828
2937
  });
1829
2938
  }
1830
- async postCommit(rawBody, cookies) {
1831
- const commit = this.options.gitCommit;
1832
- if (!commit) {
1833
- return {
1834
- status: 401,
1835
- json: {
1836
- message: "Could not detect the git commit. Check if env is missing VAL_GIT_COMMIT."
1837
- }
1838
- };
1839
- }
1840
- const params = new URLSearchParams({
1841
- commit
1842
- });
1843
- return this.withAuth(cookies, "postCommit", async ({
1844
- token
1845
- }) => {
1846
- const url = new URL(`/v1/commit/${this.options.valName}/heads/${this.options.gitBranch}/~?${params}`, this.options.valContentUrl);
1847
- const body = JSON.stringify(rawBody);
2939
+ async getFiles(filePath, query, cookies) {
2940
+ return withAuth(this.options.valSecret, cookies, "getFiles", async data => {
2941
+ const url = new URL(`/v1/files/${this.options.valName}${filePath}`, this.options.valContentUrl);
2942
+ if (typeof query.sha256 === "string") {
2943
+ url.searchParams.append("sha256", query.sha256);
2944
+ } else {
2945
+ console.warn("Missing sha256 query param");
2946
+ }
1848
2947
  const fetchRes = await fetch(url, {
1849
- method: "POST",
1850
- headers: this.getAuthHeaders(token, "application/json"),
1851
- body
2948
+ headers: getAuthHeaders(data.token)
1852
2949
  });
1853
2950
  if (fetchRes.status === 200) {
1854
- return {
1855
- status: fetchRes.status,
1856
- json: await fetchRes.json()
1857
- };
2951
+ // TODO: does this stream data?
2952
+ if (fetchRes.body) {
2953
+ return {
2954
+ status: fetchRes.status,
2955
+ headers: {
2956
+ "Content-Type": fetchRes.headers.get("Content-Type") || "",
2957
+ "Content-Length": fetchRes.headers.get("Content-Length") || "0"
2958
+ },
2959
+ body: fetchRes.body
2960
+ };
2961
+ } else {
2962
+ return {
2963
+ status: 500,
2964
+ json: {
2965
+ message: "No body in response"
2966
+ }
2967
+ };
2968
+ }
1858
2969
  } else {
1859
- return {
1860
- status: fetchRes.status
1861
- };
1862
- }
1863
- });
1864
- }
1865
- getAuthHeaders(token, type) {
1866
- if (!type) {
1867
- return {
1868
- Authorization: `Bearer ${token}`
1869
- };
1870
- }
1871
- return {
1872
- "Content-Type": type,
1873
- Authorization: `Bearer ${token}`
1874
- };
1875
- }
1876
- async consumeCode(code) {
1877
- const url = new URL(`/api/val/${this.options.valName}/auth/token`, this.options.valBuildUrl);
1878
- url.searchParams.set("code", encodeURIComponent(code));
1879
- return fetch(url, {
1880
- method: "POST",
1881
- headers: this.getAuthHeaders(this.options.apiKey, "application/json") // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
1882
- }).then(async res => {
1883
- if (res.status === 200) {
1884
- const token = await res.text();
1885
- const verification = ValAppJwtPayload.safeParse(decodeJwt(token));
1886
- if (!verification.success) {
1887
- return null;
2970
+ const fileExists = this.remoteFS.fileExists(path__namespace["default"].join(this.cwd, filePath));
2971
+ let buffer;
2972
+ if (fileExists) {
2973
+ buffer = await this.readStaticBinaryFile(path__namespace["default"].join(this.cwd, filePath));
2974
+ }
2975
+ if (!buffer) {
2976
+ return {
2977
+ status: 404,
2978
+ json: {
2979
+ message: "File not found"
2980
+ }
2981
+ };
1888
2982
  }
2983
+ const mimeType = guessMimeTypeFromPath(filePath) || "application/octet-stream";
1889
2984
  return {
1890
- ...verification.data,
1891
- token
2985
+ status: 200,
2986
+ headers: {
2987
+ "Content-Type": mimeType,
2988
+ "Content-Length": buffer.byteLength.toString()
2989
+ },
2990
+ body: bufferToReadableStream(buffer)
1892
2991
  };
1893
- } else {
1894
- console.debug("Failed to get data from code: ", res.status);
1895
- return null;
1896
2992
  }
1897
- }).catch(err => {
1898
- console.debug("Failed to get user from code: ", err);
1899
- return null;
1900
2993
  });
1901
2994
  }
1902
- getAuthorizeUrl(publicValApiRoute, token) {
1903
- const url = new URL(`/auth/${this.options.valName}/authorize`, this.options.valBuildUrl);
1904
- url.searchParams.set("redirect_uri", encodeURIComponent(`${publicValApiRoute}/callback`));
1905
- url.searchParams.set("state", token);
1906
- return url.toString();
1907
- }
1908
- getAppErrorUrl(error) {
1909
- const url = new URL(`/auth/${this.options.valName}/authorize`, this.options.valBuildUrl);
1910
- url.searchParams.set("error", encodeURIComponent(error));
1911
- return url.toString();
1912
- }
1913
2995
  }
1914
2996
  function verifyCallbackReq(stateCookie, queryParams) {
1915
2997
  if (typeof stateCookie !== "string") {
@@ -2027,11 +3109,67 @@ const IntegratedServerJwtPayload = z.z.object({
2027
3109
  org: z.z.string(),
2028
3110
  project: z.z.string()
2029
3111
  });
3112
+ async function withAuth(secret, cookies, errorMessageType, handler) {
3113
+ const cookie = cookies[internal.VAL_SESSION_COOKIE];
3114
+ if (typeof cookie === "string") {
3115
+ const decodedToken = decodeJwt(cookie, secret);
3116
+ if (!decodedToken) {
3117
+ return {
3118
+ status: 401,
3119
+ json: {
3120
+ message: "Could not verify session. You will need to login again.",
3121
+ details: "Invalid token"
3122
+ }
3123
+ };
3124
+ }
3125
+ const verification = IntegratedServerJwtPayload.safeParse(decodedToken);
3126
+ if (!verification.success) {
3127
+ return {
3128
+ status: 401,
3129
+ json: {
3130
+ message: "Session invalid or, most likely, expired. You will need to login again.",
3131
+ details: verification.error
3132
+ }
3133
+ };
3134
+ }
3135
+ return handler(verification.data).catch(err => {
3136
+ console.error(`Failed while processing: ${errorMessageType}`, err);
3137
+ return {
3138
+ status: 500,
3139
+ json: {
3140
+ message: err.message
3141
+ }
3142
+ };
3143
+ });
3144
+ } else {
3145
+ return {
3146
+ status: 401,
3147
+ json: {
3148
+ message: "Login required",
3149
+ details: {
3150
+ reason: "Cookie not found"
3151
+ }
3152
+ }
3153
+ };
3154
+ }
3155
+ }
3156
+ function getAuthHeaders(token, type) {
3157
+ if (!type) {
3158
+ return {
3159
+ Authorization: `Bearer ${token}`
3160
+ };
3161
+ }
3162
+ return {
3163
+ "Content-Type": type,
3164
+ Authorization: `Bearer ${token}`
3165
+ };
3166
+ }
2030
3167
 
2031
3168
  async function createValServer(route, opts, callbacks) {
2032
3169
  const serverOpts = await initHandlerOptions(route, opts);
2033
3170
  if (serverOpts.mode === "proxy") {
2034
- return new ProxyValServer(serverOpts, callbacks);
3171
+ const projectRoot = process.cwd(); //[process.cwd(), opts.root || ""] .filter((seg) => seg) .join("/");
3172
+ return new ProxyValServer(projectRoot, serverOpts, opts, callbacks);
2035
3173
  } else {
2036
3174
  return new LocalValServer(serverOpts, callbacks);
2037
3175
  }
@@ -2065,8 +3203,10 @@ async function initHandlerOptions(route, opts) {
2065
3203
  valSecret: maybeValSecret,
2066
3204
  valBuildUrl,
2067
3205
  valContentUrl,
2068
- gitCommit: maybeGitCommit,
2069
- gitBranch: maybeGitBranch,
3206
+ git: {
3207
+ commit: maybeGitCommit,
3208
+ branch: maybeGitBranch
3209
+ },
2070
3210
  valName: maybeValName,
2071
3211
  valEnableRedirectUrl: opts.valEnableRedirectUrl || process.env.VAL_ENABLE_REDIRECT_URL,
2072
3212
  valDisableRedirectUrl: opts.valDisableRedirectUrl || process.env.VAL_DISABLE_REDIRECT_URL
@@ -2219,6 +3359,9 @@ function createValApiRouter(route, valServerPromise, convert) {
2219
3359
  } else if (method === "POST" && path === "/commit") {
2220
3360
  const body = await req.json();
2221
3361
  return convert(await valServer.postCommit(body, getCookies(req, [VAL_SESSION_COOKIE])));
3362
+ } else if (method === "POST" && path === "/validate") {
3363
+ const body = await req.json();
3364
+ return convert(await valServer.postValidate(body, getCookies(req, [VAL_SESSION_COOKIE])));
2222
3365
  } else if (method === "GET" && path.startsWith(TREE_PATH_PREFIX)) {
2223
3366
  return withTreePath(path, TREE_PATH_PREFIX)(async treePath => convert(await valServer.getTree(treePath, {
2224
3367
  patch: url.searchParams.get("patch") || undefined,
@@ -2232,6 +3375,10 @@ function createValApiRouter(route, valServerPromise, convert) {
2232
3375
  } else if (method === "POST" && path.startsWith(PATCHES_PATH_PREFIX)) {
2233
3376
  const body = await req.json();
2234
3377
  return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.postPatches(body, getCookies(req, [VAL_SESSION_COOKIE]))));
3378
+ } else if (method === "DELETE" && path.startsWith(PATCHES_PATH_PREFIX)) {
3379
+ return withTreePath(path, PATCHES_PATH_PREFIX)(async () => convert(await valServer.deletePatches({
3380
+ id: url.searchParams.getAll("id")
3381
+ }, getCookies(req, [VAL_SESSION_COOKIE]))));
2235
3382
  } else if (path.startsWith(FILES_PATH_PREFIX)) {
2236
3383
  const treePath = path.slice(FILES_PATH_PREFIX.length);
2237
3384
  return convert(await valServer.getFiles(treePath, {
@@ -2278,6 +3425,9 @@ class ValFSHost {
2278
3425
  readDirectory(rootDir, extensions, excludes, includes, depth) {
2279
3426
  return this.valFS.readDirectory(rootDir, extensions, excludes, includes, depth);
2280
3427
  }
3428
+ rmFile(fileName) {
3429
+ this.valFS.rmFile(fileName);
3430
+ }
2281
3431
  writeFile(fileName, text, encoding) {
2282
3432
  this.valFS.writeFile(fileName, text, encoding);
2283
3433
  }
@@ -2302,67 +3452,26 @@ class ValFSHost {
2302
3452
  }
2303
3453
 
2304
3454
  // TODO: find a better name? transformFixesToPatch?
2305
- const textEncoder = new TextEncoder();
2306
3455
  async function createFixPatch(config, apply, sourcePath, validationError) {
2307
3456
  async function getImageMetadata() {
2308
- const maybeRef = validationError.value && typeof validationError.value === "object" && core.FILE_REF_PROP in validationError.value && typeof validationError.value[core.FILE_REF_PROP] === "string" ? validationError.value[core.FILE_REF_PROP] : undefined;
2309
- if (!maybeRef) {
3457
+ const fileRef = getValidationErrorFileRef(validationError);
3458
+ if (!fileRef) {
2310
3459
  // TODO:
2311
3460
  throw Error("Cannot fix image without a file reference");
2312
3461
  }
2313
- const filename = path__namespace["default"].join(config.projectRoot, maybeRef);
3462
+ const filename = path__namespace["default"].join(config.projectRoot, fileRef);
2314
3463
  const buffer = fs__default["default"].readFileSync(filename);
2315
- const imageSize = sizeOf__default["default"](buffer);
2316
- let mimeType = null;
2317
- if (imageSize.type) {
2318
- const possibleMimeType = `image/${imageSize.type}`;
2319
- if (internal.MIME_TYPES_TO_EXT[possibleMimeType]) {
2320
- mimeType = possibleMimeType;
2321
- }
2322
- const filenameBasedLookup = internal.filenameToMimeType(filename);
2323
- if (filenameBasedLookup) {
2324
- mimeType = filenameBasedLookup;
2325
- }
2326
- }
2327
- if (!mimeType) {
2328
- throw Error("Cannot determine mimetype of image");
2329
- }
2330
- const {
2331
- width,
2332
- height
2333
- } = imageSize;
2334
- if (!width || !height) {
2335
- throw Error("Cannot determine image size");
2336
- }
2337
- const sha256 = core.Internal.getSHA256Hash(textEncoder.encode(
2338
- // TODO: we should probably store the mimetype in the metadata and reuse it here
2339
- `data:${mimeType};base64,${buffer.toString("base64")}`));
2340
- return {
2341
- width,
2342
- height,
2343
- sha256,
2344
- mimeType
2345
- };
3464
+ return extractImageMetadata(filename, buffer);
2346
3465
  }
2347
3466
  async function getFileMetadata() {
2348
- const maybeRef = validationError.value && typeof validationError.value === "object" && core.FILE_REF_PROP in validationError.value && typeof validationError.value[core.FILE_REF_PROP] === "string" ? validationError.value[core.FILE_REF_PROP] : undefined;
2349
- if (!maybeRef) {
3467
+ const fileRef = getValidationErrorFileRef(validationError);
3468
+ if (!fileRef) {
2350
3469
  // TODO:
2351
- throw Error("Cannot fix image without a file reference");
3470
+ throw Error("Cannot fix file without a file reference");
2352
3471
  }
2353
- const filename = path__namespace["default"].join(config.projectRoot, maybeRef);
3472
+ const filename = path__namespace["default"].join(config.projectRoot, fileRef);
2354
3473
  const buffer = fs__default["default"].readFileSync(filename);
2355
- let mimeType = internal.filenameToMimeType(filename);
2356
- if (!mimeType) {
2357
- mimeType = "application/octet-stream";
2358
- }
2359
- const sha256 = core.Internal.getSHA256Hash(textEncoder.encode(
2360
- // TODO: we should probably store the mimetype in the metadata and reuse it here
2361
- `data:${mimeType};base64,${buffer.toString("base64")}`));
2362
- return {
2363
- sha256,
2364
- mimeType
2365
- };
3474
+ return extractFileMetadata(fileRef, buffer);
2366
3475
  }
2367
3476
  const remainingErrors = [];
2368
3477
  const patch$1 = [];
@@ -2527,6 +3636,7 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
2527
3636
  }
2528
3637
 
2529
3638
  exports.LocalValServer = LocalValServer;
3639
+ exports.Patch = Patch;
2530
3640
  exports.PatchJSON = PatchJSON;
2531
3641
  exports.Service = Service;
2532
3642
  exports.ValFSHost = ValFSHost;