@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.
- package/dist/declarations/src/LocalValServer.d.ts +33 -17
- package/dist/declarations/src/SerializedModuleContent.d.ts +1 -3
- package/dist/declarations/src/Service.d.ts +3 -3
- package/dist/declarations/src/ValFS.d.ts +3 -1
- package/dist/declarations/src/ValFSHost.d.ts +5 -2
- package/dist/declarations/src/ValServer.d.ts +99 -4
- package/dist/declarations/src/ValSourceFileHandler.d.ts +2 -2
- package/dist/declarations/src/createValApiRouter.d.ts +9 -4
- package/dist/declarations/src/index.d.ts +1 -1
- package/dist/declarations/src/patch/validation.d.ts +3 -1
- package/dist/valbuild-server.cjs.dev.js +1615 -505
- package/dist/valbuild-server.cjs.prod.js +1615 -505
- package/dist/valbuild-server.esm.js +1660 -551
- package/package.json +5 -4
@@ -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
|
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
|
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 (!
|
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
|
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 (
|
110
|
+
if (ts__namespace["default"].isStringLiteralLike(value)) {
|
109
111
|
return fp.result.ok(value.text);
|
110
|
-
} else if (
|
112
|
+
} else if (ts__namespace["default"].isNumericLiteral(value)) {
|
111
113
|
return fp.result.ok(Number(value.text));
|
112
|
-
} else if (value.kind ===
|
114
|
+
} else if (value.kind === ts__namespace["default"].SyntaxKind.TrueKeyword) {
|
113
115
|
return fp.result.ok(true);
|
114
|
-
} else if (value.kind ===
|
116
|
+
} else if (value.kind === ts__namespace["default"].SyntaxKind.FalseKeyword) {
|
115
117
|
return fp.result.ok(false);
|
116
|
-
} else if (value.kind ===
|
118
|
+
} else if (value.kind === ts__namespace["default"].SyntaxKind.NullKeyword) {
|
117
119
|
return fp.result.ok(null);
|
118
|
-
} else if (
|
120
|
+
} else if (ts__namespace["default"].isArrayLiteralExpression(value)) {
|
119
121
|
return fp.result.all(value.elements.map(evaluateExpression));
|
120
|
-
} else if (
|
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
|
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 (!
|
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 (!
|
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 (
|
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 (!
|
200
|
+
if (!ts__namespace["default"].isPropertyAccessExpression(currentNode)) {
|
199
201
|
return false;
|
200
202
|
}
|
201
|
-
if (!
|
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
|
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 (!
|
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 (!
|
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 (
|
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 (!
|
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 (!
|
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
|
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 = [
|
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
|
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 = [
|
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
|
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 = [
|
283
|
+
const args = [ts__namespace["default"].factory.createStringLiteral(value.children[0]), toExpression({
|
282
284
|
href: value.href
|
283
285
|
})];
|
284
|
-
return
|
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 =
|
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
|
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
|
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
|
302
|
+
return ts__namespace["default"].factory.createStringLiteral(value);
|
301
303
|
} else if (typeof value === "number") {
|
302
|
-
return
|
304
|
+
return ts__namespace["default"].factory.createNumericLiteral(value);
|
303
305
|
} else if (typeof value === "boolean") {
|
304
|
-
return value ?
|
306
|
+
return value ? ts__namespace["default"].factory.createTrue() : ts__namespace["default"].factory.createFalse();
|
305
307
|
} else if (value === null) {
|
306
|
-
return
|
308
|
+
return ts__namespace["default"].factory.createNull();
|
307
309
|
} else if (Array.isArray(value)) {
|
308
|
-
return
|
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
|
322
|
+
return ts__namespace["default"].factory.createObjectLiteralExpression(Object.entries(value).map(([key, value]) => createPropertyAssignment(key, value)));
|
321
323
|
} else {
|
322
|
-
return
|
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 =
|
329
|
+
const newLine = ts__namespace["default"].NewLineKind.LineFeed;
|
328
330
|
// TODO: Handle indentation of printed code
|
329
|
-
const printer =
|
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(
|
335
|
-
const span =
|
336
|
-
const newText = `${document.text.substring(0, span.start)}${replacementText}${document.text.substring(
|
337
|
-
return [document.update(newText,
|
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 ===
|
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(
|
372
|
-
span =
|
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(
|
377
|
-
span =
|
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(
|
382
|
-
span =
|
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(
|
385
|
-
return document.update(newText,
|
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 =
|
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 =
|
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 =
|
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(
|
402
|
-
return [document.update(newText,
|
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 (
|
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 (
|
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
|
-
|
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 (
|
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 (
|
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 (
|
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 (
|
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 (
|
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 (
|
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(
|
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
|
-
} =
|
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 =
|
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
|
-
...
|
822
|
-
writeFile:
|
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 ??
|
838
|
+
const scriptTarget = this.compilerOptions.target ?? ts__namespace["default"].ScriptTarget.ES2020;
|
831
839
|
if (fileContent) {
|
832
|
-
return
|
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 =
|
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 =
|
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
|
-
...
|
866
|
-
writeFile:
|
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
|
-
...
|
1120
|
-
writeFile:
|
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
|
-
|
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
|
-
|
1207
|
-
value
|
1208
|
-
|
1209
|
-
|
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
|
-
|
1223
|
-
|
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
|
-
|
1313
|
+
const sha256 = getSha256(mimeType, input);
|
1314
|
+
return {
|
1315
|
+
sha256,
|
1316
|
+
mimeType
|
1317
|
+
};
|
1226
1318
|
}
|
1227
1319
|
|
1228
|
-
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
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
|
-
|
1328
|
+
if (Array.isArray(actualMetadata)) {
|
1234
1329
|
return {
|
1235
|
-
|
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
|
-
|
1243
|
-
|
1244
|
-
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
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
|
-
|
1268
|
-
|
1269
|
-
|
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
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
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
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
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:
|
1485
|
+
status: 200,
|
1327
1486
|
json: {
|
1328
|
-
|
1329
|
-
|
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
|
-
|
1335
|
-
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
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
|
-
|
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: {
|
1791
|
+
json: {
|
1792
|
+
modules,
|
1793
|
+
validationErrors: false
|
1794
|
+
}
|
1351
1795
|
};
|
1352
1796
|
}
|
1353
|
-
|
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: "
|
1860
|
+
message: "Missing redirect_to query param"
|
1358
1861
|
}
|
1359
1862
|
};
|
1360
1863
|
}
|
1361
|
-
|
1362
|
-
|
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
|
-
|
1376
|
-
|
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
|
-
|
1442
|
-
|
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
|
-
|
1447
|
-
|
1448
|
-
|
1449
|
-
|
1450
|
-
|
1451
|
-
|
1452
|
-
|
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
|
-
|
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
|
-
|
1459
|
-
|
1460
|
-
|
1461
|
-
|
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
|
2577
|
+
message: "Failed to get patches"
|
1480
2578
|
}
|
1481
2579
|
};
|
1482
2580
|
}
|
1483
2581
|
});
|
1484
2582
|
}
|
1485
|
-
async
|
1486
|
-
|
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:
|
2628
|
+
status: 500,
|
1489
2629
|
json: {
|
1490
|
-
message: "
|
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
|
1516
|
-
const
|
1517
|
-
if (
|
1518
|
-
return
|
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
|
-
|
1530
|
-
|
1531
|
-
|
1532
|
-
|
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
|
-
|
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.
|
1538
|
-
value:
|
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:
|
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.
|
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:
|
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
|
1679
|
-
|
1680
|
-
|
1681
|
-
|
1682
|
-
|
1683
|
-
|
1684
|
-
|
1685
|
-
|
1686
|
-
|
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
|
-
|
1689
|
-
|
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
|
-
|
1695
|
-
|
1696
|
-
|
1697
|
-
|
1698
|
-
|
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
|
-
|
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:
|
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
|
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.
|
2856
|
+
return withAuth(this.options.valSecret, cookies, "getPatches", async ({
|
1742
2857
|
token
|
1743
2858
|
}) => {
|
1744
|
-
const commit = this.options.
|
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.
|
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:
|
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.
|
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.
|
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
|
1794
|
-
if (!
|
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:
|
2914
|
+
message: "Invalid patch(es)",
|
2915
|
+
details: parsedPatches.error.issues
|
1800
2916
|
}
|
1801
2917
|
};
|
1802
2918
|
}
|
1803
|
-
|
1804
|
-
|
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:
|
1816
|
-
body: JSON.stringify(
|
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
|
1831
|
-
|
1832
|
-
|
1833
|
-
|
1834
|
-
|
1835
|
-
|
1836
|
-
|
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
|
-
|
1850
|
-
headers: this.getAuthHeaders(token, "application/json"),
|
1851
|
-
body
|
2948
|
+
headers: getAuthHeaders(data.token)
|
1852
2949
|
});
|
1853
2950
|
if (fetchRes.status === 200) {
|
1854
|
-
|
1855
|
-
|
1856
|
-
|
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
|
-
|
1860
|
-
|
1861
|
-
|
1862
|
-
|
1863
|
-
|
1864
|
-
|
1865
|
-
|
1866
|
-
|
1867
|
-
|
1868
|
-
|
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
|
-
|
1891
|
-
|
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
|
-
|
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
|
-
|
2069
|
-
|
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
|
2309
|
-
if (!
|
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,
|
3462
|
+
const filename = path__namespace["default"].join(config.projectRoot, fileRef);
|
2314
3463
|
const buffer = fs__default["default"].readFileSync(filename);
|
2315
|
-
|
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
|
2349
|
-
if (!
|
3467
|
+
const fileRef = getValidationErrorFileRef(validationError);
|
3468
|
+
if (!fileRef) {
|
2350
3469
|
// TODO:
|
2351
|
-
throw Error("Cannot fix
|
3470
|
+
throw Error("Cannot fix file without a file reference");
|
2352
3471
|
}
|
2353
|
-
const filename = path__namespace["default"].join(config.projectRoot,
|
3472
|
+
const filename = path__namespace["default"].join(config.projectRoot, fileRef);
|
2354
3473
|
const buffer = fs__default["default"].readFileSync(filename);
|
2355
|
-
|
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;
|