@verax-ai/body 0.3.0 → 0.4.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/doctor.d.ts CHANGED
@@ -5,4 +5,6 @@ export type DoctorCheck = {
5
5
  detail: string;
6
6
  };
7
7
  export declare function runDoctor(env: NodeJS.ProcessEnv, argv: readonly string[]): DoctorCheck[];
8
+ /** Last 20 lines of `<stateDir>/body.log`, or a missing-file sentence. */
9
+ export declare function doctorBodyLog(stateDir: string): string;
8
10
  export declare function doctorExit(checks: readonly DoctorCheck[]): number;
package/dist/doctor.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import { join } from "node:path";
4
+ import { readWindowsMachineRoots, SystemToolError, windowsAgentTokenPath } from "./install.js";
5
+ import { readLogTail } from "./body-log.js";
3
6
  import { indexCoverage, listPieceFiles } from "@verax-ai/proxy";
4
7
  import { loadConfig, isLoopbackHost } from "./config.js";
5
8
  import { parseDownstreamDocument } from "./downstream.js";
@@ -172,24 +175,32 @@ export function runDoctor(env, argv) {
172
175
  }
173
176
  const tokenScope = scopeFromDevToken(env.VERAX_DEV_TOKEN);
174
177
  const mintScope = env.VERAX_DEV_SCOPE?.trim() ?? "";
175
- const seenScope = tokenScope ?? (mintScope !== "" ? mintScope : null);
176
- if (seenScope !== null) {
177
- const parts = seenScope.split(/\s+/).filter((s) => s !== "");
178
+ // VERAX_DEV_SCOPE cannot put verax:audit on the agent file or on a session
179
+ // that has not passed a passkey. The panel reads the ledger with that session.
180
+ if (tokenScope !== null) {
181
+ const parts = tokenScope.split(/\s+/).filter((s) => s !== "");
178
182
  if (parts.includes("verax:audit")) {
179
183
  checks.push({
180
184
  id: "dev-token-audit",
181
- level: "ok",
182
- detail: "development token scope includes verax:audit",
185
+ level: "warn",
186
+ detail: "development token carries verax:audit; that scope belongs on the passkey session, not the agent token. The panel reads /api/ledger only after a passkey sign-in",
183
187
  });
184
188
  }
185
189
  else {
186
190
  checks.push({
187
191
  id: "dev-token-audit",
188
- level: "warn",
189
- detail: "development token scope lacks verax:audit; the panel cannot read /api/ledger or /healthz counts",
192
+ level: "ok",
193
+ detail: "development token does not carry verax:audit",
190
194
  });
191
195
  }
192
196
  }
197
+ else if (mintScope !== "") {
198
+ checks.push({
199
+ id: "dev-token-audit",
200
+ level: "ok",
201
+ detail: "VERAX_DEV_SCOPE does not mint verax:audit onto the agent token or a session without a passkey; the panel reads /api/ledger with the passkey session",
202
+ });
203
+ }
193
204
  const downstreamPath = env.VERAX_DOWNSTREAM?.trim() ?? "";
194
205
  if (downstreamPath !== "") {
195
206
  try {
@@ -245,8 +256,9 @@ function jwksFileChecks(env, stateDir) {
245
256
  });
246
257
  if (stateDir === "")
247
258
  return out;
248
- const tokenPath = join(stateDir, "local-issuer", "agent.token");
249
- if (!existsSync(tokenPath))
259
+ const candidates = [agentTokenOnDisk(env), join(stateDir, "local-issuer", "agent.token")].filter((file) => file !== null);
260
+ const tokenPath = candidates.find((file) => existsSync(file));
261
+ if (!tokenPath)
250
262
  return out;
251
263
  const exp = expFromToken(readFileSync(tokenPath, "utf8"));
252
264
  const weekSec = 7 * 24 * 60 * 60;
@@ -259,6 +271,26 @@ function jwksFileChecks(env, stateDir) {
259
271
  }
260
272
  return out;
261
273
  }
274
+ /** Install token: ProgramData on Windows, `~/.verax/agent.token` on Linux and macOS. */
275
+ function agentTokenOnDisk(env) {
276
+ if (process.platform === "win32") {
277
+ const sid = env.VERAX_USER_SID?.trim().replace(/^\*/, "") ?? "";
278
+ if (!/^S-1-[0-9-]+$/i.test(sid))
279
+ return null;
280
+ try {
281
+ return windowsAgentTokenPath(env, sid, readWindowsMachineRoots());
282
+ }
283
+ catch (err) {
284
+ if (err instanceof SystemToolError)
285
+ return null;
286
+ throw err;
287
+ }
288
+ }
289
+ const home = env.HOME?.trim() || env.USERPROFILE?.trim() || homedir();
290
+ if (home === "")
291
+ return null;
292
+ return join(home, ".verax", "agent.token");
293
+ }
262
294
  function expFromToken(raw) {
263
295
  const parts = raw.trim().split(".");
264
296
  if (parts.length < 2)
@@ -318,6 +350,14 @@ function scopeFromDevToken(raw) {
318
350
  return null;
319
351
  }
320
352
  }
353
+ /** Last 20 lines of `<stateDir>/body.log`, or a missing-file sentence. */
354
+ export function doctorBodyLog(stateDir) {
355
+ const file = join(stateDir, "body.log");
356
+ const tail = readLogTail(file, 20);
357
+ if (tail === null)
358
+ return `body.log missing: ${file}\n`;
359
+ return tail;
360
+ }
321
361
  export function doctorExit(checks) {
322
362
  return checks.some((c) => c.level === "fail") ? 1 : 0;
323
363
  }
@@ -348,6 +388,8 @@ function heartbeatMaxMs(env) {
348
388
  const n = Number(env.VERAX_HEARTBEAT_MAX_MS ?? "30000");
349
389
  return Number.isFinite(n) && n > 0 ? n : 30_000;
350
390
  }
391
+ /** A pulse this far ahead of the clock is still treated as now. */
392
+ const HEARTBEAT_FUTURE_SKEW_MS = 60_000;
351
393
  function evidenceChecks(stateDir, env) {
352
394
  const checks = [];
353
395
  let pieces;
@@ -360,7 +402,24 @@ function evidenceChecks(stateDir, env) {
360
402
  }
361
403
  const srcLines = pieces.reduce((s, p) => s + countJsonl(p.decisions).lines, 0);
362
404
  const decisionN = pieces.reduce((s, p) => s + (p.closed ? p.n : countJsonl(p.decisions).lines), 0);
363
- if (decisionN > 0) {
405
+ let closedOff = null;
406
+ for (const piece of pieces) {
407
+ if (!piece.closed)
408
+ continue;
409
+ const lines = countJsonl(piece.decisions).lines;
410
+ if (lines !== piece.n) {
411
+ closedOff = { id: piece.id, lines, n: piece.n };
412
+ break;
413
+ }
414
+ }
415
+ if (closedOff) {
416
+ checks.push({
417
+ id: "ledger-index",
418
+ level: "fail",
419
+ detail: `piece ${closedOff.id} has ${closedOff.lines} decision line(s), the manifest says ${closedOff.n}`,
420
+ });
421
+ }
422
+ else if (decisionN > 0) {
364
423
  const known = new Set(pieces.map((p) => p.id));
365
424
  const cov = indexCoverage(stateDir);
366
425
  if (cov === null) {
@@ -413,6 +472,13 @@ function evidenceChecks(stateDir, env) {
413
472
  detail: "ledger has rows but no readable heartbeat; the evidence service looks silent",
414
473
  });
415
474
  }
475
+ else if (!Number.isFinite(hb.atMs) || hb.atMs > Date.now() + HEARTBEAT_FUTURE_SKEW_MS) {
476
+ checks.push({
477
+ id: "heartbeat",
478
+ level: "fail",
479
+ detail: "heartbeat time is in the future",
480
+ });
481
+ }
416
482
  else if (Date.now() - hb.atMs > heartbeatMaxMs(env)) {
417
483
  checks.push({
418
484
  id: "heartbeat",
@@ -445,6 +511,8 @@ function evidenceChecks(stateDir, env) {
445
511
  srcEffects.lines > 0 ||
446
512
  copy.lines > 0 ||
447
513
  copyEffects.lines > 0 ||
514
+ src.corrupt ||
515
+ srcEffects.corrupt ||
448
516
  copy.corrupt ||
449
517
  copyEffects.corrupt;
450
518
  if (!pieceAnything)
@@ -452,7 +520,15 @@ function evidenceChecks(stateDir, env) {
452
520
  anything = true;
453
521
  if (fail)
454
522
  continue;
455
- if (copy.corrupt || copyEffects.corrupt) {
523
+ if (src.corrupt || srcEffects.corrupt) {
524
+ const file = src.corrupt ? "decisions.jsonl" : "effects.jsonl";
525
+ fail = {
526
+ id: "evidence-copy",
527
+ level: "fail",
528
+ detail: `source ${file} on piece ${piece.id} is corrupt`,
529
+ };
530
+ }
531
+ else if (copy.corrupt || copyEffects.corrupt) {
456
532
  fail = {
457
533
  id: "evidence-copy",
458
534
  level: "fail",
@@ -473,6 +549,13 @@ function evidenceChecks(stateDir, env) {
473
549
  detail: `evidence copy is stale on piece ${piece.id} effects: ${copyEffects.lines} effect line(s) behind source ${srcEffects.lines}`,
474
550
  };
475
551
  }
552
+ else if (copy.lines > src.lines || copyEffects.lines > srcEffects.lines) {
553
+ fail = {
554
+ id: "evidence-copy",
555
+ level: "fail",
556
+ detail: `source is shorter than its evidence copy on piece ${piece.id}`,
557
+ };
558
+ }
476
559
  }
477
560
  if (anything) {
478
561
  checks.push(fail ?? {
@@ -1,3 +1,7 @@
1
+ /** Written by `verax install` before the service starts. `/healthz` echoes it. */
2
+ export declare const INSTALL_HEALTH_NONCE = "install-health-nonce";
3
+ /** Hex nonce from this install, or null when the file is absent or not that shape. */
4
+ export declare function readInstallHealthNonce(stateDir: string): string | null;
1
5
  export type HealthHeartbeat = {
2
6
  atMs: number;
3
7
  lastDecisionN: number;
@@ -1,5 +1,19 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ /** Written by `verax install` before the service starts. `/healthz` echoes it. */
4
+ export const INSTALL_HEALTH_NONCE = "install-health-nonce";
5
+ /** Hex nonce from this install, or null when the file is absent or not that shape. */
6
+ export function readInstallHealthNonce(stateDir) {
7
+ try {
8
+ const text = readFileSync(join(stateDir, INSTALL_HEALTH_NONCE), "utf8").trim();
9
+ if (!/^[0-9a-f]{64}$/i.test(text))
10
+ return null;
11
+ return text.toLowerCase();
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
3
17
  export function readHeartbeat(stateDir) {
4
18
  const path = join(stateDir, "heartbeat.json");
5
19
  if (!existsSync(path))
@@ -12,4 +12,28 @@ export declare function loadEnvFile(path: string, env?: NodeJS.ProcessEnv): {
12
12
  ok: false;
13
13
  reason: string;
14
14
  };
15
- export declare function runInitLocal(argv: readonly string[], io?: InitIo): Promise<number>;
15
+ /**
16
+ * Drop every `VERAX_*` name before an env file is applied. Comparison is
17
+ * case-insensitive because Windows environment names are.
18
+ */
19
+ export declare function clearVeraxEnv(env: NodeJS.ProcessEnv): void;
20
+ /** `serve --env-file` refuses a file that names neither JWKS source. */
21
+ export declare function envFileJwksMissing(env: NodeJS.ProcessEnv): string | null;
22
+ export type InitLocalOpts = {
23
+ /** Write the agent token here and leave no copy under the state directory. */
24
+ tokenPath?: string;
25
+ /** Installed body prints its own summary. Skip this command's user-facing text. */
26
+ quiet?: boolean;
27
+ /**
28
+ * Install mode. State files inherit the state directory ACL (svc, Administrators, SYSTEM).
29
+ * Do not grant the invoking user and do not call `restrictToOwnerWin32`.
30
+ * Elevated install does not pass a token path under the invoking user's home.
31
+ * `tokenPath` remains for a non-elevated `verax init --local`.
32
+ */
33
+ noOwnerGrant?: boolean;
34
+ /** Install mode. Defaults to process.env. SUDO_USER names the POSIX owner. */
35
+ env?: NodeJS.ProcessEnv;
36
+ /** Test hook. A throw here runs the rollback after the issuer directory exists and before any file is written. */
37
+ beforeWrite?: () => void;
38
+ };
39
+ export declare function runInitLocal(argv: readonly string[], io?: InitIo, opts?: InitLocalOpts): Promise<number>;
@@ -1,7 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { chmodSync, existsSync, lstatSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { beginWinOwnerRun, endWinOwnerRun, ensureTokenParent, mkdirLeaf, restrictToOwnerWin32, SystemToolError } from "./install.js";
5
6
  import { calculateJwkThumbprint, exportJWK, exportPKCS8, generateKeyPair, importPKCS8, SignJWT, } from "jose";
6
7
  import { EX_CONFIG } from "./config.js";
7
8
  const ISSUER = "verax-local";
@@ -20,25 +21,50 @@ export function loadEnvFile(path, env = process.env) {
20
21
  catch {
21
22
  return { ok: false, reason: "env file is missing or unreadable" };
22
23
  }
24
+ const parsed = [];
23
25
  for (const line of text.split(/\r?\n/)) {
24
- const trimmed = line.trim();
26
+ let trimmed = line.trim();
25
27
  if (trimmed === "" || trimmed.startsWith("#"))
26
28
  continue;
29
+ if (trimmed.startsWith("export "))
30
+ trimmed = trimmed.slice("export ".length).trim();
27
31
  const eq = trimmed.indexOf("=");
28
32
  if (eq <= 0)
29
33
  continue;
30
34
  const key = trimmed.slice(0, eq).trim();
31
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
32
- continue;
35
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || !key.startsWith("VERAX_")) {
36
+ return { ok: false, reason: `env file refuses ${key}` };
37
+ }
33
38
  let value = trimmed.slice(eq + 1).trim();
34
39
  if ((value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
35
40
  (value.startsWith("'") && value.endsWith("'") && value.length >= 2)) {
36
41
  value = value.slice(1, -1);
37
42
  }
38
- env[key] = value;
43
+ parsed.push([key, value]);
39
44
  }
45
+ clearVeraxEnv(env);
46
+ for (const [key, value] of parsed)
47
+ env[key] = value;
40
48
  return { ok: true };
41
49
  }
50
+ /**
51
+ * Drop every `VERAX_*` name before an env file is applied. Comparison is
52
+ * case-insensitive because Windows environment names are.
53
+ */
54
+ export function clearVeraxEnv(env) {
55
+ for (const key of Object.keys(env)) {
56
+ if (key.toUpperCase().startsWith("VERAX_"))
57
+ delete env[key];
58
+ }
59
+ }
60
+ /** `serve --env-file` refuses a file that names neither JWKS source. */
61
+ export function envFileJwksMissing(env) {
62
+ const file = env.VERAX_JWKS_FILE?.trim() ?? "";
63
+ const url = env.VERAX_JWKS_URL?.trim() ?? "";
64
+ if (file === "" && url === "")
65
+ return "env file sets neither VERAX_JWKS_FILE nor VERAX_JWKS_URL";
66
+ return null;
67
+ }
42
68
  function shippedPolicyPath() {
43
69
  const here = dirname(fileURLToPath(import.meta.url));
44
70
  let resolved = null;
@@ -59,7 +85,13 @@ function shippedPolicyPath() {
59
85
  }
60
86
  return null;
61
87
  }
62
- function ownerOnly(path) {
88
+ /** POSIX keeps owner-only mode. Windows grants the current user only outside install mode. */
89
+ function ownerOnly(path, grantOwner) {
90
+ if (process.platform === "win32") {
91
+ if (grantOwner)
92
+ restrictToOwnerWin32(path);
93
+ return;
94
+ }
63
95
  try {
64
96
  chmodSync(path, 0o600);
65
97
  }
@@ -125,7 +157,25 @@ function parseInitArgs(argv) {
125
157
  }
126
158
  return { stateDir: resolve(stateDir), force, days, port };
127
159
  }
128
- export async function runInitLocal(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
160
+ function pathExists(target) {
161
+ try {
162
+ lstatSync(target);
163
+ return true;
164
+ }
165
+ catch {
166
+ return false;
167
+ }
168
+ }
169
+ export async function runInitLocal(argv, io = { stdout: process.stdout, stderr: process.stderr }, opts = {}) {
170
+ beginWinOwnerRun();
171
+ try {
172
+ return await runInitLocalBody(argv, io, opts);
173
+ }
174
+ finally {
175
+ endWinOwnerRun();
176
+ }
177
+ }
178
+ async function runInitLocalBody(argv, io, opts) {
129
179
  const parsed = parseInitArgs(argv);
130
180
  if ("error" in parsed) {
131
181
  io.stderr.write(`${parsed.error}\n`);
@@ -162,46 +212,82 @@ export async function runInitLocal(argv, io = { stdout: process.stdout, stderr:
162
212
  .setIssuedAt(now)
163
213
  .setExpirationTime(now + parsed.days * 24 * 60 * 60)
164
214
  .sign(key);
165
- mkdirSync(issuerDir, { recursive: true, mode: 0o700 });
166
- const keyPath = join(issuerDir, "key.pem");
167
- const jwksPath = join(issuerDir, "jwks.json");
168
- const tokenPath = join(issuerDir, "agent.token");
169
- const envPath = join(stateDir, "verax.env");
170
- writeFileSync(keyPath, pem, { encoding: "utf8", mode: 0o600 });
171
- ownerOnly(keyPath);
172
- writeFileSync(jwksPath, `${JSON.stringify({ keys: [publicJwk] })}\n`, { encoding: "utf8", mode: 0o600 });
173
- writeFileSync(tokenPath, token, { encoding: "utf8", mode: 0o600 });
174
- ownerOnly(tokenPath);
175
- const wrote = [keyPath, jwksPath, tokenPath];
176
- if (!existsSync(policyDest) && policySrc) {
177
- writeFileSync(policyDest, readFileSync(policySrc));
178
- wrote.push(policyDest);
179
- }
180
- const envBody = [
181
- `VERAX_ISSUER=${ISSUER}`,
182
- `VERAX_JWKS_FILE=${jwksPath}`,
183
- `VERAX_AUDIENCE=${origin}`,
184
- `VERAX_STATE_DIR=${stateDir}`,
185
- `VERAX_POLICY_FILE=${policyDest}`,
186
- `VERAX_BIND=127.0.0.1:${parsed.port}`,
187
- "",
188
- ].join("\n");
189
- writeFileSync(envPath, envBody, { encoding: "utf8" });
190
- wrote.push(envPath);
191
- const lines = [
192
- ...wrote.map((p) => `wrote ${p}`),
193
- "",
194
- "Start the body:",
195
- `verax serve --env-file ${shQuote(envPath)}`,
196
- "",
197
- "Claude Code (sh):",
198
- `claude mcp add --transport http verax ${origin}/mcp --header "Authorization: Bearer $(cat ${shQuote(tokenPath)})"`,
199
- "",
200
- "Claude Code (PowerShell):",
201
- `claude mcp add --transport http verax ${origin}/mcp --header "Authorization: Bearer $(Get-Content -Raw ${psQuote(tokenPath)})"`,
202
- "",
203
- "Cursor / generic mcp.json:",
204
- `{
215
+ const createdState = !pathExists(stateDir);
216
+ const issuerExisted = pathExists(issuerDir);
217
+ const wrote = [];
218
+ const createdFiles = new Set();
219
+ const rollback = () => {
220
+ if (createdState) {
221
+ rmSync(stateDir, { recursive: true, force: true });
222
+ return;
223
+ }
224
+ for (const p of [...createdFiles].reverse())
225
+ rmSync(p, { force: true });
226
+ if (!issuerExisted)
227
+ rmSync(issuerDir, { recursive: true, force: true });
228
+ };
229
+ const writeNew = (target, contents, mode) => {
230
+ if (!pathExists(target))
231
+ createdFiles.add(target);
232
+ writeFileSync(target, contents, mode === undefined ? { encoding: "utf8" } : { encoding: "utf8", mode });
233
+ wrote.push(target);
234
+ };
235
+ const grantOwner = opts.noOwnerGrant !== true;
236
+ try {
237
+ if (createdState)
238
+ mkdirLeaf(stateDir, 0o700);
239
+ if (process.platform === "win32" && grantOwner)
240
+ restrictToOwnerWin32(stateDir);
241
+ // --force keeps the existing issuer directory and rewrites what is in it.
242
+ mkdirLeaf(issuerDir, 0o700, { existingOk: parsed.force === true });
243
+ opts.beforeWrite?.();
244
+ const keyPath = join(issuerDir, "key.pem");
245
+ const jwksPath = join(issuerDir, "jwks.json");
246
+ const insideToken = join(issuerDir, "agent.token");
247
+ const external = opts.tokenPath !== undefined && resolve(opts.tokenPath) !== resolve(insideToken);
248
+ const tokenPath = external ? resolve(opts.tokenPath) : insideToken;
249
+ if (external)
250
+ ensureTokenParent(dirname(tokenPath), opts.env ?? process.env);
251
+ const envPath = join(stateDir, "verax.env");
252
+ writeNew(keyPath, pem, 0o600);
253
+ ownerOnly(keyPath, grantOwner);
254
+ writeNew(jwksPath, `${JSON.stringify({ keys: [publicJwk] })}\n`, 0o600);
255
+ ownerOnly(jwksPath, grantOwner);
256
+ writeNew(tokenPath, token, 0o600);
257
+ ownerOnly(tokenPath, grantOwner);
258
+ if (external && existsSync(insideToken))
259
+ unlinkSync(insideToken);
260
+ if (!existsSync(policyDest) && policySrc) {
261
+ writeNew(policyDest, readFileSync(policySrc, "utf8"));
262
+ ownerOnly(policyDest, grantOwner);
263
+ }
264
+ const envBody = [
265
+ `VERAX_ISSUER=${ISSUER}`,
266
+ `VERAX_JWKS_FILE=${jwksPath}`,
267
+ `VERAX_AUDIENCE=${origin}`,
268
+ `VERAX_STATE_DIR=${stateDir}`,
269
+ `VERAX_POLICY_FILE=${policyDest}`,
270
+ `VERAX_BIND=127.0.0.1:${parsed.port}`,
271
+ "",
272
+ ].join("\n");
273
+ writeNew(envPath, envBody);
274
+ ownerOnly(envPath, grantOwner);
275
+ const lines = [
276
+ ...wrote.map((p) => `wrote ${p}`),
277
+ "",
278
+ "A shell as the same user can read this directory and approve its own calls, so for anything that matters run the body as another OS user or in a container, or keep the agent in a sandbox that cannot read the state directory.",
279
+ "",
280
+ "Start the body:",
281
+ `verax serve --env-file ${shQuote(envPath)}`,
282
+ "",
283
+ "Claude Code (sh):",
284
+ `claude mcp add --transport http verax ${origin}/mcp --header "Authorization: Bearer $(cat ${shQuote(tokenPath)})"`,
285
+ "",
286
+ "Claude Code (PowerShell):",
287
+ `claude mcp add --transport http verax ${origin}/mcp --header "Authorization: Bearer $(Get-Content -Raw ${psQuote(tokenPath)})"`,
288
+ "",
289
+ "Cursor / generic mcp.json:",
290
+ `{
205
291
  "mcpServers": {
206
292
  "verax": {
207
293
  "url": "${origin}/mcp",
@@ -209,11 +295,22 @@ export async function runInitLocal(argv, io = { stdout: process.stdout, stderr:
209
295
  }
210
296
  }
211
297
  }`,
212
- `The token is the content of ${tokenPath}.`,
213
- "",
214
- "A held call is approved on this machine with: verax approve",
215
- "",
216
- ];
217
- io.stdout.write(lines.join("\n"));
218
- return 0;
298
+ `The token is the content of ${tokenPath}.`,
299
+ "",
300
+ "A held call is approved on this machine with: verax approve",
301
+ "",
302
+ ];
303
+ if (!opts.quiet)
304
+ io.stdout.write(lines.join("\n"));
305
+ return 0;
306
+ }
307
+ catch (err) {
308
+ const externalToken = wrote.find((p) => !p.startsWith(stateDir));
309
+ rollback();
310
+ if (externalToken && createdFiles.has(externalToken))
311
+ rmSync(externalToken, { force: true });
312
+ const detail = err instanceof Error ? err.message : "owner-only failed";
313
+ io.stderr.write(`${detail}\n`);
314
+ return err instanceof SystemToolError ? EX_CONFIG : 1;
315
+ }
219
316
  }