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