@verax-ai/body 0.1.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.
Files changed (63) hide show
  1. package/dist/approve-cli.d.ts +12 -0
  2. package/dist/approve-cli.js +93 -0
  3. package/dist/atomic-write.d.ts +1 -0
  4. package/dist/atomic-write.js +65 -0
  5. package/dist/auth.d.ts +10 -0
  6. package/dist/auth.js +59 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +112 -0
  9. package/dist/config.d.ts +27 -0
  10. package/dist/config.js +71 -0
  11. package/dist/desktop.d.ts +29 -0
  12. package/dist/desktop.js +341 -0
  13. package/dist/doctor.d.ts +8 -0
  14. package/dist/doctor.js +342 -0
  15. package/dist/halt.d.ts +1 -0
  16. package/dist/halt.js +12 -0
  17. package/dist/health-extras.d.ts +11 -0
  18. package/dist/health-extras.js +47 -0
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.js +4 -0
  21. package/dist/inputs-read.d.ts +12 -0
  22. package/dist/inputs-read.js +64 -0
  23. package/dist/inventory-file.d.ts +18 -0
  24. package/dist/inventory-file.js +41 -0
  25. package/dist/keys.d.ts +12 -0
  26. package/dist/keys.js +55 -0
  27. package/dist/main.d.ts +1 -0
  28. package/dist/main.js +34 -0
  29. package/dist/metrics.d.ts +2 -0
  30. package/dist/metrics.js +23 -0
  31. package/dist/no-bypass-scan.d.ts +10 -0
  32. package/dist/no-bypass-scan.js +95 -0
  33. package/dist/operator-cli.d.ts +1 -0
  34. package/dist/operator-cli.js +25 -0
  35. package/dist/operator-credentials.d.ts +23 -0
  36. package/dist/operator-credentials.js +56 -0
  37. package/dist/operator-pairing.d.ts +25 -0
  38. package/dist/operator-pairing.js +67 -0
  39. package/dist/policy-store.d.ts +3 -0
  40. package/dist/policy-store.js +44 -0
  41. package/dist/reconcile-cli.d.ts +24 -0
  42. package/dist/reconcile-cli.js +258 -0
  43. package/dist/revoke.d.ts +1 -0
  44. package/dist/revoke.js +16 -0
  45. package/dist/rp-config.d.ts +15 -0
  46. package/dist/rp-config.js +30 -0
  47. package/dist/server.d.ts +5 -0
  48. package/dist/server.js +484 -0
  49. package/dist/tools/audit.d.ts +2 -0
  50. package/dist/tools/audit.js +48 -0
  51. package/dist/tools/memory.d.ts +10 -0
  52. package/dist/tools/memory.js +137 -0
  53. package/dist/tools/message.d.ts +4 -0
  54. package/dist/tools/message.js +44 -0
  55. package/dist/tools/spend.d.ts +3 -0
  56. package/dist/tools/spend.js +19 -0
  57. package/dist/unlock.d.ts +9 -0
  58. package/dist/unlock.js +109 -0
  59. package/dist/wiring.d.ts +23 -0
  60. package/dist/wiring.js +101 -0
  61. package/dist/witness.d.ts +33 -0
  62. package/dist/witness.js +321 -0
  63. package/package.json +40 -0
@@ -0,0 +1,341 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { createConnection } from "node:net";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ const here = dirname(fileURLToPath(import.meta.url));
7
+ const repoRoot = join(here, "..", "..", "..");
8
+ /** Newest modification time under a file or directory, ignoring build output. */
9
+ function newestUnder(path, since) {
10
+ let entry;
11
+ try {
12
+ entry = statSync(path);
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ if (!entry.isDirectory())
18
+ return entry.mtimeMs > since;
19
+ let names;
20
+ try {
21
+ names = readdirSync(path, { withFileTypes: true });
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ for (const name of names) {
27
+ if (name.name === "node_modules" || name.name === "dist" || name.name.startsWith("."))
28
+ continue;
29
+ if (newestUnder(join(path, name.name), since))
30
+ return true;
31
+ }
32
+ return false;
33
+ }
34
+ /**
35
+ * True when the panel has no build, or when a watched source is newer than the
36
+ * one it has. The launcher used to skip the build whenever `dist/index.html`
37
+ * existed, so a session served whatever had been built once: editing the panel
38
+ * and reopening it showed the previous app, with no sign that it was stale.
39
+ */
40
+ export function panelBuildNeeded(distIndex, sourceRoots) {
41
+ let builtAt;
42
+ try {
43
+ builtAt = statSync(distIndex).mtimeMs;
44
+ }
45
+ catch {
46
+ return true;
47
+ }
48
+ return sourceRoots.some((root) => newestUnder(root, builtAt));
49
+ }
50
+ export function parseDesktopArgs(argv) {
51
+ const rest = argv.slice(1);
52
+ let stateDir = "";
53
+ let panelPort = 5173;
54
+ let issuerPort = Number(process.env.VERAX_DEV_ISSUER_PORT ?? "8790");
55
+ let bodyPort = 8787;
56
+ let browser;
57
+ let inventoryFile;
58
+ for (let i = 0; i < rest.length; i += 1) {
59
+ const a = rest[i];
60
+ if (a === "--state") {
61
+ stateDir = rest[++i] ?? "";
62
+ continue;
63
+ }
64
+ if (a === "--port") {
65
+ panelPort = Number(rest[++i]);
66
+ continue;
67
+ }
68
+ if (a === "--issuer-port") {
69
+ issuerPort = Number(rest[++i]);
70
+ continue;
71
+ }
72
+ if (a === "--body-port") {
73
+ bodyPort = Number(rest[++i]);
74
+ continue;
75
+ }
76
+ if (a === "--browser") {
77
+ browser = rest[++i];
78
+ continue;
79
+ }
80
+ if (a === "--inventory") {
81
+ inventoryFile = rest[++i];
82
+ continue;
83
+ }
84
+ if (a.startsWith("-"))
85
+ return { error: `flag-unknown:${a}` };
86
+ }
87
+ if (!stateDir)
88
+ return { error: "usage" };
89
+ if (![panelPort, issuerPort, bodyPort].every((n) => Number.isInteger(n) && n > 0 && n < 65536)) {
90
+ return { error: "port-invalid" };
91
+ }
92
+ return { stateDir, panelPort, issuerPort, bodyPort, browser, inventoryFile };
93
+ }
94
+ export function portOpen(port, host = "127.0.0.1") {
95
+ return new Promise((resolve) => {
96
+ const sock = createConnection({ port, host });
97
+ sock.once("connect", () => {
98
+ sock.end();
99
+ resolve(true);
100
+ });
101
+ sock.once("error", () => resolve(false));
102
+ });
103
+ }
104
+ async function waitPort(port, ms) {
105
+ const start = Date.now();
106
+ while (Date.now() - start < ms) {
107
+ if (await portOpen(port))
108
+ return true;
109
+ await new Promise((r) => setTimeout(r, 100));
110
+ }
111
+ return false;
112
+ }
113
+ export function killTree(pid) {
114
+ if (pid == null)
115
+ return;
116
+ if (process.platform === "win32") {
117
+ spawnSync("taskkill", ["/T", "/PID", String(pid), "/F"], { windowsHide: true, stdio: "ignore" });
118
+ return;
119
+ }
120
+ try {
121
+ process.kill(-pid, "SIGTERM");
122
+ }
123
+ catch {
124
+ try {
125
+ process.kill(pid, "SIGTERM");
126
+ }
127
+ catch {
128
+ // gone
129
+ }
130
+ }
131
+ }
132
+ /** The panel's redirect is its own origin, so the issuer has to be told which port
133
+ * this run put it on: the allow-list default only holds 5173 and 4173. */
134
+ export function issuerEnv(base, opts, audience, issuerUrl) {
135
+ return {
136
+ ...base,
137
+ NODE_ENV: "development",
138
+ VERAX_STATE_DIR: opts.stateDir,
139
+ VERAX_DEV_ISSUER_PORT: String(opts.issuerPort),
140
+ VERAX_AUDIENCE: audience,
141
+ VERAX_ISSUER: issuerUrl,
142
+ VERAX_DEV_REDIRECT_URIS: `http://127.0.0.1:${opts.panelPort}/`,
143
+ };
144
+ }
145
+ function cleanEnv() {
146
+ const env = { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" };
147
+ delete env.VERAX_DEV_TOKEN;
148
+ return env;
149
+ }
150
+ /**
151
+ * `windowsHide` is right for the servers: it keeps a console window off the
152
+ * screen. It is wrong for the browser. On Windows the flag becomes SW_HIDE in
153
+ * the child's STARTUPINFO, and Chrome and Edge honour it for their first
154
+ * window: the process starts, renderers run, the panel answers on its port,
155
+ * and the app window is created hidden. That is the desktop app opening to
156
+ * nothing. The browser must be spawned with the flag off.
157
+ */
158
+ function spawnLogged(cmd, args, env, cwd, hideWindow = true) {
159
+ return spawn(cmd, args, {
160
+ cwd,
161
+ env,
162
+ stdio: ["ignore", "pipe", "pipe"],
163
+ windowsHide: hideWindow,
164
+ detached: process.platform !== "win32",
165
+ });
166
+ }
167
+ function defaultBrowser() {
168
+ const edge = [
169
+ "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
170
+ "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
171
+ ];
172
+ const chrome = [
173
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
174
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
175
+ "/usr/bin/google-chrome",
176
+ ];
177
+ for (const p of edge)
178
+ if (existsSync(p))
179
+ return p;
180
+ for (const p of chrome)
181
+ if (existsSync(p))
182
+ return p;
183
+ return null;
184
+ }
185
+ function collectOutput(child, sink) {
186
+ const feed = (c) => {
187
+ sink.text += String(c);
188
+ };
189
+ child.stdout?.on("data", feed);
190
+ child.stderr?.on("data", feed);
191
+ }
192
+ export async function runDesktop(opts, writeErr = (s) => process.stderr.write(s)) {
193
+ mkdirSync(opts.stateDir, { recursive: true });
194
+ const tokenPath = join(opts.stateDir, "dev-token");
195
+ const issuerScript = join(repoRoot, "scripts", "dev-issuer.mjs");
196
+ const mainTs = join(here, "main.ts");
197
+ const viteJs = join(repoRoot, "node_modules", "vite", "bin", "vite.js");
198
+ const panelDir = join(repoRoot, "apps", "panel");
199
+ const policy = join(repoRoot, "packages", "proxy", "policy", "default.json");
200
+ const audience = `http://127.0.0.1:${opts.bodyPort}`;
201
+ const issuerUrl = `http://127.0.0.1:${opts.issuerPort}`;
202
+ const kids = [];
203
+ const log = { text: "" };
204
+ const stopAll = () => {
205
+ for (const c of kids)
206
+ killTree(c.pid);
207
+ };
208
+ try {
209
+ const issuer = spawnLogged(process.execPath, ["--experimental-strip-types", issuerScript, "--out", tokenPath], issuerEnv(cleanEnv(), opts, audience, issuerUrl), repoRoot);
210
+ kids.push(issuer);
211
+ collectOutput(issuer, log);
212
+ if (!(await waitPort(opts.issuerPort, 15_000))) {
213
+ writeErr("desktop-issuer-timeout\n");
214
+ stopAll();
215
+ return 1;
216
+ }
217
+ const token = readFileSync(tokenPath, "utf8").trim();
218
+ if (token === "") {
219
+ writeErr("desktop-token-missing\n");
220
+ stopAll();
221
+ return 1;
222
+ }
223
+ const body = spawnLogged(process.execPath, ["--experimental-strip-types", mainTs], {
224
+ ...cleanEnv(),
225
+ VERAX_STATE_DIR: opts.stateDir,
226
+ VERAX_ISSUER: issuerUrl,
227
+ VERAX_JWKS_URL: `${issuerUrl}/.well-known/jwks.json`,
228
+ VERAX_AUDIENCE: audience,
229
+ VERAX_BIND: `127.0.0.1:${opts.bodyPort}`,
230
+ VERAX_POLICY_FILE: policy,
231
+ ...(opts.inventoryFile ? { VERAX_INVENTORY_FILE: opts.inventoryFile } : {}),
232
+ }, repoRoot);
233
+ kids.push(body);
234
+ collectOutput(body, log);
235
+ if (!(await waitPort(opts.bodyPort, 15_000))) {
236
+ writeErr("desktop-body-timeout\n");
237
+ stopAll();
238
+ return 1;
239
+ }
240
+ const panelSources = [
241
+ join(panelDir, "src"),
242
+ join(panelDir, "index.html"),
243
+ join(panelDir, "vite.config.ts"),
244
+ join(repoRoot, "packages"),
245
+ ];
246
+ if (panelBuildNeeded(join(panelDir, "dist", "index.html"), panelSources)) {
247
+ const built = spawnSync(process.execPath, [viteJs, "build"], {
248
+ cwd: panelDir,
249
+ env: { ...cleanEnv(), VERAX_BODY_URL: audience },
250
+ encoding: "utf8",
251
+ windowsHide: true,
252
+ });
253
+ if (built.status !== 0) {
254
+ const detail = `${built.stderr ?? ""}\n${built.stdout ?? ""}`.slice(0, 800);
255
+ writeErr(`desktop-panel-build-failed\n${detail}\n`);
256
+ stopAll();
257
+ return 1;
258
+ }
259
+ }
260
+ const panel = spawnLogged(process.execPath, [viteJs, "preview", "--host", "127.0.0.1", "--port", String(opts.panelPort), "--strictPort"], {
261
+ ...cleanEnv(),
262
+ VERAX_BODY_URL: audience,
263
+ }, panelDir);
264
+ kids.push(panel);
265
+ collectOutput(panel, log);
266
+ if (!(await waitPort(opts.panelPort, 20_000))) {
267
+ writeErr("desktop-panel-timeout\n");
268
+ stopAll();
269
+ return 1;
270
+ }
271
+ const url = `http://127.0.0.1:${opts.panelPort}`;
272
+ const scriptBrowser = Boolean(opts.browser && /\.(mjs|js|ts)$/.test(opts.browser));
273
+ const browserBin = scriptBrowser ? process.execPath : (opts.browser ?? defaultBrowser());
274
+ if (!browserBin) {
275
+ writeErr("desktop-browser-missing\n");
276
+ stopAll();
277
+ return 1;
278
+ }
279
+ const profileDir = join(opts.stateDir, "browser-profile");
280
+ mkdirSync(profileDir, { recursive: true });
281
+ const browserArgv = scriptBrowser
282
+ ? [opts.browser, url]
283
+ : [
284
+ `--user-data-dir=${profileDir}`,
285
+ "--no-first-run",
286
+ "--no-default-browser-check",
287
+ `--app=${url}`,
288
+ "--window-size=1360,880",
289
+ ];
290
+ const browser = spawnLogged(browserBin, browserArgv, cleanEnv(), repoRoot, false);
291
+ kids.push(browser);
292
+ collectOutput(browser, log);
293
+ if (log.text.includes(token)) {
294
+ writeErr("desktop-token-leaked\n");
295
+ stopAll();
296
+ return 1;
297
+ }
298
+ process.stdout.write(`desktop-ready issuer=${opts.issuerPort} body=${opts.bodyPort} panel=${opts.panelPort}\n`);
299
+ const onSignal = () => {
300
+ stopAll();
301
+ process.exit(1);
302
+ };
303
+ process.once("SIGINT", onSignal);
304
+ process.once("SIGTERM", onSignal);
305
+ const closed = new Promise((resolve) => {
306
+ browser.once("close", () => resolve());
307
+ });
308
+ const exitedEarly = await Promise.race([
309
+ closed.then(() => true),
310
+ new Promise((resolve) => {
311
+ setTimeout(() => resolve(false), 3_000);
312
+ }),
313
+ ]);
314
+ if (exitedEarly) {
315
+ writeErr("desktop-browser-exited-early\n");
316
+ process.removeListener("SIGINT", onSignal);
317
+ process.removeListener("SIGTERM", onSignal);
318
+ stopAll();
319
+ return 1;
320
+ }
321
+ await closed;
322
+ process.removeListener("SIGINT", onSignal);
323
+ process.removeListener("SIGTERM", onSignal);
324
+ stopAll();
325
+ return 0;
326
+ }
327
+ catch (err) {
328
+ stopAll();
329
+ const msg = err instanceof Error ? err.message : "unknown";
330
+ writeErr(`desktop-failed:${msg}\n`);
331
+ return 1;
332
+ }
333
+ }
334
+ export async function desktopMain(argv) {
335
+ const parsed = parseDesktopArgs(argv);
336
+ if ("error" in parsed) {
337
+ process.stderr.write("verax desktop [--state <dir>] [--port 5173] [--browser <cmd>] [--inventory <file>]\n");
338
+ return 78;
339
+ }
340
+ return runDesktop(parsed);
341
+ }
@@ -0,0 +1,8 @@
1
+ export type DoctorLevel = "ok" | "warn" | "fail";
2
+ export type DoctorCheck = {
3
+ id: string;
4
+ level: DoctorLevel;
5
+ detail: string;
6
+ };
7
+ export declare function runDoctor(env: NodeJS.ProcessEnv, argv: readonly string[]): DoctorCheck[];
8
+ export declare function doctorExit(checks: readonly DoctorCheck[]): number;
package/dist/doctor.js ADDED
@@ -0,0 +1,342 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { loadConfig, isLoopbackHost } from "./config.js";
4
+ import { pidAlive, readLockFile } from "./unlock.js";
5
+ const SECRET_RE = /sk-|-----BEGIN|Bearer |eyJ[A-Za-z0-9_-]{10,}\./;
6
+ const SECRET_NAME = /^(VERAX_DEV_TOKEN|.*_(TOKEN|SECRET|KEY))$/;
7
+ export function runDoctor(env, argv) {
8
+ const checks = [];
9
+ const issuer = Boolean(env.VERAX_ISSUER?.trim());
10
+ const jwks = Boolean(env.VERAX_JWKS_URL?.trim());
11
+ const audience = Boolean(env.VERAX_AUDIENCE?.trim());
12
+ checks.push({
13
+ id: "issuer-jwks-audience",
14
+ level: issuer && jwks && audience ? "ok" : "fail",
15
+ detail: issuer && jwks && audience
16
+ ? "VERAX_ISSUER, VERAX_JWKS_URL, and VERAX_AUDIENCE are set"
17
+ : "one of VERAX_ISSUER, VERAX_JWKS_URL, VERAX_AUDIENCE is missing",
18
+ });
19
+ const loaded = loadConfig({
20
+ ...env,
21
+ VERAX_STATE_DIR: env.VERAX_STATE_DIR || "placeholder",
22
+ VERAX_POLICY_FILE: env.VERAX_POLICY_FILE || "placeholder",
23
+ });
24
+ if (loaded.ok) {
25
+ const loop = isLoopbackHost(loaded.value.bindHost);
26
+ if (loop) {
27
+ checks.push({ id: "bind", level: "ok", detail: `bind ${loaded.value.bindHost} is loopback` });
28
+ }
29
+ else if (loaded.value.tlsTerminated) {
30
+ checks.push({
31
+ id: "bind",
32
+ level: "ok",
33
+ detail: "non-loopback bind with VERAX_TLS_TERMINATED=1",
34
+ });
35
+ }
36
+ else {
37
+ checks.push({
38
+ id: "bind",
39
+ level: "fail",
40
+ detail: "non-loopback bind without VERAX_TLS_TERMINATED=1",
41
+ });
42
+ }
43
+ }
44
+ else if (loaded.reason.includes("non-loopback")) {
45
+ checks.push({ id: "bind", level: "fail", detail: loaded.reason });
46
+ }
47
+ else {
48
+ checks.push({ id: "bind", level: "warn", detail: "bind not judged; issuer triple incomplete" });
49
+ }
50
+ const stateDir = env.VERAX_STATE_DIR?.trim() ?? "";
51
+ if (stateDir === "" || !existsSync(stateDir)) {
52
+ checks.push({
53
+ id: "state-dir",
54
+ level: stateDir === "" ? "fail" : "warn",
55
+ detail: stateDir === "" ? "VERAX_STATE_DIR is missing" : "VERAX_STATE_DIR does not exist yet",
56
+ });
57
+ }
58
+ else if (process.platform === "win32") {
59
+ checks.push({
60
+ id: "state-dir",
61
+ level: "warn",
62
+ detail: "not checked on this platform",
63
+ });
64
+ }
65
+ else {
66
+ const mode = statSync(stateDir).mode & 0o777;
67
+ checks.push({
68
+ id: "state-dir",
69
+ level: (mode & 0o077) === 0 ? "ok" : "fail",
70
+ detail: (mode & 0o077) === 0
71
+ ? "VERAX_STATE_DIR is owner-only"
72
+ : `VERAX_STATE_DIR mode ${mode.toString(8)} is not 0700`,
73
+ });
74
+ }
75
+ if (stateDir !== "" && existsSync(stateDir)) {
76
+ if (existsSync(join(stateDir, "memory"))) {
77
+ checks.push({
78
+ id: "legacy-memory",
79
+ level: "warn",
80
+ detail: "legacy memory/ is present; reads do not merge it. Move records under tenants/<tenantKey>/memory/",
81
+ });
82
+ }
83
+ const lockPath = join(stateDir, "ledger.lock");
84
+ if (existsSync(lockPath)) {
85
+ const existing = readLockFile(lockPath);
86
+ if (!existing) {
87
+ checks.push({ id: "ledger-lock", level: "fail", detail: "ledger.lock is unreadable" });
88
+ }
89
+ else if (existing.pid === process.pid) {
90
+ checks.push({ id: "ledger-lock", level: "ok", detail: `ledger.lock is this process (${existing.pid})` });
91
+ }
92
+ else if (pidAlive(existing.pid)) {
93
+ checks.push({ id: "ledger-lock", level: "warn", detail: `ledger.lock is held by live pid ${existing.pid}` });
94
+ }
95
+ else {
96
+ checks.push({
97
+ id: "ledger-lock",
98
+ level: "fail",
99
+ detail: `ledger.lock is held by dead pid ${existing.pid}; run: verax unlock ${stateDir}`,
100
+ });
101
+ }
102
+ }
103
+ checks.push(...evidenceChecks(stateDir, env));
104
+ }
105
+ const secretNames = Object.entries(env)
106
+ .filter(([k, v]) => Boolean(v) && (SECRET_NAME.test(k) || SECRET_RE.test(String(v))))
107
+ .map(([k]) => k);
108
+ const secretInArgv = argv.some((a) => SECRET_RE.test(a));
109
+ if (secretInArgv) {
110
+ checks.push({
111
+ id: "secrets-on-argv",
112
+ level: "fail",
113
+ detail: "process.argv matches sk-, PEM begin, Bearer, or JWT",
114
+ });
115
+ }
116
+ else if (secretNames.length > 0) {
117
+ checks.push({
118
+ id: "secrets-in-env",
119
+ level: "fail",
120
+ detail: `named env keys look secret: ${secretNames.join(", ")}`,
121
+ });
122
+ }
123
+ else {
124
+ checks.push({
125
+ id: "secrets-on-argv",
126
+ level: "ok",
127
+ detail: "no sk-, PEM, Bearer, or JWT pattern on argv or env",
128
+ });
129
+ }
130
+ const panelPort = panelPortOf(env);
131
+ const portSource = env.VERAX_PANEL_PORT?.trim() ? "VERAX_PANEL_PORT" : "the 5173 default, VERAX_PANEL_PORT unset";
132
+ const redirectUris = redirectAllowList(env);
133
+ const panelRedirect = `http://127.0.0.1:${panelPort}/`;
134
+ // Nothing here opens a port: the number comes from the environment, so both
135
+ // lines name where it came from and an ok cannot be read as a live check.
136
+ if (redirectUris.some((u) => sameRedirect(u, panelRedirect))) {
137
+ checks.push({
138
+ id: "panel-redirect-uri",
139
+ level: "ok",
140
+ detail: `panel port ${panelPort} (${portSource}) is on the issuer redirect allow-list`,
141
+ });
142
+ }
143
+ else {
144
+ checks.push({
145
+ id: "panel-redirect-uri",
146
+ level: "warn",
147
+ detail: `panel port ${panelPort} (${portSource}) is not on the issuer redirect allow-list; set VERAX_DEV_REDIRECT_URIS=${panelRedirect} or run verax desktop so it passes the panel port`,
148
+ });
149
+ }
150
+ const configuredIssuer = stripSlash(env.VERAX_ISSUER?.trim() ?? "");
151
+ const panelIssuer = stripSlash(env.VITE_VERAX_ISSUER?.trim() || "http://127.0.0.1:8790");
152
+ if (configuredIssuer !== "" && configuredIssuer === panelIssuer) {
153
+ checks.push({
154
+ id: "panel-issuer",
155
+ level: "ok",
156
+ detail: "panel last-resort issuer matches VERAX_ISSUER",
157
+ });
158
+ }
159
+ else if (configuredIssuer !== "") {
160
+ checks.push({
161
+ id: "panel-issuer",
162
+ level: "warn",
163
+ detail: `panel last-resort issuer ${panelIssuer} differs from VERAX_ISSUER ${configuredIssuer}; the session reads authorization_servers from resource metadata — if that document is unreachable the panel will not start`,
164
+ });
165
+ }
166
+ const tokenScope = scopeFromDevToken(env.VERAX_DEV_TOKEN);
167
+ const mintScope = env.VERAX_DEV_SCOPE?.trim() ?? "";
168
+ const seenScope = tokenScope ?? (mintScope !== "" ? mintScope : null);
169
+ if (seenScope !== null) {
170
+ const parts = seenScope.split(/\s+/).filter((s) => s !== "");
171
+ if (parts.includes("verax:audit")) {
172
+ checks.push({
173
+ id: "dev-token-audit",
174
+ level: "ok",
175
+ detail: "development token scope includes verax:audit",
176
+ });
177
+ }
178
+ else {
179
+ checks.push({
180
+ id: "dev-token-audit",
181
+ level: "warn",
182
+ detail: "development token scope lacks verax:audit; the panel cannot read /api/ledger or /healthz counts",
183
+ });
184
+ }
185
+ }
186
+ return checks;
187
+ }
188
+ const DEFAULT_REDIRECTS = ["http://127.0.0.1:5173/", "http://127.0.0.1:4173/"];
189
+ function stripSlash(s) {
190
+ return s.replace(/\/$/, "");
191
+ }
192
+ function panelPortOf(env) {
193
+ const raw = env.VERAX_PANEL_PORT?.trim() ?? "";
194
+ const n = Number(raw);
195
+ if (Number.isInteger(n) && n > 0 && n < 65536)
196
+ return n;
197
+ return 5173;
198
+ }
199
+ function redirectAllowList(env) {
200
+ const raw = env.VERAX_DEV_REDIRECT_URIS ?? "";
201
+ const listed = raw
202
+ .split(",")
203
+ .map((s) => s.trim())
204
+ .filter((s) => s !== "");
205
+ return listed.length > 0 ? listed : DEFAULT_REDIRECTS;
206
+ }
207
+ function sameRedirect(a, b) {
208
+ try {
209
+ const na = new URL(a);
210
+ const nb = new URL(b);
211
+ const pa = na.pathname === "" ? "/" : na.pathname;
212
+ const pb = nb.pathname === "" ? "/" : nb.pathname;
213
+ return `${na.origin}${pa}` === `${nb.origin}${pb}`;
214
+ }
215
+ catch {
216
+ return false;
217
+ }
218
+ }
219
+ function scopeFromDevToken(raw) {
220
+ if (!raw || raw.trim() === "")
221
+ return null;
222
+ const parts = raw.trim().split(".");
223
+ if (parts.length < 2)
224
+ return null;
225
+ try {
226
+ const json = Buffer.from(parts[1], "base64url").toString("utf8");
227
+ const payload = JSON.parse(json);
228
+ return typeof payload.scope === "string" ? payload.scope : null;
229
+ }
230
+ catch {
231
+ return null;
232
+ }
233
+ }
234
+ export function doctorExit(checks) {
235
+ return checks.some((c) => c.level === "fail") ? 1 : 0;
236
+ }
237
+ function countJsonl(path) {
238
+ if (!existsSync(path))
239
+ return { lines: 0, corrupt: false };
240
+ try {
241
+ const text = readFileSync(path, "utf8");
242
+ let lines = 0;
243
+ for (const line of text.split("\n")) {
244
+ if (line === "")
245
+ continue;
246
+ lines += 1;
247
+ try {
248
+ JSON.parse(line);
249
+ }
250
+ catch {
251
+ return { lines, corrupt: true };
252
+ }
253
+ }
254
+ return { lines, corrupt: false };
255
+ }
256
+ catch {
257
+ return { lines: 0, corrupt: true };
258
+ }
259
+ }
260
+ function heartbeatMaxMs(env) {
261
+ const n = Number(env.VERAX_HEARTBEAT_MAX_MS ?? "30000");
262
+ return Number.isFinite(n) && n > 0 ? n : 30_000;
263
+ }
264
+ function evidenceChecks(stateDir, env) {
265
+ const checks = [];
266
+ const src = countJsonl(join(stateDir, "decisions.jsonl"));
267
+ const hbPath = join(stateDir, "heartbeat.json");
268
+ let hb = null;
269
+ if (existsSync(hbPath)) {
270
+ try {
271
+ hb = JSON.parse(readFileSync(hbPath, "utf8"));
272
+ }
273
+ catch {
274
+ hb = null;
275
+ }
276
+ }
277
+ if (src.lines > 0 || hb) {
278
+ if (!hb || typeof hb.atMs !== "number") {
279
+ checks.push({
280
+ id: "heartbeat",
281
+ level: "fail",
282
+ detail: "ledger has rows but no readable heartbeat; the evidence service looks silent",
283
+ });
284
+ }
285
+ else if (Date.now() - hb.atMs > heartbeatMaxMs(env)) {
286
+ checks.push({
287
+ id: "heartbeat",
288
+ level: "fail",
289
+ detail: `heartbeat is silent; last pulse ${hb.atMs} lastDecisionN ${String(hb.lastDecisionN ?? "?")}`,
290
+ });
291
+ }
292
+ else {
293
+ checks.push({
294
+ id: "heartbeat",
295
+ level: "ok",
296
+ detail: `heartbeat live; lastDecisionN ${String(hb.lastDecisionN ?? "?")}`,
297
+ });
298
+ }
299
+ }
300
+ // Both halves of the evidence are mirrored, so both are compared: an effects
301
+ // copy that quietly drops rows is the same silence as a missing decision copy.
302
+ const srcEffects = countJsonl(join(stateDir, "effects.jsonl"));
303
+ const copy = countJsonl(join(stateDir, "evidence-copy", "decisions.jsonl"));
304
+ const copyEffects = countJsonl(join(stateDir, "evidence-copy", "effects.jsonl"));
305
+ const anything = src.lines > 0 ||
306
+ srcEffects.lines > 0 ||
307
+ copy.lines > 0 ||
308
+ copyEffects.lines > 0 ||
309
+ copy.corrupt ||
310
+ copyEffects.corrupt;
311
+ if (anything) {
312
+ if (copy.corrupt || copyEffects.corrupt) {
313
+ checks.push({
314
+ id: "evidence-copy",
315
+ level: "fail",
316
+ detail: `evidence-copy/${copy.corrupt ? "decisions" : "effects"}.jsonl is corrupt`,
317
+ });
318
+ }
319
+ else if (copy.lines < src.lines) {
320
+ checks.push({
321
+ id: "evidence-copy",
322
+ level: "fail",
323
+ detail: `evidence copy is stale: ${copy.lines} lines behind source ${src.lines}`,
324
+ });
325
+ }
326
+ else if (copyEffects.lines < srcEffects.lines) {
327
+ checks.push({
328
+ id: "evidence-copy",
329
+ level: "fail",
330
+ detail: `evidence copy is stale on effects: ${copyEffects.lines} effect line(s) behind source ${srcEffects.lines}`,
331
+ });
332
+ }
333
+ else {
334
+ checks.push({
335
+ id: "evidence-copy",
336
+ level: "ok",
337
+ detail: `evidence copy has ${copy.lines} decision line(s) and ${copyEffects.lines} effect line(s)`,
338
+ });
339
+ }
340
+ }
341
+ return checks;
342
+ }
package/dist/halt.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function runHalt(stateDir: string, writeErr?: (s: string) => void): number;