@valbuild/server 0.97.7 → 0.99.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/checkRemoteRef.d.ts +20 -0
- package/dist/declarations/src/evalValConfigFile.d.ts +17 -0
- package/dist/declarations/src/extractMetadata.d.ts +4 -0
- package/dist/declarations/src/getFileExt.d.ts +1 -0
- package/dist/declarations/src/getValidationErrorFileRef.d.ts +2 -0
- package/dist/declarations/src/hasRemoteFileSchema.d.ts +5 -0
- package/dist/declarations/src/index.d.ts +11 -0
- package/dist/declarations/src/loadValModules.d.ts +11 -1
- package/dist/declarations/src/login.d.ts +69 -0
- package/dist/declarations/src/modulePathMap.d.ts +4 -0
- package/dist/declarations/src/patch/ts/valModule.d.ts +8 -0
- package/dist/declarations/src/validateMetadata.d.ts +6 -0
- package/dist/valbuild-server.cjs.dev.js +630 -201
- package/dist/valbuild-server.cjs.prod.js +630 -201
- package/dist/valbuild-server.esm.js +452 -45
- package/package.json +4 -4
|
@@ -13,12 +13,13 @@ var node_module = require('node:module');
|
|
|
13
13
|
var internal = require('@valbuild/shared/internal');
|
|
14
14
|
var server = require('@valbuild/ui/server');
|
|
15
15
|
var crypto$1 = require('crypto');
|
|
16
|
-
var
|
|
16
|
+
var z = require('zod');
|
|
17
17
|
var sizeOf = require('image-size');
|
|
18
18
|
var zodValidationError = require('zod-validation-error');
|
|
19
19
|
var sucrase = require('sucrase');
|
|
20
20
|
var http = require('http');
|
|
21
21
|
var https = require('https');
|
|
22
|
+
var fs$1 = require('fs/promises');
|
|
22
23
|
|
|
23
24
|
function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
|
|
24
25
|
|
|
@@ -45,9 +46,11 @@ var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
|
45
46
|
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
46
47
|
var vm__default = /*#__PURE__*/_interopDefault(vm);
|
|
47
48
|
var crypto__default = /*#__PURE__*/_interopDefault(crypto$1);
|
|
49
|
+
var z__default = /*#__PURE__*/_interopDefault(z);
|
|
48
50
|
var sizeOf__default = /*#__PURE__*/_interopDefault(sizeOf);
|
|
49
51
|
var http__default = /*#__PURE__*/_interopDefault(http);
|
|
50
52
|
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
53
|
+
var fs__default$1 = /*#__PURE__*/_interopDefault(fs$1);
|
|
51
54
|
|
|
52
55
|
class ValSyntaxError {
|
|
53
56
|
constructor(message, node) {
|
|
@@ -957,6 +960,15 @@ const getCompilerOptions = (rootDir, parseConfigHost) => {
|
|
|
957
960
|
return parsedConfigFile.options;
|
|
958
961
|
};
|
|
959
962
|
|
|
963
|
+
/**
|
|
964
|
+
* The filesystem seam `loadValModules` reads through.
|
|
965
|
+
*
|
|
966
|
+
* Defaults to `ts.sys`, i.e. the real filesystem. An editor integration (see
|
|
967
|
+
* `@valbuild/language-server`) passes a host that overlays unsaved buffers, so
|
|
968
|
+
* that evaluation sees what the user is looking at rather than what was last
|
|
969
|
+
* saved.
|
|
970
|
+
*/
|
|
971
|
+
|
|
960
972
|
/**
|
|
961
973
|
* Loads the project's root `val.modules.ts` (or `.js`) using Node's `vm`
|
|
962
974
|
* module and returns its default export (a `ValModules` registry).
|
|
@@ -978,24 +990,24 @@ const getCompilerOptions = (rootDir, parseConfigHost) => {
|
|
|
978
990
|
* same trust level as running the project's build. It must never be used to
|
|
979
991
|
* evaluate untrusted or third-party modules.
|
|
980
992
|
*/
|
|
981
|
-
function loadValModules(projectRoot) {
|
|
982
|
-
const valModulesPath = findValModulesPath(projectRoot);
|
|
993
|
+
function loadValModules(projectRoot, host = ts__default["default"].sys) {
|
|
994
|
+
const valModulesPath = findValModulesPath(projectRoot, host);
|
|
983
995
|
if (!valModulesPath) {
|
|
984
996
|
throw Error(`Could not find 'val.modules.ts' nor 'val.modules.js' in project root: '${projectRoot}'`);
|
|
985
997
|
}
|
|
986
|
-
const compilerOptions = getCompilerOptions(projectRoot,
|
|
998
|
+
const compilerOptions = getCompilerOptions(projectRoot, host);
|
|
987
999
|
const cache = {};
|
|
988
|
-
const loaded = loadModule(valModulesPath, cache, compilerOptions);
|
|
1000
|
+
const loaded = loadModule(valModulesPath, cache, compilerOptions, host);
|
|
989
1001
|
const valModules = loaded.exports.default;
|
|
990
1002
|
if (!valModules) {
|
|
991
1003
|
throw Error(`Val modules file at path: '${valModulesPath}' must have a default export. Got: ${valModules}`);
|
|
992
1004
|
}
|
|
993
1005
|
return valModules;
|
|
994
1006
|
}
|
|
995
|
-
function findValModulesPath(projectRoot) {
|
|
1007
|
+
function findValModulesPath(projectRoot, host) {
|
|
996
1008
|
for (const fileName of ["val.modules.ts", "val.modules.js"]) {
|
|
997
1009
|
const candidate = path__namespace["default"].join(projectRoot, fileName);
|
|
998
|
-
if (
|
|
1010
|
+
if (host.fileExists(candidate)) {
|
|
999
1011
|
return candidate;
|
|
1000
1012
|
}
|
|
1001
1013
|
}
|
|
@@ -1036,12 +1048,15 @@ function makeStub(spec) {
|
|
|
1036
1048
|
const stub = new Proxy({}, handler);
|
|
1037
1049
|
return stub;
|
|
1038
1050
|
}
|
|
1039
|
-
function loadModule(absPath, cache, compilerOptions) {
|
|
1051
|
+
function loadModule(absPath, cache, compilerOptions, host) {
|
|
1040
1052
|
const cached = cache[absPath];
|
|
1041
1053
|
if (cached) {
|
|
1042
1054
|
return cached;
|
|
1043
1055
|
}
|
|
1044
|
-
const code =
|
|
1056
|
+
const code = host.readFile(absPath);
|
|
1057
|
+
if (code === undefined) {
|
|
1058
|
+
throw Error(`Could not read Val module file: '${absPath}'`);
|
|
1059
|
+
}
|
|
1045
1060
|
const transpiled = ts__default["default"].transpileModule(code, {
|
|
1046
1061
|
compilerOptions: {
|
|
1047
1062
|
target: ts__default["default"].ScriptTarget.ES2020,
|
|
@@ -1064,17 +1079,17 @@ function loadModule(absPath, cache, compilerOptions) {
|
|
|
1064
1079
|
return makeStub(spec);
|
|
1065
1080
|
}
|
|
1066
1081
|
if (spec.startsWith(".") || path__namespace["default"].isAbsolute(spec)) {
|
|
1067
|
-
const resolved = resolveRelative(dirName, spec);
|
|
1082
|
+
const resolved = resolveRelative(dirName, spec, host);
|
|
1068
1083
|
if (!resolved) {
|
|
1069
1084
|
throw Error(`Could not resolve module '${spec}' from '${absPath}'`);
|
|
1070
1085
|
}
|
|
1071
|
-
return loadModule(resolved, cache, compilerOptions).exports;
|
|
1086
|
+
return loadModule(resolved, cache, compilerOptions, host).exports;
|
|
1072
1087
|
}
|
|
1073
1088
|
// Non-relative specifier: it might be a tsconfig path alias (e.g. "_/val.config")
|
|
1074
1089
|
// pointing at a local source file, or an actual node_modules package.
|
|
1075
|
-
const tsResolved = (_ts$resolveModuleName = ts__default["default"].resolveModuleName(spec, absPath, compilerOptions,
|
|
1090
|
+
const tsResolved = (_ts$resolveModuleName = ts__default["default"].resolveModuleName(spec, absPath, compilerOptions, host).resolvedModule) === null || _ts$resolveModuleName === void 0 ? void 0 : _ts$resolveModuleName.resolvedFileName;
|
|
1076
1091
|
if (tsResolved && !tsResolved.includes("/node_modules/") && !tsResolved.endsWith(".d.ts")) {
|
|
1077
|
-
return loadModule(tsResolved, cache, compilerOptions).exports;
|
|
1092
|
+
return loadModule(tsResolved, cache, compilerOptions, host).exports;
|
|
1078
1093
|
}
|
|
1079
1094
|
// Real node_modules package – use the real require so user modules share
|
|
1080
1095
|
// the same @valbuild/core instance as extractValModules.
|
|
@@ -1100,24 +1115,25 @@ function loadModule(absPath, cache, compilerOptions) {
|
|
|
1100
1115
|
script.runInContext(context);
|
|
1101
1116
|
return moduleObj;
|
|
1102
1117
|
}
|
|
1103
|
-
function resolveRelative(dirName, spec) {
|
|
1118
|
+
function resolveRelative(dirName, spec, host) {
|
|
1119
|
+
var _host$directoryExists;
|
|
1104
1120
|
const base = path__namespace["default"].resolve(dirName, spec);
|
|
1105
1121
|
// Exact file (with extension)
|
|
1106
|
-
if (
|
|
1122
|
+
if (host.fileExists(base)) {
|
|
1107
1123
|
return base;
|
|
1108
1124
|
}
|
|
1109
1125
|
// Probe extensions (handles `./x.val` -> `./x.val.ts`)
|
|
1110
1126
|
for (const ext of RESOLVE_EXTENSIONS) {
|
|
1111
1127
|
const candidate = base + ext;
|
|
1112
|
-
if (
|
|
1128
|
+
if (host.fileExists(candidate)) {
|
|
1113
1129
|
return candidate;
|
|
1114
1130
|
}
|
|
1115
1131
|
}
|
|
1116
1132
|
// Directory index
|
|
1117
|
-
if (
|
|
1133
|
+
if (((_host$directoryExists = host.directoryExists) === null || _host$directoryExists === void 0 ? void 0 : _host$directoryExists.call(host, base)) ?? fs__default["default"].existsSync(base)) {
|
|
1118
1134
|
for (const ext of RESOLVE_EXTENSIONS) {
|
|
1119
1135
|
const candidate = path__namespace["default"].join(base, "index" + ext);
|
|
1120
|
-
if (
|
|
1136
|
+
if (host.fileExists(candidate)) {
|
|
1121
1137
|
return candidate;
|
|
1122
1138
|
}
|
|
1123
1139
|
}
|
|
@@ -1144,7 +1160,9 @@ async function createService(projectRoot, host = {
|
|
|
1144
1160
|
}) {
|
|
1145
1161
|
const compilerOptions = getCompilerOptions(projectRoot, host);
|
|
1146
1162
|
const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
|
|
1147
|
-
|
|
1163
|
+
// Read val.modules (and everything it imports) through the same host, so an
|
|
1164
|
+
// embedder that overlays unsaved editor buffers evaluates what the user sees.
|
|
1165
|
+
const valModules = loadValModules(projectRoot, host);
|
|
1148
1166
|
const extracted = await core.extractValModules(valModules);
|
|
1149
1167
|
return new Service(projectRoot, sourceFileHandler, extracted);
|
|
1150
1168
|
}
|
|
@@ -1254,9 +1272,9 @@ function decodeJwt(token, secretKey) {
|
|
|
1254
1272
|
function getExpire() {
|
|
1255
1273
|
return Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 4; // 4 days
|
|
1256
1274
|
}
|
|
1257
|
-
const JwtHeaderSchema =
|
|
1258
|
-
alg:
|
|
1259
|
-
typ:
|
|
1275
|
+
const JwtHeaderSchema = z.z.object({
|
|
1276
|
+
alg: z.z.literal("HS256"),
|
|
1277
|
+
typ: z.z.literal("JWT")
|
|
1260
1278
|
});
|
|
1261
1279
|
const jwtHeader = {
|
|
1262
1280
|
alg: "HS256",
|
|
@@ -2271,9 +2289,9 @@ class ValOpsFS extends ValOps {
|
|
|
2271
2289
|
});
|
|
2272
2290
|
if (res.ok) {
|
|
2273
2291
|
const json = await res.json();
|
|
2274
|
-
const parsed =
|
|
2275
|
-
nonce:
|
|
2276
|
-
expiresAt:
|
|
2292
|
+
const parsed = z.z.object({
|
|
2293
|
+
nonce: z.z.string(),
|
|
2294
|
+
expiresAt: z.z.string()
|
|
2277
2295
|
}).safeParse(json);
|
|
2278
2296
|
if (parsed.success) {
|
|
2279
2297
|
return {
|
|
@@ -2834,7 +2852,7 @@ class ValOpsFS extends ValOps {
|
|
|
2834
2852
|
}]
|
|
2835
2853
|
};
|
|
2836
2854
|
}
|
|
2837
|
-
const metadataParseRes = this.parseJsonFile(metadataFilePath,
|
|
2855
|
+
const metadataParseRes = this.parseJsonFile(metadataFilePath, z.z.record(z.z.string(), z.z.union([z.z.string(), z.z.number()])));
|
|
2838
2856
|
if (metadataParseRes.error) {
|
|
2839
2857
|
return {
|
|
2840
2858
|
errors: [metadataParseRes.error]
|
|
@@ -3240,121 +3258,121 @@ class FSOpsHost {
|
|
|
3240
3258
|
fs__default["default"].copyFileSync(from, to);
|
|
3241
3259
|
}
|
|
3242
3260
|
}
|
|
3243
|
-
const FSPatch =
|
|
3244
|
-
path:
|
|
3261
|
+
const FSPatch = z.z.object({
|
|
3262
|
+
path: z.z.string().refine(p => p.startsWith("/") && p.includes(".val."), "Path is not valid. Must start with '/' and include '.val.'"),
|
|
3245
3263
|
patch: internal.Patch,
|
|
3246
|
-
patchId:
|
|
3247
|
-
baseSha:
|
|
3264
|
+
patchId: z.z.string(),
|
|
3265
|
+
baseSha: z.z.string(),
|
|
3248
3266
|
parentRef: internal.ParentRef,
|
|
3249
|
-
authorId:
|
|
3250
|
-
createdAt:
|
|
3251
|
-
coreVersion:
|
|
3267
|
+
authorId: z.z.string().refine(p => true).nullable(),
|
|
3268
|
+
createdAt: z.z.string().datetime(),
|
|
3269
|
+
coreVersion: z.z.string().nullable(),
|
|
3252
3270
|
// TODO: use this to check if patch is compatible with current core version?
|
|
3253
|
-
sessionId:
|
|
3271
|
+
sessionId: z.z.string().nullable()
|
|
3254
3272
|
});
|
|
3255
|
-
const FSPatchBase =
|
|
3256
|
-
baseSha:
|
|
3257
|
-
timestamp:
|
|
3273
|
+
const FSPatchBase = z.z.object({
|
|
3274
|
+
baseSha: z.z.string().refine(p => true),
|
|
3275
|
+
timestamp: z.z.string().datetime()
|
|
3258
3276
|
});
|
|
3259
3277
|
|
|
3260
3278
|
const textEncoder$1 = new TextEncoder();
|
|
3261
|
-
const PatchId =
|
|
3262
|
-
const CommitSha =
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
const ModuleFilePath =
|
|
3266
|
-
const Metadata =
|
|
3267
|
-
mimeType:
|
|
3268
|
-
width:
|
|
3269
|
-
height:
|
|
3270
|
-
}),
|
|
3271
|
-
mimeType:
|
|
3279
|
+
const PatchId = z.z.string().refine(s => !!s); // TODO: validate
|
|
3280
|
+
const CommitSha = z.z.string().refine(s => !!s); // TODO: validate
|
|
3281
|
+
z.z.string().refine(s => !!s); // TODO: validate
|
|
3282
|
+
z.z.string().refine(s => !!s); // TODO: validate
|
|
3283
|
+
const ModuleFilePath = z.z.string().refine(s => !!s); // TODO: validate
|
|
3284
|
+
const Metadata = z.z.union([z.z.object({
|
|
3285
|
+
mimeType: z.z.string(),
|
|
3286
|
+
width: z.z.number(),
|
|
3287
|
+
height: z.z.number()
|
|
3288
|
+
}), z.z.object({
|
|
3289
|
+
mimeType: z.z.string()
|
|
3272
3290
|
})]);
|
|
3273
|
-
const MetadataRes =
|
|
3291
|
+
const MetadataRes = z.z.object({
|
|
3274
3292
|
filePath: ModuleFilePath,
|
|
3275
3293
|
metadata: Metadata,
|
|
3276
|
-
type:
|
|
3294
|
+
type: z.z.union([z.z.literal("file"), z.z.literal("image")]).nullable()
|
|
3277
3295
|
});
|
|
3278
|
-
const SummaryResponse =
|
|
3279
|
-
commitSummary:
|
|
3296
|
+
const SummaryResponse = z.z.object({
|
|
3297
|
+
commitSummary: z.z.string()
|
|
3280
3298
|
});
|
|
3281
|
-
const GetApplicablePatches =
|
|
3282
|
-
patches:
|
|
3283
|
-
path:
|
|
3299
|
+
const GetApplicablePatches = z.z.object({
|
|
3300
|
+
patches: z.z.array(z.z.object({
|
|
3301
|
+
path: z.z.string(),
|
|
3284
3302
|
patch: internal.Patch.nullable(),
|
|
3285
|
-
patchId:
|
|
3286
|
-
authorId:
|
|
3287
|
-
baseSha:
|
|
3288
|
-
createdAt:
|
|
3289
|
-
applied:
|
|
3290
|
-
commitSha:
|
|
3303
|
+
patchId: z.z.string(),
|
|
3304
|
+
authorId: z.z.string().nullable(),
|
|
3305
|
+
baseSha: z.z.string(),
|
|
3306
|
+
createdAt: z.z.string(),
|
|
3307
|
+
applied: z.z.object({
|
|
3308
|
+
commitSha: z.z.string()
|
|
3291
3309
|
}).nullable()
|
|
3292
3310
|
})),
|
|
3293
|
-
commits:
|
|
3294
|
-
commitSha:
|
|
3295
|
-
clientCommitSha:
|
|
3296
|
-
parentCommitSha:
|
|
3297
|
-
commitMessage:
|
|
3298
|
-
branch:
|
|
3299
|
-
creator:
|
|
3300
|
-
createdAt:
|
|
3311
|
+
commits: z.z.array(z.z.object({
|
|
3312
|
+
commitSha: z.z.string(),
|
|
3313
|
+
clientCommitSha: z.z.string(),
|
|
3314
|
+
parentCommitSha: z.z.string(),
|
|
3315
|
+
commitMessage: z.z.string().nullable(),
|
|
3316
|
+
branch: z.z.string(),
|
|
3317
|
+
creator: z.z.string(),
|
|
3318
|
+
createdAt: z.z.string()
|
|
3301
3319
|
})).optional(),
|
|
3302
|
-
deployments:
|
|
3303
|
-
deploymentId:
|
|
3304
|
-
commitSha:
|
|
3305
|
-
deploymentState:
|
|
3306
|
-
createdAt:
|
|
3307
|
-
updatedAt:
|
|
3320
|
+
deployments: z.z.array(z.z.object({
|
|
3321
|
+
deploymentId: z.z.string(),
|
|
3322
|
+
commitSha: z.z.string(),
|
|
3323
|
+
deploymentState: z.z.string(),
|
|
3324
|
+
createdAt: z.z.string(),
|
|
3325
|
+
updatedAt: z.z.string()
|
|
3308
3326
|
})).optional()
|
|
3309
3327
|
});
|
|
3310
|
-
const FilesResponse =
|
|
3311
|
-
files:
|
|
3312
|
-
filePath:
|
|
3313
|
-
location:
|
|
3328
|
+
const FilesResponse = z.z.object({
|
|
3329
|
+
files: z.z.array(z.z.union([z.z.object({
|
|
3330
|
+
filePath: z.z.string(),
|
|
3331
|
+
location: z.z.literal("patch"),
|
|
3314
3332
|
patchId: PatchId,
|
|
3315
|
-
value:
|
|
3316
|
-
remote:
|
|
3317
|
-
}),
|
|
3318
|
-
filePath:
|
|
3319
|
-
location:
|
|
3333
|
+
value: z.z.string(),
|
|
3334
|
+
remote: z.z.boolean()
|
|
3335
|
+
}), z.z.object({
|
|
3336
|
+
filePath: z.z.string(),
|
|
3337
|
+
location: z.z.literal("repo"),
|
|
3320
3338
|
commitSha: CommitSha,
|
|
3321
|
-
value:
|
|
3339
|
+
value: z.z.string()
|
|
3322
3340
|
})])),
|
|
3323
|
-
errors:
|
|
3324
|
-
filePath:
|
|
3325
|
-
location:
|
|
3341
|
+
errors: z.z.array(z.z.union([z.z.object({
|
|
3342
|
+
filePath: z.z.string(),
|
|
3343
|
+
location: z.z.literal("patch"),
|
|
3326
3344
|
patchId: PatchId,
|
|
3327
|
-
message:
|
|
3328
|
-
remote:
|
|
3329
|
-
}),
|
|
3330
|
-
filePath:
|
|
3331
|
-
location:
|
|
3345
|
+
message: z.z.string(),
|
|
3346
|
+
remote: z.z.boolean()
|
|
3347
|
+
}), z.z.object({
|
|
3348
|
+
filePath: z.z.string(),
|
|
3349
|
+
location: z.z.literal("repo"),
|
|
3332
3350
|
commitSha: CommitSha,
|
|
3333
|
-
message:
|
|
3351
|
+
message: z.z.string()
|
|
3334
3352
|
})])).optional()
|
|
3335
3353
|
});
|
|
3336
|
-
const SavePatchResponse =
|
|
3354
|
+
const SavePatchResponse = z.z.object({
|
|
3337
3355
|
patchId: PatchId
|
|
3338
3356
|
});
|
|
3339
|
-
const DeletePatchesResponse =
|
|
3340
|
-
deleted:
|
|
3341
|
-
errors:
|
|
3342
|
-
message:
|
|
3357
|
+
const DeletePatchesResponse = z.z.object({
|
|
3358
|
+
deleted: z.z.array(PatchId),
|
|
3359
|
+
errors: z.z.array(z.z.object({
|
|
3360
|
+
message: z.z.string(),
|
|
3343
3361
|
patchId: PatchId
|
|
3344
3362
|
})).optional()
|
|
3345
3363
|
});
|
|
3346
|
-
const SavePatchFileResponse =
|
|
3364
|
+
const SavePatchFileResponse = z.z.object({
|
|
3347
3365
|
patchId: PatchId,
|
|
3348
3366
|
filePath: ModuleFilePath
|
|
3349
3367
|
});
|
|
3350
|
-
const CommitResponse =
|
|
3351
|
-
updatedFiles:
|
|
3368
|
+
const CommitResponse = z.z.object({
|
|
3369
|
+
updatedFiles: z.z.array(z.z.string()),
|
|
3352
3370
|
commit: CommitSha,
|
|
3353
|
-
branch:
|
|
3371
|
+
branch: z.z.string()
|
|
3354
3372
|
});
|
|
3355
|
-
const NonceResponse =
|
|
3356
|
-
nonce:
|
|
3357
|
-
url:
|
|
3373
|
+
const NonceResponse = z.z.object({
|
|
3374
|
+
nonce: z.z.string(),
|
|
3375
|
+
url: z.z.string()
|
|
3358
3376
|
});
|
|
3359
3377
|
class ValOpsHttp extends ValOps {
|
|
3360
3378
|
constructor(contentUrl, project, commitSha,
|
|
@@ -3397,9 +3415,9 @@ class ValOpsHttp extends ValOps {
|
|
|
3397
3415
|
});
|
|
3398
3416
|
if (res.ok) {
|
|
3399
3417
|
const json = await res.json();
|
|
3400
|
-
const parsed =
|
|
3401
|
-
nonce:
|
|
3402
|
-
expiresAt:
|
|
3418
|
+
const parsed = z.z.object({
|
|
3419
|
+
nonce: z.z.string(),
|
|
3420
|
+
expiresAt: z.z.string()
|
|
3403
3421
|
}).safeParse(json);
|
|
3404
3422
|
if (parsed.success) {
|
|
3405
3423
|
const {
|
|
@@ -4238,8 +4256,8 @@ class ValOpsHttp extends ValOps {
|
|
|
4238
4256
|
};
|
|
4239
4257
|
}
|
|
4240
4258
|
if ((_res$headers$get5 = res.headers.get("Content-Type")) !== null && _res$headers$get5 !== void 0 && _res$headers$get5.includes("application/json")) {
|
|
4241
|
-
const json =
|
|
4242
|
-
isNotFastForward:
|
|
4259
|
+
const json = z.z.object({
|
|
4260
|
+
isNotFastForward: z.z.boolean().optional()
|
|
4243
4261
|
}).safeParse(await res.json());
|
|
4244
4262
|
if (json.success && json.data.isNotFastForward) {
|
|
4245
4263
|
return {
|
|
@@ -4272,10 +4290,10 @@ class ValOpsHttp extends ValOps {
|
|
|
4272
4290
|
}
|
|
4273
4291
|
|
|
4274
4292
|
const host = process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST;
|
|
4275
|
-
const SettingsSchema =
|
|
4276
|
-
publicProjectId:
|
|
4277
|
-
remoteFileBuckets:
|
|
4278
|
-
bucket:
|
|
4293
|
+
const SettingsSchema = z.z.object({
|
|
4294
|
+
publicProjectId: z.z.string(),
|
|
4295
|
+
remoteFileBuckets: z.z.array(z.z.object({
|
|
4296
|
+
bucket: z.z.string()
|
|
4279
4297
|
}))
|
|
4280
4298
|
});
|
|
4281
4299
|
async function getSettings(projectName, auth) {
|
|
@@ -4421,21 +4439,21 @@ function hasRemoteFileSchema(schema) {
|
|
|
4421
4439
|
|
|
4422
4440
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
4423
4441
|
const ValServer = (valModules, options, callbacks) => {
|
|
4424
|
-
const AIContentBlock =
|
|
4425
|
-
type:
|
|
4426
|
-
text:
|
|
4427
|
-
}),
|
|
4428
|
-
type:
|
|
4429
|
-
url:
|
|
4442
|
+
const AIContentBlock = z.z.union([z.z.object({
|
|
4443
|
+
type: z.z.literal("text"),
|
|
4444
|
+
text: z.z.string()
|
|
4445
|
+
}), z.z.object({
|
|
4446
|
+
type: z.z.literal("image_url"),
|
|
4447
|
+
url: z.z.string()
|
|
4430
4448
|
})]);
|
|
4431
|
-
const AIMessageContent =
|
|
4432
|
-
const ProfilesResponse =
|
|
4433
|
-
profiles:
|
|
4434
|
-
profileId:
|
|
4435
|
-
fullName:
|
|
4436
|
-
email:
|
|
4437
|
-
avatar:
|
|
4438
|
-
url:
|
|
4449
|
+
const AIMessageContent = z.z.union([z.z.string(), z.z.array(AIContentBlock)]);
|
|
4450
|
+
const ProfilesResponse = z.z.object({
|
|
4451
|
+
profiles: z.z.array(z.z.object({
|
|
4452
|
+
profileId: z.z.string(),
|
|
4453
|
+
fullName: z.z.string(),
|
|
4454
|
+
email: z.z.string().optional(),
|
|
4455
|
+
avatar: z.z.object({
|
|
4456
|
+
url: z.z.string()
|
|
4439
4457
|
}).nullable()
|
|
4440
4458
|
}))
|
|
4441
4459
|
});
|
|
@@ -4942,12 +4960,12 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
4942
4960
|
headers: getAuthHeaders(data.token, "application/json")
|
|
4943
4961
|
});
|
|
4944
4962
|
if (fetchRes.status === 200) {
|
|
4945
|
-
const json =
|
|
4946
|
-
member_role:
|
|
4947
|
-
id:
|
|
4948
|
-
full_name:
|
|
4949
|
-
username:
|
|
4950
|
-
avatar_url:
|
|
4963
|
+
const json = z.z.object({
|
|
4964
|
+
member_role: z.z.union([z.z.literal("owner"), z.z.literal("developer"), z.z.literal("editor")]).optional(),
|
|
4965
|
+
id: z.z.string(),
|
|
4966
|
+
full_name: z.z.string().optional(),
|
|
4967
|
+
username: z.z.string().optional(),
|
|
4968
|
+
avatar_url: z.z.string().optional()
|
|
4951
4969
|
}).safeParse(await fetchRes.json());
|
|
4952
4970
|
if (json.success) {
|
|
4953
4971
|
return {
|
|
@@ -4969,8 +4987,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
4969
4987
|
};
|
|
4970
4988
|
}
|
|
4971
4989
|
} else {
|
|
4972
|
-
const json =
|
|
4973
|
-
message:
|
|
4990
|
+
const json = z.z.object({
|
|
4991
|
+
message: z.z.string()
|
|
4974
4992
|
}).safeParse(await fetchRes.json());
|
|
4975
4993
|
const message = internal.getErrorMessageFromUnknownJson(json, "Unknown error");
|
|
4976
4994
|
return {
|
|
@@ -5832,8 +5850,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
5832
5850
|
}
|
|
5833
5851
|
};
|
|
5834
5852
|
}
|
|
5835
|
-
const PostSaveBody =
|
|
5836
|
-
patchIds:
|
|
5853
|
+
const PostSaveBody = z.z.object({
|
|
5854
|
+
patchIds: z.z.array(z.z.string().refine(id => true // TODO:
|
|
5837
5855
|
))
|
|
5838
5856
|
});
|
|
5839
5857
|
const bodyRes = PostSaveBody.safeParse(body);
|
|
@@ -6069,16 +6087,16 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
6069
6087
|
const authData = authDataRes.json.remoteFileAuth;
|
|
6070
6088
|
const execFetch = async headers => {
|
|
6071
6089
|
try {
|
|
6072
|
-
const SessionsResponse =
|
|
6073
|
-
sessions:
|
|
6074
|
-
id:
|
|
6075
|
-
name:
|
|
6076
|
-
createdAt:
|
|
6077
|
-
updatedAt:
|
|
6090
|
+
const SessionsResponse = z.z.object({
|
|
6091
|
+
sessions: z.z.array(z.z.object({
|
|
6092
|
+
id: z.z.string(),
|
|
6093
|
+
name: z.z.string().nullable(),
|
|
6094
|
+
createdAt: z.z.string(),
|
|
6095
|
+
updatedAt: z.z.string()
|
|
6078
6096
|
})),
|
|
6079
|
-
nextCursor:
|
|
6080
|
-
updatedAt:
|
|
6081
|
-
id:
|
|
6097
|
+
nextCursor: z.z.object({
|
|
6098
|
+
updatedAt: z.z.string(),
|
|
6099
|
+
id: z.z.string()
|
|
6082
6100
|
}).nullable().optional()
|
|
6083
6101
|
});
|
|
6084
6102
|
const params = new URLSearchParams();
|
|
@@ -6269,14 +6287,14 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
6269
6287
|
const authData = authDataRes.json.remoteFileAuth;
|
|
6270
6288
|
const execFetch = async headers => {
|
|
6271
6289
|
try {
|
|
6272
|
-
const SessionMessagesResponse =
|
|
6273
|
-
messages:
|
|
6274
|
-
role:
|
|
6290
|
+
const SessionMessagesResponse = z.z.object({
|
|
6291
|
+
messages: z.z.array(z.z.object({
|
|
6292
|
+
role: z.z.string(),
|
|
6275
6293
|
content: AIMessageContent
|
|
6276
6294
|
})),
|
|
6277
|
-
nextCursor:
|
|
6278
|
-
updatedAt:
|
|
6279
|
-
id:
|
|
6295
|
+
nextCursor: z.z.object({
|
|
6296
|
+
updatedAt: z.z.string(),
|
|
6297
|
+
id: z.z.string()
|
|
6280
6298
|
}).nullable().optional()
|
|
6281
6299
|
});
|
|
6282
6300
|
const params = new URLSearchParams();
|
|
@@ -6425,14 +6443,14 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
6425
6443
|
};
|
|
6426
6444
|
}
|
|
6427
6445
|
const rawUpstreamJson = await upstreamRes.json();
|
|
6428
|
-
const UpstreamResponse =
|
|
6429
|
-
patchId:
|
|
6430
|
-
files:
|
|
6431
|
-
filePath:
|
|
6432
|
-
metadata:
|
|
6433
|
-
width:
|
|
6434
|
-
height:
|
|
6435
|
-
mimeType:
|
|
6446
|
+
const UpstreamResponse = z.z.object({
|
|
6447
|
+
patchId: z.z.string(),
|
|
6448
|
+
files: z.z.array(z.z.object({
|
|
6449
|
+
filePath: z.z.string(),
|
|
6450
|
+
metadata: z.z.object({
|
|
6451
|
+
width: z.z.number(),
|
|
6452
|
+
height: z.z.number(),
|
|
6453
|
+
mimeType: z.z.string()
|
|
6436
6454
|
})
|
|
6437
6455
|
}))
|
|
6438
6456
|
});
|
|
@@ -6591,8 +6609,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
6591
6609
|
}
|
|
6592
6610
|
};
|
|
6593
6611
|
}
|
|
6594
|
-
const UpstreamResponse =
|
|
6595
|
-
key:
|
|
6612
|
+
const UpstreamResponse = z.z.object({
|
|
6613
|
+
key: z.z.string()
|
|
6596
6614
|
});
|
|
6597
6615
|
const json = UpstreamResponse.safeParse(await upstreamRes.json());
|
|
6598
6616
|
if (!json.success) {
|
|
@@ -6775,18 +6793,18 @@ function getStateFromCookie(stateCookie) {
|
|
|
6775
6793
|
function createStateCookie(state) {
|
|
6776
6794
|
return Buffer.from(JSON.stringify(state), "utf8").toString("base64");
|
|
6777
6795
|
}
|
|
6778
|
-
const ValAppJwtPayload =
|
|
6779
|
-
sub:
|
|
6780
|
-
exp:
|
|
6781
|
-
project:
|
|
6782
|
-
org:
|
|
6796
|
+
const ValAppJwtPayload = z.z.object({
|
|
6797
|
+
sub: z.z.string(),
|
|
6798
|
+
exp: z.z.number(),
|
|
6799
|
+
project: z.z.string(),
|
|
6800
|
+
org: z.z.string()
|
|
6783
6801
|
});
|
|
6784
|
-
const IntegratedServerJwtPayload =
|
|
6785
|
-
sub:
|
|
6786
|
-
exp:
|
|
6787
|
-
token:
|
|
6788
|
-
org:
|
|
6789
|
-
project:
|
|
6802
|
+
const IntegratedServerJwtPayload = z.z.object({
|
|
6803
|
+
sub: z.z.string(),
|
|
6804
|
+
exp: z.z.number(),
|
|
6805
|
+
token: z.z.string(),
|
|
6806
|
+
org: z.z.string(),
|
|
6807
|
+
project: z.z.string()
|
|
6790
6808
|
});
|
|
6791
6809
|
async function withAuth(secret, cookies, errorMessageType, handler) {
|
|
6792
6810
|
const cookie = cookies[internal.VAL_SESSION_COOKIE];
|
|
@@ -7282,7 +7300,7 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
|
7282
7300
|
let query = {};
|
|
7283
7301
|
if (reqDefinition.query) {
|
|
7284
7302
|
const queryRules = reqDefinition.query;
|
|
7285
|
-
const queryRes =
|
|
7303
|
+
const queryRes = z.z.object(queryRules).safeParse(actualQueryParams);
|
|
7286
7304
|
if (!queryRes.success) {
|
|
7287
7305
|
return zodErrorResult(queryRes.error, `invalid query params: (${JSON.stringify(actualQueryParams)})`);
|
|
7288
7306
|
}
|
|
@@ -7356,7 +7374,7 @@ function getCookies(req, cookiesDef) {
|
|
|
7356
7374
|
input[name.trim()] = value;
|
|
7357
7375
|
}
|
|
7358
7376
|
}
|
|
7359
|
-
return
|
|
7377
|
+
return z.z.object(cookiesDef).safeParse(input);
|
|
7360
7378
|
}
|
|
7361
7379
|
|
|
7362
7380
|
const JsFileLookupMapping = [
|
|
@@ -7760,6 +7778,33 @@ async function downloadFileFromRemote(ref, filePath) {
|
|
|
7760
7778
|
// record-level fix expands into per-entry errors that should point at the
|
|
7761
7779
|
// individual entry (e.g. `?p="/public/val/logo.png"`) rather than the record.
|
|
7762
7780
|
|
|
7781
|
+
// Gallery entries are keyed by their file path. Remote galleries key uploaded
|
|
7782
|
+
// entries by a remote URL while keeping the file on disk at its local path, so
|
|
7783
|
+
// normalize a remote-URL key back to that local path for on-disk reads.
|
|
7784
|
+
function galleryKeyToLocalPath(key) {
|
|
7785
|
+
const res = core.Internal.remote.splitRemoteRef(key);
|
|
7786
|
+
return res.status === "success" ? `/${res.filePath}` : key;
|
|
7787
|
+
}
|
|
7788
|
+
|
|
7789
|
+
// Patch path of the record that holds a media entry. Media keys are file paths
|
|
7790
|
+
// that can contain dots, which sourceToPatchPath cannot round-trip, so strip
|
|
7791
|
+
// the (JSON-encoded) key segment off the entry source path and convert the
|
|
7792
|
+
// remaining parent path — whose ancestors are plain object keys / array
|
|
7793
|
+
// indices — instead.
|
|
7794
|
+
function parentPatchPathOfMediaEntry(sourcePath, entryKey) {
|
|
7795
|
+
const jsonKey = JSON.stringify(entryKey);
|
|
7796
|
+
let parent = sourcePath;
|
|
7797
|
+
if (parent.endsWith(jsonKey)) {
|
|
7798
|
+
parent = parent.slice(0, parent.length - jsonKey.length);
|
|
7799
|
+
}
|
|
7800
|
+
if (parent.endsWith(".")) {
|
|
7801
|
+
parent = parent.slice(0, -1);
|
|
7802
|
+
} else if (parent.endsWith(core.Internal.ModuleFilePathSep)) {
|
|
7803
|
+
parent = parent.slice(0, parent.length - core.Internal.ModuleFilePathSep.length);
|
|
7804
|
+
}
|
|
7805
|
+
return patch.sourceToPatchPath(parent);
|
|
7806
|
+
}
|
|
7807
|
+
|
|
7763
7808
|
// TODO: find a better name? transformFixesToPatch?
|
|
7764
7809
|
async function createFixPatch(config, apply, sourcePath, validationError, remoteFiles, moduleSource, moduleSchema) {
|
|
7765
7810
|
const remainingErrors = [];
|
|
@@ -7937,6 +7982,61 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
7937
7982
|
path: patch.sourceToPatchPath(sourcePath)
|
|
7938
7983
|
});
|
|
7939
7984
|
}
|
|
7985
|
+
} else if (fix === "images:upload-remote" || fix === "files:upload-remote") {
|
|
7986
|
+
// Gallery entry: the record is keyed by the file path, so uploading to
|
|
7987
|
+
// remote means renaming the key from the local path to the remote URL
|
|
7988
|
+
// (remove the old key, add the new one with the same metadata).
|
|
7989
|
+
const remoteFile = remoteFiles[sourcePath];
|
|
7990
|
+
if (!remoteFile) {
|
|
7991
|
+
remainingErrors.push({
|
|
7992
|
+
...validationError,
|
|
7993
|
+
message: "Cannot fix local to remote gallery entry: remote file was not uploaded",
|
|
7994
|
+
fixes: undefined
|
|
7995
|
+
});
|
|
7996
|
+
continue;
|
|
7997
|
+
}
|
|
7998
|
+
const metadata = remoteFile.metadata;
|
|
7999
|
+
if (!metadata) {
|
|
8000
|
+
remainingErrors.push({
|
|
8001
|
+
...validationError,
|
|
8002
|
+
message: "Failed to get metadata for remote gallery file",
|
|
8003
|
+
fixes: undefined
|
|
8004
|
+
});
|
|
8005
|
+
continue;
|
|
8006
|
+
}
|
|
8007
|
+
// The entry is keyed by its (local) file path, which can contain dots
|
|
8008
|
+
// (e.g. image.png). sourceToPatchPath can't round-trip such keys, so
|
|
8009
|
+
// build the patch path from the parent record path plus the key (which
|
|
8010
|
+
// validateMediaKey carries as the error value), like the UI does.
|
|
8011
|
+
const entryKey = validationError.value;
|
|
8012
|
+
if (typeof entryKey !== "string") {
|
|
8013
|
+
remainingErrors.push({
|
|
8014
|
+
...validationError,
|
|
8015
|
+
message: "Cannot rewrite remote gallery entry: missing entry key",
|
|
8016
|
+
fixes: undefined
|
|
8017
|
+
});
|
|
8018
|
+
continue;
|
|
8019
|
+
}
|
|
8020
|
+
const parentPath = parentPatchPathOfMediaEntry(sourcePath, entryKey);
|
|
8021
|
+
const removePath = parentPath.concat(entryKey);
|
|
8022
|
+
const addPath = parentPath.concat(remoteFile.ref);
|
|
8023
|
+
if (!patch.isNotRoot(removePath) || !patch.isNotRoot(addPath)) {
|
|
8024
|
+
remainingErrors.push({
|
|
8025
|
+
...validationError,
|
|
8026
|
+
message: "Cannot rewrite remote gallery entry at root path",
|
|
8027
|
+
fixes: undefined
|
|
8028
|
+
});
|
|
8029
|
+
continue;
|
|
8030
|
+
}
|
|
8031
|
+
patch$1.push({
|
|
8032
|
+
op: "remove",
|
|
8033
|
+
path: removePath
|
|
8034
|
+
});
|
|
8035
|
+
patch$1.push({
|
|
8036
|
+
op: "add",
|
|
8037
|
+
path: addPath,
|
|
8038
|
+
value: metadata
|
|
8039
|
+
});
|
|
7940
8040
|
} else if (fix === "image:download-remote" || fix === "file:download-remote") {
|
|
7941
8041
|
const v = getRemoteValueFromValidationError(validationError);
|
|
7942
8042
|
if (!v.success) {
|
|
@@ -8013,7 +8113,7 @@ async function createFixPatch(config, apply, sourcePath, validationError, remote
|
|
|
8013
8113
|
}
|
|
8014
8114
|
const gallerySource = moduleSource;
|
|
8015
8115
|
for (const [entryKey, storedEntry] of Object.entries(gallerySource)) {
|
|
8016
|
-
const filename = path__namespace["default"].join(config.projectRoot, entryKey);
|
|
8116
|
+
const filename = path__namespace["default"].join(config.projectRoot, galleryKeyToLocalPath(entryKey));
|
|
8017
8117
|
let buffer;
|
|
8018
8118
|
try {
|
|
8019
8119
|
buffer = fs__default["default"].readFileSync(filename);
|
|
@@ -8241,6 +8341,294 @@ async function getFileMetadata(projectRoot, validationError) {
|
|
|
8241
8341
|
return extractFileMetadata(fileRef);
|
|
8242
8342
|
}
|
|
8243
8343
|
|
|
8344
|
+
function validateMetadata(actualMetadata, expectedMetadata) {
|
|
8345
|
+
const missingMetadata = [];
|
|
8346
|
+
const erroneousMetadata = {};
|
|
8347
|
+
if (typeof actualMetadata !== "object" || actualMetadata === null) {
|
|
8348
|
+
return {
|
|
8349
|
+
globalErrors: ["Metadata is wrong type: must be an object."]
|
|
8350
|
+
};
|
|
8351
|
+
}
|
|
8352
|
+
if (Array.isArray(actualMetadata)) {
|
|
8353
|
+
return {
|
|
8354
|
+
globalErrors: ["Metadata is wrong type: cannot be an array."]
|
|
8355
|
+
};
|
|
8356
|
+
}
|
|
8357
|
+
const recordMetadata = actualMetadata;
|
|
8358
|
+
const globalErrors = [];
|
|
8359
|
+
for (const anyKey in expectedMetadata) {
|
|
8360
|
+
if (typeof anyKey !== "string") {
|
|
8361
|
+
globalErrors.push(`Expected metadata has key '${anyKey}' that is not typeof 'string', but: '${typeof anyKey}'. This is most likely a Val bug.`);
|
|
8362
|
+
} else {
|
|
8363
|
+
if (anyKey in actualMetadata) {
|
|
8364
|
+
const key = anyKey;
|
|
8365
|
+
if (expectedMetadata[key] !== recordMetadata[key]) {
|
|
8366
|
+
erroneousMetadata[key] = `Expected metadata '${key}' to be ${JSON.stringify(expectedMetadata[key])}, but got ${JSON.stringify(recordMetadata[key])}.`;
|
|
8367
|
+
}
|
|
8368
|
+
} else {
|
|
8369
|
+
missingMetadata.push(anyKey);
|
|
8370
|
+
}
|
|
8371
|
+
}
|
|
8372
|
+
}
|
|
8373
|
+
if (globalErrors.length === 0 && missingMetadata.length === 0 && Object.keys(erroneousMetadata).length === 0) {
|
|
8374
|
+
return false;
|
|
8375
|
+
}
|
|
8376
|
+
return {
|
|
8377
|
+
missingMetadata,
|
|
8378
|
+
erroneousMetadata
|
|
8379
|
+
};
|
|
8380
|
+
}
|
|
8381
|
+
|
|
8382
|
+
/**
|
|
8383
|
+
* NOTE: this is intentionally NOT `SharedValConfig` from `@valbuild/shared`.
|
|
8384
|
+
* That schema requires `files.directory` to be exactly `/public/val`, whereas
|
|
8385
|
+
* this one accepts any path beneath `/public`. Unifying them would change which
|
|
8386
|
+
* configs are accepted, so they are kept separate on purpose.
|
|
8387
|
+
*/
|
|
8388
|
+
const ValConfigSchema = z__default["default"].object({
|
|
8389
|
+
project: z__default["default"].string().optional(),
|
|
8390
|
+
root: z__default["default"].string().optional(),
|
|
8391
|
+
files: z__default["default"].object({
|
|
8392
|
+
directory: z__default["default"].string().refine(val => (val === "/public" || val.startsWith("/public/") && !val.endsWith("/")) &&
|
|
8393
|
+
// Reject path traversal so the directory cannot escape /public
|
|
8394
|
+
!val.split("/").some(segment => segment === "." || segment === ".."), {
|
|
8395
|
+
message: "files.directory must start with '/public', must not end with '/' and must not contain '.' or '..' segments"
|
|
8396
|
+
})
|
|
8397
|
+
}).optional(),
|
|
8398
|
+
gitCommit: z__default["default"].string().optional(),
|
|
8399
|
+
gitBranch: z__default["default"].string().optional(),
|
|
8400
|
+
defaultTheme: z__default["default"].union([z__default["default"].literal("light"), z__default["default"].literal("dark")]).optional(),
|
|
8401
|
+
ai: z__default["default"].object({
|
|
8402
|
+
commitMessages: z__default["default"].object({
|
|
8403
|
+
disabled: z__default["default"].boolean().optional()
|
|
8404
|
+
}).optional()
|
|
8405
|
+
}).optional()
|
|
8406
|
+
});
|
|
8407
|
+
|
|
8408
|
+
/**
|
|
8409
|
+
* `ENOENT` (nothing at this path) and `ENOTDIR` (a path segment is a file) both
|
|
8410
|
+
* mean the config file simply is not there.
|
|
8411
|
+
*/
|
|
8412
|
+
function isFileNotFound(err) {
|
|
8413
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : undefined;
|
|
8414
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
8415
|
+
}
|
|
8416
|
+
|
|
8417
|
+
/**
|
|
8418
|
+
* Read and evaluate a `val.config.{ts,js}` file from disk.
|
|
8419
|
+
*
|
|
8420
|
+
* Returns `null` if the file does not exist, and throws if it exists but does
|
|
8421
|
+
* not export a valid `config` object.
|
|
8422
|
+
*
|
|
8423
|
+
* Used by the Val CLI and by `@valbuild/language-server`, so that an editor
|
|
8424
|
+
* resolves project config exactly the way the CLI does.
|
|
8425
|
+
*/
|
|
8426
|
+
async function evalValConfigFile(projectRoot, configFileName) {
|
|
8427
|
+
const valConfigPath = path__namespace["default"].join(projectRoot, configFileName);
|
|
8428
|
+
let code;
|
|
8429
|
+
try {
|
|
8430
|
+
code = await fs__default$1["default"].readFile(valConfigPath, "utf-8");
|
|
8431
|
+
} catch (err) {
|
|
8432
|
+
// A missing file means "this project does not use this config file name",
|
|
8433
|
+
// which callers handle by trying the next candidate. Anything else (no read
|
|
8434
|
+
// permission, an IO error, a directory where a file was expected) is a real
|
|
8435
|
+
// problem and must not be reported as "no config".
|
|
8436
|
+
if (isFileNotFound(err)) {
|
|
8437
|
+
return null;
|
|
8438
|
+
}
|
|
8439
|
+
throw Error(`Could not read Val config file at path: '${valConfigPath}': ${err instanceof Error ? err.message : String(err)}`);
|
|
8440
|
+
}
|
|
8441
|
+
if (!code.trim()) {
|
|
8442
|
+
return null;
|
|
8443
|
+
}
|
|
8444
|
+
const transpiled = ts__default["default"].transpileModule(code, {
|
|
8445
|
+
compilerOptions: {
|
|
8446
|
+
target: ts__default["default"].ScriptTarget.ES2020,
|
|
8447
|
+
module: ts__default["default"].ModuleKind.CommonJS,
|
|
8448
|
+
esModuleInterop: true
|
|
8449
|
+
},
|
|
8450
|
+
fileName: valConfigPath
|
|
8451
|
+
});
|
|
8452
|
+
const projectRootRequire = node_module.createRequire(valConfigPath);
|
|
8453
|
+
const exportsObj = {};
|
|
8454
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8455
|
+
const sandbox = {
|
|
8456
|
+
exports: exportsObj,
|
|
8457
|
+
module: {
|
|
8458
|
+
exports: exportsObj
|
|
8459
|
+
},
|
|
8460
|
+
require: projectRootRequire,
|
|
8461
|
+
// NOTE: this is a security risk, but this code is running in the users own environment at the CLI level
|
|
8462
|
+
__filename: valConfigPath,
|
|
8463
|
+
__dirname: projectRoot,
|
|
8464
|
+
console,
|
|
8465
|
+
process
|
|
8466
|
+
};
|
|
8467
|
+
sandbox.global = sandbox;
|
|
8468
|
+
const context = vm__default["default"].createContext(sandbox);
|
|
8469
|
+
const script = new vm__default["default"].Script(transpiled.outputText, {
|
|
8470
|
+
filename: valConfigPath
|
|
8471
|
+
});
|
|
8472
|
+
script.runInContext(context);
|
|
8473
|
+
const valConfig = sandbox.module.exports.config;
|
|
8474
|
+
if (!valConfig) {
|
|
8475
|
+
throw Error(`Val config file at path: '${valConfigPath}' must export a config object. Got: ${valConfig}`);
|
|
8476
|
+
}
|
|
8477
|
+
const result = ValConfigSchema.safeParse(valConfig);
|
|
8478
|
+
if (!result.success) {
|
|
8479
|
+
throw Error(`Val config file at path: '${valConfigPath}' has invalid schema: ${result.error.message}`);
|
|
8480
|
+
}
|
|
8481
|
+
return result.data;
|
|
8482
|
+
}
|
|
8483
|
+
|
|
8484
|
+
/**
|
|
8485
|
+
* Resolve the project's Val config, trying the TypeScript file first and
|
|
8486
|
+
* falling back to JavaScript. Mirrors what the CLI has always done at each of
|
|
8487
|
+
* its call sites.
|
|
8488
|
+
*/
|
|
8489
|
+
async function findAndEvalValConfigFile(projectRoot) {
|
|
8490
|
+
return (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
|
|
8491
|
+
}
|
|
8492
|
+
|
|
8493
|
+
/**
|
|
8494
|
+
* The Val login device flow, as reusable primitives.
|
|
8495
|
+
*
|
|
8496
|
+
* The CLI wraps these with terminal output, and `@valbuild/language-server`
|
|
8497
|
+
* wraps them with LSP `window/showDocument` and progress reporting. Neither the
|
|
8498
|
+
* polling nor the token handling is duplicated between them.
|
|
8499
|
+
*
|
|
8500
|
+
* Deliberately no `console` output and no `process.exit`: every failure is a
|
|
8501
|
+
* thrown {@link ValLoginError} so an embedder can decide how to surface it.
|
|
8502
|
+
*/
|
|
8503
|
+
|
|
8504
|
+
const DEFAULT_LOGIN_HOST = "https://admin.val.build";
|
|
8505
|
+
function defaultHost() {
|
|
8506
|
+
return process.env.VAL_BUILD_URL || DEFAULT_LOGIN_HOST;
|
|
8507
|
+
}
|
|
8508
|
+
class ValLoginError extends Error {
|
|
8509
|
+
constructor(code, message, details) {
|
|
8510
|
+
super(message);
|
|
8511
|
+
this.code = code;
|
|
8512
|
+
this.details = details;
|
|
8513
|
+
this.name = "ValLoginError";
|
|
8514
|
+
}
|
|
8515
|
+
}
|
|
8516
|
+
|
|
8517
|
+
/** A login attempt that is waiting for the user to confirm in a browser. */
|
|
8518
|
+
|
|
8519
|
+
/**
|
|
8520
|
+
* Begin a login attempt. The caller is responsible for getting
|
|
8521
|
+
* {@link ValLoginSession.url} in front of the user.
|
|
8522
|
+
*/
|
|
8523
|
+
async function startValLogin(options = {}) {
|
|
8524
|
+
var _response$headers$get;
|
|
8525
|
+
const host = options.host ?? defaultHost();
|
|
8526
|
+
const response = await fetch(`${host}/api/login`, {
|
|
8527
|
+
method: "POST",
|
|
8528
|
+
headers: {
|
|
8529
|
+
"Content-Type": "application/json"
|
|
8530
|
+
}
|
|
8531
|
+
});
|
|
8532
|
+
if (response.status >= 500) {
|
|
8533
|
+
const text = await response.text().catch(() => "");
|
|
8534
|
+
throw new ValLoginError("server-error", "An error occurred on the server.", text ? `Server response: ${text} (status: ${response.status})` : `Status: ${response.status}`);
|
|
8535
|
+
}
|
|
8536
|
+
if (!((_response$headers$get = response.headers.get("content-type")) !== null && _response$headers$get !== void 0 && _response$headers$get.includes("application/json"))) {
|
|
8537
|
+
const text = await response.text().catch(() => "");
|
|
8538
|
+
throw new ValLoginError("unexpected-content-type", "Unexpected failure while trying to login (content type was not JSON).", text ? `Server response: ${text} (status: ${response.status})` : `Status: ${response.status}`);
|
|
8539
|
+
}
|
|
8540
|
+
const json = await response.json();
|
|
8541
|
+
const nonce = json === null || json === void 0 ? void 0 : json.nonce;
|
|
8542
|
+
const url = json === null || json === void 0 ? void 0 : json.url;
|
|
8543
|
+
if (typeof nonce !== "string" || typeof url !== "string") {
|
|
8544
|
+
throw new ValLoginError("unexpected-response", "Unexpected response from the server.", JSON.stringify(json));
|
|
8545
|
+
}
|
|
8546
|
+
return {
|
|
8547
|
+
nonce,
|
|
8548
|
+
url
|
|
8549
|
+
};
|
|
8550
|
+
}
|
|
8551
|
+
const DEFAULT_LOGIN_MAX_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
8552
|
+
const DEFAULT_LOGIN_POLL_INTERVAL = 1000;
|
|
8553
|
+
|
|
8554
|
+
/**
|
|
8555
|
+
* Poll until the user confirms the login in their browser.
|
|
8556
|
+
*
|
|
8557
|
+
* Accepts an `AbortSignal` so an editor can cancel the flow when the user
|
|
8558
|
+
* dismisses the prompt, instead of leaving a poll loop running for 5 minutes.
|
|
8559
|
+
*/
|
|
8560
|
+
async function awaitValLoginConfirmation(nonce, options = {}) {
|
|
8561
|
+
const host = options.host ?? defaultHost();
|
|
8562
|
+
const maxDuration = options.maxDurationMs ?? DEFAULT_LOGIN_MAX_DURATION;
|
|
8563
|
+
const pollInterval = options.pollIntervalMs ?? DEFAULT_LOGIN_POLL_INTERVAL;
|
|
8564
|
+
const now = options.now ?? (() => Date.now());
|
|
8565
|
+
const start = now();
|
|
8566
|
+
while (now() - start < maxDuration) {
|
|
8567
|
+
var _options$signal, _options$signal2;
|
|
8568
|
+
if ((_options$signal = options.signal) !== null && _options$signal !== void 0 && _options$signal.aborted) {
|
|
8569
|
+
throw new ValLoginError("aborted", "Login was cancelled.");
|
|
8570
|
+
}
|
|
8571
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
8572
|
+
if ((_options$signal2 = options.signal) !== null && _options$signal2 !== void 0 && _options$signal2.aborted) {
|
|
8573
|
+
throw new ValLoginError("aborted", "Login was cancelled.");
|
|
8574
|
+
}
|
|
8575
|
+
const response = await fetch(`${host}/api/login?token=${nonce}&consume=true`, {
|
|
8576
|
+
method: "POST"
|
|
8577
|
+
});
|
|
8578
|
+
if (response.status >= 500) {
|
|
8579
|
+
throw new ValLoginError("server-error", "An error occurred on the server.", `Status: ${response.status}`);
|
|
8580
|
+
}
|
|
8581
|
+
if (response.status === 200) {
|
|
8582
|
+
var _json$profile;
|
|
8583
|
+
const json = await response.json().catch(() => null);
|
|
8584
|
+
if (!json) {
|
|
8585
|
+
// A 200 with an empty body is not a confirmation; keep polling rather
|
|
8586
|
+
// than failing the login, which is what the CLI has always done.
|
|
8587
|
+
continue;
|
|
8588
|
+
}
|
|
8589
|
+
if (typeof ((_json$profile = json.profile) === null || _json$profile === void 0 ? void 0 : _json$profile.email) === "string" && typeof json.pat === "string") {
|
|
8590
|
+
return json;
|
|
8591
|
+
}
|
|
8592
|
+
throw new ValLoginError("unexpected-response", "Unexpected response from the server.", JSON.stringify(json));
|
|
8593
|
+
}
|
|
8594
|
+
}
|
|
8595
|
+
throw new ValLoginError("timeout", "Login confirmation timed out.");
|
|
8596
|
+
}
|
|
8597
|
+
|
|
8598
|
+
/**
|
|
8599
|
+
* Write a completed login to the project's personal access token file.
|
|
8600
|
+
*
|
|
8601
|
+
* @returns the path the token was written to.
|
|
8602
|
+
*/
|
|
8603
|
+
function persistPersonalAccessToken(projectRoot, result) {
|
|
8604
|
+
const filePath = getPersonalAccessTokenPath(projectRoot);
|
|
8605
|
+
// NOTE: no restrictive mode on the directory. This is the project's shared
|
|
8606
|
+
// `.val`, which also holds the pending patches the dev server reads and
|
|
8607
|
+
// writes; making it owner-only would lock out anyone who did not run the
|
|
8608
|
+
// login. Only the token file itself is tightened.
|
|
8609
|
+
fs__default["default"].mkdirSync(path__namespace["default"].dirname(filePath), {
|
|
8610
|
+
recursive: true
|
|
8611
|
+
});
|
|
8612
|
+
// The token is a credential: keep it owner-only rather than at the mercy of
|
|
8613
|
+
// the process umask. `mode` only applies when the file is created, so an
|
|
8614
|
+
// already existing file is chmod-ed explicitly.
|
|
8615
|
+
fs__default["default"].writeFileSync(filePath, JSON.stringify(result, null, 2), {
|
|
8616
|
+
mode: 0o600
|
|
8617
|
+
});
|
|
8618
|
+
try {
|
|
8619
|
+
fs__default["default"].chmodSync(filePath, 0o600);
|
|
8620
|
+
} catch {
|
|
8621
|
+
// Not all platforms / filesystems support POSIX modes (Windows, some
|
|
8622
|
+
// network mounts). Failing to tighten them must not fail the login.
|
|
8623
|
+
}
|
|
8624
|
+
return filePath;
|
|
8625
|
+
}
|
|
8626
|
+
|
|
8627
|
+
/**
|
|
8628
|
+
* NOTE: `@valbuild/language-server` has a near-identical `modulePathMap.ts`.
|
|
8629
|
+
* Fix traversal bugs in both, or fold them together.
|
|
8630
|
+
*/
|
|
8631
|
+
|
|
8244
8632
|
function getModulePathRange(modulePath, modulePathMap,
|
|
8245
8633
|
// Which part of an object/record member to point at. For an object property
|
|
8246
8634
|
// the resolved node's own range is the *key* (property name); the *value*
|
|
@@ -8250,31 +8638,39 @@ function getModulePathRange(modulePath, modulePathMap,
|
|
|
8250
8638
|
// existing callers.
|
|
8251
8639
|
target = "key") {
|
|
8252
8640
|
var _range$children;
|
|
8253
|
-
// Handle
|
|
8254
|
-
if (
|
|
8255
|
-
return undefined;
|
|
8256
|
-
}
|
|
8257
|
-
let segments;
|
|
8258
|
-
try {
|
|
8259
|
-
// Quote-aware splitter that correctly handles keys containing dots
|
|
8260
|
-
// (e.g. file refs like `"/public/val/images/logo.png"`), unlike a naive
|
|
8261
|
-
// split on ".". Throws on malformed input (e.g. unbalanced quotes).
|
|
8262
|
-
segments = core.Internal.splitModulePath(modulePath);
|
|
8263
|
-
} catch {
|
|
8264
|
-
// Return undefined if the module path is malformed. This can happen when
|
|
8265
|
-
// there are upstream errors in schema serialization.
|
|
8641
|
+
// Handle invalid module paths gracefully
|
|
8642
|
+
if (typeof modulePath !== "string") {
|
|
8266
8643
|
return undefined;
|
|
8267
8644
|
}
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8645
|
+
let range;
|
|
8646
|
+
if (modulePath === "") {
|
|
8647
|
+
// The empty module path is the module root, stored under the "" key: the
|
|
8648
|
+
// source argument of `c.define`. This is what module-level diagnostics
|
|
8649
|
+
// (which have no module path) resolve to.
|
|
8650
|
+
range = modulePathMap[""];
|
|
8651
|
+
} else {
|
|
8652
|
+
let segments;
|
|
8653
|
+
try {
|
|
8654
|
+
// Quote-aware splitter that correctly handles keys containing dots
|
|
8655
|
+
// (e.g. file refs like `"/public/val/images/logo.png"`), unlike a naive
|
|
8656
|
+
// split on ".". Throws on malformed input (e.g. unbalanced quotes).
|
|
8657
|
+
segments = core.Internal.splitModulePath(modulePath);
|
|
8658
|
+
} catch {
|
|
8659
|
+
// Return undefined if the module path is malformed. This can happen when
|
|
8660
|
+
// there are upstream errors in schema serialization.
|
|
8661
|
+
return undefined;
|
|
8662
|
+
}
|
|
8663
|
+
if (segments.length === 0) {
|
|
8664
|
+
return undefined;
|
|
8665
|
+
}
|
|
8666
|
+
range = modulePathMap[segments[0]];
|
|
8667
|
+
for (const pathSegment of segments.slice(1)) {
|
|
8668
|
+
var _range;
|
|
8669
|
+
if (!range) {
|
|
8670
|
+
break;
|
|
8671
|
+
}
|
|
8672
|
+
range = (_range = range) === null || _range === void 0 || (_range = _range.children) === null || _range === void 0 ? void 0 : _range[pathSegment];
|
|
8276
8673
|
}
|
|
8277
|
-
range = (_range = range) === null || _range === void 0 || (_range = _range.children) === null || _range === void 0 ? void 0 : _range[pathSegment];
|
|
8278
8674
|
}
|
|
8279
8675
|
if (!range) {
|
|
8280
8676
|
return undefined;
|
|
@@ -8306,7 +8702,20 @@ function createModulePathMap(sourceFile) {
|
|
|
8306
8702
|
if (ts__default["default"].isExportAssignment(child)) {
|
|
8307
8703
|
const contentNode = child.expression && ts__default["default"].isCallExpression(child.expression) && child.expression.arguments[2];
|
|
8308
8704
|
if (contentNode) {
|
|
8309
|
-
|
|
8705
|
+
const map = traverse(contentNode, sourceFile) ?? {};
|
|
8706
|
+
// Always expose the module root under the "" key so diagnostics without
|
|
8707
|
+
// a module path still resolve to a location. `traverse` already emits ""
|
|
8708
|
+
// for primitive roots (with a tighter range), so keep that if present.
|
|
8709
|
+
// Object/record keys are never "" (see traverseObjectLiteral), so this
|
|
8710
|
+
// cannot collide with a real key.
|
|
8711
|
+
return {
|
|
8712
|
+
...map,
|
|
8713
|
+
"": map[""] ?? {
|
|
8714
|
+
children: {},
|
|
8715
|
+
start: sourceFile.getLineAndCharacterOfPosition(contentNode.getStart(sourceFile)),
|
|
8716
|
+
end: sourceFile.getLineAndCharacterOfPosition(contentNode.getEnd())
|
|
8717
|
+
}
|
|
8718
|
+
};
|
|
8310
8719
|
}
|
|
8311
8720
|
}
|
|
8312
8721
|
}
|
|
@@ -8499,12 +8908,19 @@ function readCapturedReport(snapshotDir) {
|
|
|
8499
8908
|
return JSON.parse(fs__default["default"].readFileSync(reportPath, "utf-8"));
|
|
8500
8909
|
}
|
|
8501
8910
|
|
|
8911
|
+
exports.DEFAULT_LOGIN_HOST = DEFAULT_LOGIN_HOST;
|
|
8912
|
+
exports.DEFAULT_LOGIN_MAX_DURATION = DEFAULT_LOGIN_MAX_DURATION;
|
|
8913
|
+
exports.DEFAULT_LOGIN_POLL_INTERVAL = DEFAULT_LOGIN_POLL_INTERVAL;
|
|
8502
8914
|
exports.Service = Service;
|
|
8503
8915
|
exports.ValFSHost = ValFSHost;
|
|
8916
|
+
exports.ValLoginError = ValLoginError;
|
|
8504
8917
|
exports.ValModuleLoader = ValModuleLoader;
|
|
8505
8918
|
exports.ValOpsFS = ValOpsFS;
|
|
8506
8919
|
exports.ValOpsHttp = ValOpsHttp;
|
|
8507
8920
|
exports.ValSourceFileHandler = ValSourceFileHandler;
|
|
8921
|
+
exports.analyzeValModule = analyzeValModule;
|
|
8922
|
+
exports.awaitValLoginConfirmation = awaitValLoginConfirmation;
|
|
8923
|
+
exports.checkRemoteRef = checkRemoteRef;
|
|
8508
8924
|
exports.compareWithCapturedReport = compareWithCapturedReport;
|
|
8509
8925
|
exports.createFixPatch = createFixPatch;
|
|
8510
8926
|
exports.createModulePathMap = createModulePathMap;
|
|
@@ -8512,18 +8928,31 @@ exports.createService = createService;
|
|
|
8512
8928
|
exports.createValApiRouter = createValApiRouter;
|
|
8513
8929
|
exports.createValServer = createValServer;
|
|
8514
8930
|
exports.decodeJwt = decodeJwt;
|
|
8931
|
+
exports.downloadFileFromRemote = downloadFileFromRemote;
|
|
8515
8932
|
exports.encodeJwt = encodeJwt;
|
|
8933
|
+
exports.evalValConfigFile = evalValConfigFile;
|
|
8934
|
+
exports.extractFileMetadata = extractFileMetadata;
|
|
8935
|
+
exports.extractImageMetadata = extractImageMetadata;
|
|
8936
|
+
exports.findAndEvalValConfigFile = findAndEvalValConfigFile;
|
|
8516
8937
|
exports.formatPatchSourceError = formatPatchSourceError;
|
|
8517
8938
|
exports.formatSyntaxErrorTree = formatSyntaxErrorTree;
|
|
8939
|
+
exports.getCachedRemoteFileDir = getCachedRemoteFileDir;
|
|
8940
|
+
exports.getCachedRemoteFilePath = getCachedRemoteFilePath;
|
|
8518
8941
|
exports.getCompilerOptions = getCompilerOptions;
|
|
8519
8942
|
exports.getExpire = getExpire;
|
|
8943
|
+
exports.getFileExt = getFileExt;
|
|
8520
8944
|
exports.getModulePathRange = getModulePathRange;
|
|
8521
8945
|
exports.getPersonalAccessTokenPath = getPersonalAccessTokenPath;
|
|
8522
8946
|
exports.getSettings = getSettings;
|
|
8947
|
+
exports.getValidationErrorFileRef = getValidationErrorFileRef;
|
|
8948
|
+
exports.hasRemoteFileSchema = hasRemoteFileSchema;
|
|
8523
8949
|
exports.loadValModules = loadValModules;
|
|
8524
8950
|
exports.parsePersonalAccessTokenFile = parsePersonalAccessTokenFile;
|
|
8525
8951
|
exports.patchSourceFile = patchSourceFile;
|
|
8952
|
+
exports.persistPersonalAccessToken = persistPersonalAccessToken;
|
|
8526
8953
|
exports.readCapturedReport = readCapturedReport;
|
|
8527
8954
|
exports.replaySnapshot = replaySnapshot;
|
|
8528
8955
|
exports.safeReadGit = safeReadGit;
|
|
8956
|
+
exports.startValLogin = startValLogin;
|
|
8529
8957
|
exports.uploadRemoteFile = uploadRemoteFile;
|
|
8958
|
+
exports.validateMetadata = validateMetadata;
|