@valbuild/server 0.97.7 → 0.98.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 +503 -177
- package/dist/valbuild-server.cjs.prod.js +503 -177
- package/dist/valbuild-server.esm.js +325 -21
- package/package.json +1 -1
|
@@ -6,16 +6,17 @@ import * as path from 'path';
|
|
|
6
6
|
import path__default from 'path';
|
|
7
7
|
import fs, { promises } from 'fs';
|
|
8
8
|
import vm from 'node:vm';
|
|
9
|
-
import { Module } from 'node:module';
|
|
9
|
+
import { Module, createRequire } from 'node:module';
|
|
10
10
|
import { resolveSchemaSourceFixForError, getErrorMessageFromUnknownJson, Patch, ParentRef, VAL_ENABLE_COOKIE_NAME, VAL_STATE_COOKIE, VAL_SESSION_COOKIE, Api } from '@valbuild/shared/internal';
|
|
11
11
|
import { createUIRequestHandler } from '@valbuild/ui/server';
|
|
12
12
|
import crypto$1 from 'crypto';
|
|
13
|
-
import { z } from 'zod';
|
|
13
|
+
import z$1, { z } from 'zod';
|
|
14
14
|
import sizeOf from 'image-size';
|
|
15
15
|
import { fromError } from 'zod-validation-error';
|
|
16
16
|
import { transform } from 'sucrase';
|
|
17
17
|
import http from 'http';
|
|
18
18
|
import https from 'https';
|
|
19
|
+
import fs$1 from 'fs/promises';
|
|
19
20
|
|
|
20
21
|
class ValSyntaxError {
|
|
21
22
|
constructor(message, node) {
|
|
@@ -925,6 +926,15 @@ const getCompilerOptions = (rootDir, parseConfigHost) => {
|
|
|
925
926
|
return parsedConfigFile.options;
|
|
926
927
|
};
|
|
927
928
|
|
|
929
|
+
/**
|
|
930
|
+
* The filesystem seam `loadValModules` reads through.
|
|
931
|
+
*
|
|
932
|
+
* Defaults to `ts.sys`, i.e. the real filesystem. An editor integration (see
|
|
933
|
+
* `@valbuild/language-server`) passes a host that overlays unsaved buffers, so
|
|
934
|
+
* that evaluation sees what the user is looking at rather than what was last
|
|
935
|
+
* saved.
|
|
936
|
+
*/
|
|
937
|
+
|
|
928
938
|
/**
|
|
929
939
|
* Loads the project's root `val.modules.ts` (or `.js`) using Node's `vm`
|
|
930
940
|
* module and returns its default export (a `ValModules` registry).
|
|
@@ -946,24 +956,24 @@ const getCompilerOptions = (rootDir, parseConfigHost) => {
|
|
|
946
956
|
* same trust level as running the project's build. It must never be used to
|
|
947
957
|
* evaluate untrusted or third-party modules.
|
|
948
958
|
*/
|
|
949
|
-
function loadValModules(projectRoot) {
|
|
950
|
-
const valModulesPath = findValModulesPath(projectRoot);
|
|
959
|
+
function loadValModules(projectRoot, host = ts.sys) {
|
|
960
|
+
const valModulesPath = findValModulesPath(projectRoot, host);
|
|
951
961
|
if (!valModulesPath) {
|
|
952
962
|
throw Error(`Could not find 'val.modules.ts' nor 'val.modules.js' in project root: '${projectRoot}'`);
|
|
953
963
|
}
|
|
954
|
-
const compilerOptions = getCompilerOptions(projectRoot,
|
|
964
|
+
const compilerOptions = getCompilerOptions(projectRoot, host);
|
|
955
965
|
const cache = {};
|
|
956
|
-
const loaded = loadModule(valModulesPath, cache, compilerOptions);
|
|
966
|
+
const loaded = loadModule(valModulesPath, cache, compilerOptions, host);
|
|
957
967
|
const valModules = loaded.exports.default;
|
|
958
968
|
if (!valModules) {
|
|
959
969
|
throw Error(`Val modules file at path: '${valModulesPath}' must have a default export. Got: ${valModules}`);
|
|
960
970
|
}
|
|
961
971
|
return valModules;
|
|
962
972
|
}
|
|
963
|
-
function findValModulesPath(projectRoot) {
|
|
973
|
+
function findValModulesPath(projectRoot, host) {
|
|
964
974
|
for (const fileName of ["val.modules.ts", "val.modules.js"]) {
|
|
965
975
|
const candidate = path__default.join(projectRoot, fileName);
|
|
966
|
-
if (
|
|
976
|
+
if (host.fileExists(candidate)) {
|
|
967
977
|
return candidate;
|
|
968
978
|
}
|
|
969
979
|
}
|
|
@@ -1004,12 +1014,15 @@ function makeStub(spec) {
|
|
|
1004
1014
|
const stub = new Proxy({}, handler);
|
|
1005
1015
|
return stub;
|
|
1006
1016
|
}
|
|
1007
|
-
function loadModule(absPath, cache, compilerOptions) {
|
|
1017
|
+
function loadModule(absPath, cache, compilerOptions, host) {
|
|
1008
1018
|
const cached = cache[absPath];
|
|
1009
1019
|
if (cached) {
|
|
1010
1020
|
return cached;
|
|
1011
1021
|
}
|
|
1012
|
-
const code =
|
|
1022
|
+
const code = host.readFile(absPath);
|
|
1023
|
+
if (code === undefined) {
|
|
1024
|
+
throw Error(`Could not read Val module file: '${absPath}'`);
|
|
1025
|
+
}
|
|
1013
1026
|
const transpiled = ts.transpileModule(code, {
|
|
1014
1027
|
compilerOptions: {
|
|
1015
1028
|
target: ts.ScriptTarget.ES2020,
|
|
@@ -1032,17 +1045,17 @@ function loadModule(absPath, cache, compilerOptions) {
|
|
|
1032
1045
|
return makeStub(spec);
|
|
1033
1046
|
}
|
|
1034
1047
|
if (spec.startsWith(".") || path__default.isAbsolute(spec)) {
|
|
1035
|
-
const resolved = resolveRelative(dirName, spec);
|
|
1048
|
+
const resolved = resolveRelative(dirName, spec, host);
|
|
1036
1049
|
if (!resolved) {
|
|
1037
1050
|
throw Error(`Could not resolve module '${spec}' from '${absPath}'`);
|
|
1038
1051
|
}
|
|
1039
|
-
return loadModule(resolved, cache, compilerOptions).exports;
|
|
1052
|
+
return loadModule(resolved, cache, compilerOptions, host).exports;
|
|
1040
1053
|
}
|
|
1041
1054
|
// Non-relative specifier: it might be a tsconfig path alias (e.g. "_/val.config")
|
|
1042
1055
|
// pointing at a local source file, or an actual node_modules package.
|
|
1043
|
-
const tsResolved = (_ts$resolveModuleName = ts.resolveModuleName(spec, absPath, compilerOptions,
|
|
1056
|
+
const tsResolved = (_ts$resolveModuleName = ts.resolveModuleName(spec, absPath, compilerOptions, host).resolvedModule) === null || _ts$resolveModuleName === void 0 ? void 0 : _ts$resolveModuleName.resolvedFileName;
|
|
1044
1057
|
if (tsResolved && !tsResolved.includes("/node_modules/") && !tsResolved.endsWith(".d.ts")) {
|
|
1045
|
-
return loadModule(tsResolved, cache, compilerOptions).exports;
|
|
1058
|
+
return loadModule(tsResolved, cache, compilerOptions, host).exports;
|
|
1046
1059
|
}
|
|
1047
1060
|
// Real node_modules package – use the real require so user modules share
|
|
1048
1061
|
// the same @valbuild/core instance as extractValModules.
|
|
@@ -1068,24 +1081,25 @@ function loadModule(absPath, cache, compilerOptions) {
|
|
|
1068
1081
|
script.runInContext(context);
|
|
1069
1082
|
return moduleObj;
|
|
1070
1083
|
}
|
|
1071
|
-
function resolveRelative(dirName, spec) {
|
|
1084
|
+
function resolveRelative(dirName, spec, host) {
|
|
1085
|
+
var _host$directoryExists;
|
|
1072
1086
|
const base = path__default.resolve(dirName, spec);
|
|
1073
1087
|
// Exact file (with extension)
|
|
1074
|
-
if (
|
|
1088
|
+
if (host.fileExists(base)) {
|
|
1075
1089
|
return base;
|
|
1076
1090
|
}
|
|
1077
1091
|
// Probe extensions (handles `./x.val` -> `./x.val.ts`)
|
|
1078
1092
|
for (const ext of RESOLVE_EXTENSIONS) {
|
|
1079
1093
|
const candidate = base + ext;
|
|
1080
|
-
if (
|
|
1094
|
+
if (host.fileExists(candidate)) {
|
|
1081
1095
|
return candidate;
|
|
1082
1096
|
}
|
|
1083
1097
|
}
|
|
1084
1098
|
// Directory index
|
|
1085
|
-
if (
|
|
1099
|
+
if (((_host$directoryExists = host.directoryExists) === null || _host$directoryExists === void 0 ? void 0 : _host$directoryExists.call(host, base)) ?? fs.existsSync(base)) {
|
|
1086
1100
|
for (const ext of RESOLVE_EXTENSIONS) {
|
|
1087
1101
|
const candidate = path__default.join(base, "index" + ext);
|
|
1088
|
-
if (
|
|
1102
|
+
if (host.fileExists(candidate)) {
|
|
1089
1103
|
return candidate;
|
|
1090
1104
|
}
|
|
1091
1105
|
}
|
|
@@ -1112,7 +1126,9 @@ async function createService(projectRoot, host = {
|
|
|
1112
1126
|
}) {
|
|
1113
1127
|
const compilerOptions = getCompilerOptions(projectRoot, host);
|
|
1114
1128
|
const sourceFileHandler = new ValSourceFileHandler(projectRoot, compilerOptions, host);
|
|
1115
|
-
|
|
1129
|
+
// Read val.modules (and everything it imports) through the same host, so an
|
|
1130
|
+
// embedder that overlays unsaved editor buffers evaluates what the user sees.
|
|
1131
|
+
const valModules = loadValModules(projectRoot, host);
|
|
1116
1132
|
const extracted = await extractValModules(valModules);
|
|
1117
1133
|
return new Service(projectRoot, sourceFileHandler, extracted);
|
|
1118
1134
|
}
|
|
@@ -8209,6 +8225,294 @@ async function getFileMetadata(projectRoot, validationError) {
|
|
|
8209
8225
|
return extractFileMetadata(fileRef);
|
|
8210
8226
|
}
|
|
8211
8227
|
|
|
8228
|
+
function validateMetadata(actualMetadata, expectedMetadata) {
|
|
8229
|
+
const missingMetadata = [];
|
|
8230
|
+
const erroneousMetadata = {};
|
|
8231
|
+
if (typeof actualMetadata !== "object" || actualMetadata === null) {
|
|
8232
|
+
return {
|
|
8233
|
+
globalErrors: ["Metadata is wrong type: must be an object."]
|
|
8234
|
+
};
|
|
8235
|
+
}
|
|
8236
|
+
if (Array.isArray(actualMetadata)) {
|
|
8237
|
+
return {
|
|
8238
|
+
globalErrors: ["Metadata is wrong type: cannot be an array."]
|
|
8239
|
+
};
|
|
8240
|
+
}
|
|
8241
|
+
const recordMetadata = actualMetadata;
|
|
8242
|
+
const globalErrors = [];
|
|
8243
|
+
for (const anyKey in expectedMetadata) {
|
|
8244
|
+
if (typeof anyKey !== "string") {
|
|
8245
|
+
globalErrors.push(`Expected metadata has key '${anyKey}' that is not typeof 'string', but: '${typeof anyKey}'. This is most likely a Val bug.`);
|
|
8246
|
+
} else {
|
|
8247
|
+
if (anyKey in actualMetadata) {
|
|
8248
|
+
const key = anyKey;
|
|
8249
|
+
if (expectedMetadata[key] !== recordMetadata[key]) {
|
|
8250
|
+
erroneousMetadata[key] = `Expected metadata '${key}' to be ${JSON.stringify(expectedMetadata[key])}, but got ${JSON.stringify(recordMetadata[key])}.`;
|
|
8251
|
+
}
|
|
8252
|
+
} else {
|
|
8253
|
+
missingMetadata.push(anyKey);
|
|
8254
|
+
}
|
|
8255
|
+
}
|
|
8256
|
+
}
|
|
8257
|
+
if (globalErrors.length === 0 && missingMetadata.length === 0 && Object.keys(erroneousMetadata).length === 0) {
|
|
8258
|
+
return false;
|
|
8259
|
+
}
|
|
8260
|
+
return {
|
|
8261
|
+
missingMetadata,
|
|
8262
|
+
erroneousMetadata
|
|
8263
|
+
};
|
|
8264
|
+
}
|
|
8265
|
+
|
|
8266
|
+
/**
|
|
8267
|
+
* NOTE: this is intentionally NOT `SharedValConfig` from `@valbuild/shared`.
|
|
8268
|
+
* That schema requires `files.directory` to be exactly `/public/val`, whereas
|
|
8269
|
+
* this one accepts any path beneath `/public`. Unifying them would change which
|
|
8270
|
+
* configs are accepted, so they are kept separate on purpose.
|
|
8271
|
+
*/
|
|
8272
|
+
const ValConfigSchema = z$1.object({
|
|
8273
|
+
project: z$1.string().optional(),
|
|
8274
|
+
root: z$1.string().optional(),
|
|
8275
|
+
files: z$1.object({
|
|
8276
|
+
directory: z$1.string().refine(val => (val === "/public" || val.startsWith("/public/") && !val.endsWith("/")) &&
|
|
8277
|
+
// Reject path traversal so the directory cannot escape /public
|
|
8278
|
+
!val.split("/").some(segment => segment === "." || segment === ".."), {
|
|
8279
|
+
message: "files.directory must start with '/public', must not end with '/' and must not contain '.' or '..' segments"
|
|
8280
|
+
})
|
|
8281
|
+
}).optional(),
|
|
8282
|
+
gitCommit: z$1.string().optional(),
|
|
8283
|
+
gitBranch: z$1.string().optional(),
|
|
8284
|
+
defaultTheme: z$1.union([z$1.literal("light"), z$1.literal("dark")]).optional(),
|
|
8285
|
+
ai: z$1.object({
|
|
8286
|
+
commitMessages: z$1.object({
|
|
8287
|
+
disabled: z$1.boolean().optional()
|
|
8288
|
+
}).optional()
|
|
8289
|
+
}).optional()
|
|
8290
|
+
});
|
|
8291
|
+
|
|
8292
|
+
/**
|
|
8293
|
+
* `ENOENT` (nothing at this path) and `ENOTDIR` (a path segment is a file) both
|
|
8294
|
+
* mean the config file simply is not there.
|
|
8295
|
+
*/
|
|
8296
|
+
function isFileNotFound(err) {
|
|
8297
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : undefined;
|
|
8298
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
8299
|
+
}
|
|
8300
|
+
|
|
8301
|
+
/**
|
|
8302
|
+
* Read and evaluate a `val.config.{ts,js}` file from disk.
|
|
8303
|
+
*
|
|
8304
|
+
* Returns `null` if the file does not exist, and throws if it exists but does
|
|
8305
|
+
* not export a valid `config` object.
|
|
8306
|
+
*
|
|
8307
|
+
* Used by the Val CLI and by `@valbuild/language-server`, so that an editor
|
|
8308
|
+
* resolves project config exactly the way the CLI does.
|
|
8309
|
+
*/
|
|
8310
|
+
async function evalValConfigFile(projectRoot, configFileName) {
|
|
8311
|
+
const valConfigPath = path__default.join(projectRoot, configFileName);
|
|
8312
|
+
let code;
|
|
8313
|
+
try {
|
|
8314
|
+
code = await fs$1.readFile(valConfigPath, "utf-8");
|
|
8315
|
+
} catch (err) {
|
|
8316
|
+
// A missing file means "this project does not use this config file name",
|
|
8317
|
+
// which callers handle by trying the next candidate. Anything else (no read
|
|
8318
|
+
// permission, an IO error, a directory where a file was expected) is a real
|
|
8319
|
+
// problem and must not be reported as "no config".
|
|
8320
|
+
if (isFileNotFound(err)) {
|
|
8321
|
+
return null;
|
|
8322
|
+
}
|
|
8323
|
+
throw Error(`Could not read Val config file at path: '${valConfigPath}': ${err instanceof Error ? err.message : String(err)}`);
|
|
8324
|
+
}
|
|
8325
|
+
if (!code.trim()) {
|
|
8326
|
+
return null;
|
|
8327
|
+
}
|
|
8328
|
+
const transpiled = ts.transpileModule(code, {
|
|
8329
|
+
compilerOptions: {
|
|
8330
|
+
target: ts.ScriptTarget.ES2020,
|
|
8331
|
+
module: ts.ModuleKind.CommonJS,
|
|
8332
|
+
esModuleInterop: true
|
|
8333
|
+
},
|
|
8334
|
+
fileName: valConfigPath
|
|
8335
|
+
});
|
|
8336
|
+
const projectRootRequire = createRequire(valConfigPath);
|
|
8337
|
+
const exportsObj = {};
|
|
8338
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8339
|
+
const sandbox = {
|
|
8340
|
+
exports: exportsObj,
|
|
8341
|
+
module: {
|
|
8342
|
+
exports: exportsObj
|
|
8343
|
+
},
|
|
8344
|
+
require: projectRootRequire,
|
|
8345
|
+
// NOTE: this is a security risk, but this code is running in the users own environment at the CLI level
|
|
8346
|
+
__filename: valConfigPath,
|
|
8347
|
+
__dirname: projectRoot,
|
|
8348
|
+
console,
|
|
8349
|
+
process
|
|
8350
|
+
};
|
|
8351
|
+
sandbox.global = sandbox;
|
|
8352
|
+
const context = vm.createContext(sandbox);
|
|
8353
|
+
const script = new vm.Script(transpiled.outputText, {
|
|
8354
|
+
filename: valConfigPath
|
|
8355
|
+
});
|
|
8356
|
+
script.runInContext(context);
|
|
8357
|
+
const valConfig = sandbox.module.exports.config;
|
|
8358
|
+
if (!valConfig) {
|
|
8359
|
+
throw Error(`Val config file at path: '${valConfigPath}' must export a config object. Got: ${valConfig}`);
|
|
8360
|
+
}
|
|
8361
|
+
const result = ValConfigSchema.safeParse(valConfig);
|
|
8362
|
+
if (!result.success) {
|
|
8363
|
+
throw Error(`Val config file at path: '${valConfigPath}' has invalid schema: ${result.error.message}`);
|
|
8364
|
+
}
|
|
8365
|
+
return result.data;
|
|
8366
|
+
}
|
|
8367
|
+
|
|
8368
|
+
/**
|
|
8369
|
+
* Resolve the project's Val config, trying the TypeScript file first and
|
|
8370
|
+
* falling back to JavaScript. Mirrors what the CLI has always done at each of
|
|
8371
|
+
* its call sites.
|
|
8372
|
+
*/
|
|
8373
|
+
async function findAndEvalValConfigFile(projectRoot) {
|
|
8374
|
+
return (await evalValConfigFile(projectRoot, "val.config.ts")) || (await evalValConfigFile(projectRoot, "val.config.js"));
|
|
8375
|
+
}
|
|
8376
|
+
|
|
8377
|
+
/**
|
|
8378
|
+
* The Val login device flow, as reusable primitives.
|
|
8379
|
+
*
|
|
8380
|
+
* The CLI wraps these with terminal output, and `@valbuild/language-server`
|
|
8381
|
+
* wraps them with LSP `window/showDocument` and progress reporting. Neither the
|
|
8382
|
+
* polling nor the token handling is duplicated between them.
|
|
8383
|
+
*
|
|
8384
|
+
* Deliberately no `console` output and no `process.exit`: every failure is a
|
|
8385
|
+
* thrown {@link ValLoginError} so an embedder can decide how to surface it.
|
|
8386
|
+
*/
|
|
8387
|
+
|
|
8388
|
+
const DEFAULT_LOGIN_HOST = "https://admin.val.build";
|
|
8389
|
+
function defaultHost() {
|
|
8390
|
+
return process.env.VAL_BUILD_URL || DEFAULT_LOGIN_HOST;
|
|
8391
|
+
}
|
|
8392
|
+
class ValLoginError extends Error {
|
|
8393
|
+
constructor(code, message, details) {
|
|
8394
|
+
super(message);
|
|
8395
|
+
this.code = code;
|
|
8396
|
+
this.details = details;
|
|
8397
|
+
this.name = "ValLoginError";
|
|
8398
|
+
}
|
|
8399
|
+
}
|
|
8400
|
+
|
|
8401
|
+
/** A login attempt that is waiting for the user to confirm in a browser. */
|
|
8402
|
+
|
|
8403
|
+
/**
|
|
8404
|
+
* Begin a login attempt. The caller is responsible for getting
|
|
8405
|
+
* {@link ValLoginSession.url} in front of the user.
|
|
8406
|
+
*/
|
|
8407
|
+
async function startValLogin(options = {}) {
|
|
8408
|
+
var _response$headers$get;
|
|
8409
|
+
const host = options.host ?? defaultHost();
|
|
8410
|
+
const response = await fetch(`${host}/api/login`, {
|
|
8411
|
+
method: "POST",
|
|
8412
|
+
headers: {
|
|
8413
|
+
"Content-Type": "application/json"
|
|
8414
|
+
}
|
|
8415
|
+
});
|
|
8416
|
+
if (response.status >= 500) {
|
|
8417
|
+
const text = await response.text().catch(() => "");
|
|
8418
|
+
throw new ValLoginError("server-error", "An error occurred on the server.", text ? `Server response: ${text} (status: ${response.status})` : `Status: ${response.status}`);
|
|
8419
|
+
}
|
|
8420
|
+
if (!((_response$headers$get = response.headers.get("content-type")) !== null && _response$headers$get !== void 0 && _response$headers$get.includes("application/json"))) {
|
|
8421
|
+
const text = await response.text().catch(() => "");
|
|
8422
|
+
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}`);
|
|
8423
|
+
}
|
|
8424
|
+
const json = await response.json();
|
|
8425
|
+
const nonce = json === null || json === void 0 ? void 0 : json.nonce;
|
|
8426
|
+
const url = json === null || json === void 0 ? void 0 : json.url;
|
|
8427
|
+
if (typeof nonce !== "string" || typeof url !== "string") {
|
|
8428
|
+
throw new ValLoginError("unexpected-response", "Unexpected response from the server.", JSON.stringify(json));
|
|
8429
|
+
}
|
|
8430
|
+
return {
|
|
8431
|
+
nonce,
|
|
8432
|
+
url
|
|
8433
|
+
};
|
|
8434
|
+
}
|
|
8435
|
+
const DEFAULT_LOGIN_MAX_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
8436
|
+
const DEFAULT_LOGIN_POLL_INTERVAL = 1000;
|
|
8437
|
+
|
|
8438
|
+
/**
|
|
8439
|
+
* Poll until the user confirms the login in their browser.
|
|
8440
|
+
*
|
|
8441
|
+
* Accepts an `AbortSignal` so an editor can cancel the flow when the user
|
|
8442
|
+
* dismisses the prompt, instead of leaving a poll loop running for 5 minutes.
|
|
8443
|
+
*/
|
|
8444
|
+
async function awaitValLoginConfirmation(nonce, options = {}) {
|
|
8445
|
+
const host = options.host ?? defaultHost();
|
|
8446
|
+
const maxDuration = options.maxDurationMs ?? DEFAULT_LOGIN_MAX_DURATION;
|
|
8447
|
+
const pollInterval = options.pollIntervalMs ?? DEFAULT_LOGIN_POLL_INTERVAL;
|
|
8448
|
+
const now = options.now ?? (() => Date.now());
|
|
8449
|
+
const start = now();
|
|
8450
|
+
while (now() - start < maxDuration) {
|
|
8451
|
+
var _options$signal, _options$signal2;
|
|
8452
|
+
if ((_options$signal = options.signal) !== null && _options$signal !== void 0 && _options$signal.aborted) {
|
|
8453
|
+
throw new ValLoginError("aborted", "Login was cancelled.");
|
|
8454
|
+
}
|
|
8455
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
8456
|
+
if ((_options$signal2 = options.signal) !== null && _options$signal2 !== void 0 && _options$signal2.aborted) {
|
|
8457
|
+
throw new ValLoginError("aborted", "Login was cancelled.");
|
|
8458
|
+
}
|
|
8459
|
+
const response = await fetch(`${host}/api/login?token=${nonce}&consume=true`, {
|
|
8460
|
+
method: "POST"
|
|
8461
|
+
});
|
|
8462
|
+
if (response.status >= 500) {
|
|
8463
|
+
throw new ValLoginError("server-error", "An error occurred on the server.", `Status: ${response.status}`);
|
|
8464
|
+
}
|
|
8465
|
+
if (response.status === 200) {
|
|
8466
|
+
var _json$profile;
|
|
8467
|
+
const json = await response.json().catch(() => null);
|
|
8468
|
+
if (!json) {
|
|
8469
|
+
// A 200 with an empty body is not a confirmation; keep polling rather
|
|
8470
|
+
// than failing the login, which is what the CLI has always done.
|
|
8471
|
+
continue;
|
|
8472
|
+
}
|
|
8473
|
+
if (typeof ((_json$profile = json.profile) === null || _json$profile === void 0 ? void 0 : _json$profile.email) === "string" && typeof json.pat === "string") {
|
|
8474
|
+
return json;
|
|
8475
|
+
}
|
|
8476
|
+
throw new ValLoginError("unexpected-response", "Unexpected response from the server.", JSON.stringify(json));
|
|
8477
|
+
}
|
|
8478
|
+
}
|
|
8479
|
+
throw new ValLoginError("timeout", "Login confirmation timed out.");
|
|
8480
|
+
}
|
|
8481
|
+
|
|
8482
|
+
/**
|
|
8483
|
+
* Write a completed login to the project's personal access token file.
|
|
8484
|
+
*
|
|
8485
|
+
* @returns the path the token was written to.
|
|
8486
|
+
*/
|
|
8487
|
+
function persistPersonalAccessToken(projectRoot, result) {
|
|
8488
|
+
const filePath = getPersonalAccessTokenPath(projectRoot);
|
|
8489
|
+
// NOTE: no restrictive mode on the directory. This is the project's shared
|
|
8490
|
+
// `.val`, which also holds the pending patches the dev server reads and
|
|
8491
|
+
// writes; making it owner-only would lock out anyone who did not run the
|
|
8492
|
+
// login. Only the token file itself is tightened.
|
|
8493
|
+
fs.mkdirSync(path__default.dirname(filePath), {
|
|
8494
|
+
recursive: true
|
|
8495
|
+
});
|
|
8496
|
+
// The token is a credential: keep it owner-only rather than at the mercy of
|
|
8497
|
+
// the process umask. `mode` only applies when the file is created, so an
|
|
8498
|
+
// already existing file is chmod-ed explicitly.
|
|
8499
|
+
fs.writeFileSync(filePath, JSON.stringify(result, null, 2), {
|
|
8500
|
+
mode: 0o600
|
|
8501
|
+
});
|
|
8502
|
+
try {
|
|
8503
|
+
fs.chmodSync(filePath, 0o600);
|
|
8504
|
+
} catch {
|
|
8505
|
+
// Not all platforms / filesystems support POSIX modes (Windows, some
|
|
8506
|
+
// network mounts). Failing to tighten them must not fail the login.
|
|
8507
|
+
}
|
|
8508
|
+
return filePath;
|
|
8509
|
+
}
|
|
8510
|
+
|
|
8511
|
+
/**
|
|
8512
|
+
* NOTE: `@valbuild/language-server` has a near-identical `modulePathMap.ts`.
|
|
8513
|
+
* Fix traversal bugs in both, or fold them together.
|
|
8514
|
+
*/
|
|
8515
|
+
|
|
8212
8516
|
function getModulePathRange(modulePath, modulePathMap,
|
|
8213
8517
|
// Which part of an object/record member to point at. For an object property
|
|
8214
8518
|
// the resolved node's own range is the *key* (property name); the *value*
|
|
@@ -8467,4 +8771,4 @@ function readCapturedReport(snapshotDir) {
|
|
|
8467
8771
|
return JSON.parse(fs.readFileSync(reportPath, "utf-8"));
|
|
8468
8772
|
}
|
|
8469
8773
|
|
|
8470
|
-
export { Service, ValFSHost, ValModuleLoader, ValOpsFS, ValOpsHttp, ValSourceFileHandler, compareWithCapturedReport, createFixPatch, createModulePathMap, createService, createValApiRouter, createValServer, decodeJwt, encodeJwt, formatPatchSourceError, formatSyntaxErrorTree, getCompilerOptions, getExpire, getModulePathRange, getPersonalAccessTokenPath, getSettings, loadValModules, parsePersonalAccessTokenFile, patchSourceFile, readCapturedReport, replaySnapshot, safeReadGit, uploadRemoteFile };
|
|
8774
|
+
export { DEFAULT_LOGIN_HOST, DEFAULT_LOGIN_MAX_DURATION, DEFAULT_LOGIN_POLL_INTERVAL, Service, ValFSHost, ValLoginError, ValModuleLoader, ValOpsFS, ValOpsHttp, ValSourceFileHandler, analyzeValModule, awaitValLoginConfirmation, checkRemoteRef, compareWithCapturedReport, createFixPatch, createModulePathMap, createService, createValApiRouter, createValServer, decodeJwt, downloadFileFromRemote, encodeJwt, evalValConfigFile, extractFileMetadata, extractImageMetadata, findAndEvalValConfigFile, formatPatchSourceError, formatSyntaxErrorTree, getCachedRemoteFileDir, getCachedRemoteFilePath, getCompilerOptions, getExpire, getFileExt, getModulePathRange, getPersonalAccessTokenPath, getSettings, getValidationErrorFileRef, hasRemoteFileSchema, loadValModules, parsePersonalAccessTokenFile, patchSourceFile, persistPersonalAccessToken, readCapturedReport, replaySnapshot, safeReadGit, startValLogin, uploadRemoteFile, validateMetadata };
|