@verax-ai/body 0.2.2 → 0.3.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/README.md CHANGED
@@ -29,7 +29,8 @@ Bearer token the configured issuer did not sign.
29
29
  | Variable | Required | Meaning |
30
30
  | --- | --- | --- |
31
31
  | `VERAX_ISSUER` | yes | Issuer the agent's token must carry. |
32
- | `VERAX_JWKS_URL` | yes | Where the body fetches the keys that verify that token. |
32
+ | `VERAX_JWKS_URL` | one of | Where the body fetches the keys that verify that token. Set this or `VERAX_JWKS_FILE`, not both. |
33
+ | `VERAX_JWKS_FILE` | one of | Path to a JWKS JSON file read once at start, instead of fetching `VERAX_JWKS_URL`. Accepted only with a loopback bind. |
33
34
  | `VERAX_AUDIENCE` | yes | Audience the token must name, so a token minted for something else is refused. |
34
35
  | `VERAX_STATE_DIR` | yes | Directory the ledger, keys and approvals live in. It stays on this machine. |
35
36
  | `VERAX_POLICY_FILE` | yes | Policy the gate applies. `@verax-ai/proxy` ships `policy/default.json`, which denies what it does not name. |
package/dist/auth.d.ts CHANGED
@@ -6,5 +6,9 @@ export type VerifiedBearer = {
6
6
  };
7
7
  export declare function resourceMetadataUrl(audience: string): string;
8
8
  export declare function wwwAuthenticate(audience: string): string;
9
- export declare function createVerifier(jwksUrl: string, issuer: string, audience: string): (token: string) => Promise<VerifiedBearer>;
9
+ export declare class JwksFileError extends Error {
10
+ readonly code = 78;
11
+ constructor(message?: string);
12
+ }
13
+ export declare function createVerifier(jwksUrl: string, issuer: string, audience: string, jwksFile?: string | null): (token: string) => Promise<VerifiedBearer>;
10
14
  export declare function readBearer(header: string | undefined): string | null;
package/dist/auth.js CHANGED
@@ -1,4 +1,5 @@
1
- import { createRemoteJWKSet, jwtVerify } from "jose";
1
+ import { readFileSync } from "node:fs";
2
+ import { createLocalJWKSet, createRemoteJWKSet, jwtVerify } from "jose";
2
3
  const ALGS = ["ES256", "EdDSA"];
3
4
  export function resourceMetadataUrl(audience) {
4
5
  const u = new URL(audience);
@@ -11,8 +12,31 @@ export function resourceMetadataUrl(audience) {
11
12
  export function wwwAuthenticate(audience) {
12
13
  return `Bearer resource_metadata="${resourceMetadataUrl(audience)}"`;
13
14
  }
14
- export function createVerifier(jwksUrl, issuer, audience) {
15
- const jwks = createRemoteJWKSet(new URL(jwksUrl));
15
+ function localJwks(path) {
16
+ let parsed;
17
+ try {
18
+ parsed = JSON.parse(readFileSync(path, "utf8"));
19
+ }
20
+ catch {
21
+ throw new JwksFileError();
22
+ }
23
+ if (parsed === null ||
24
+ typeof parsed !== "object" ||
25
+ !Array.isArray(parsed.keys) ||
26
+ parsed.keys.length === 0) {
27
+ throw new JwksFileError();
28
+ }
29
+ return createLocalJWKSet(parsed);
30
+ }
31
+ export class JwksFileError extends Error {
32
+ code = 78;
33
+ constructor(message = "VERAX_JWKS_FILE is missing or unparsable") {
34
+ super(message);
35
+ this.name = "JwksFileError";
36
+ }
37
+ }
38
+ export function createVerifier(jwksUrl, issuer, audience, jwksFile) {
39
+ const jwks = jwksFile ? localJwks(jwksFile) : createRemoteJWKSet(new URL(jwksUrl));
16
40
  return async (token) => {
17
41
  const { payload } = await jwtVerify(token, jwks, {
18
42
  issuer,
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { runApprove } from "./approve-cli.js";
6
6
  import { runDemo } from "./demo.js";
7
7
  import { desktopMain } from "./desktop.js";
8
8
  import { doctorExit, runDoctor } from "./doctor.js";
9
+ import { loadEnvFile, runInitLocal } from "./init-local.js";
9
10
  import { runHalt } from "./halt.js";
10
11
  import { main } from "./main.js";
11
12
  import { runOperator } from "./operator-cli.js";
@@ -18,6 +19,9 @@ const HELP = `verax - the body an agent asks before it acts, and the ledger it a
18
19
  Usage: verax <command> [options]
19
20
 
20
21
  (no command) serve MCP over Streamable HTTP at /mcp on VERAX_BIND (default 127.0.0.1:8787)
22
+ serve [--env-file <file>] same as serving, after loading KEY=VALUE lines from that file
23
+ init --local <stateDir> [--force] [--days N] [--port N]
24
+ write a loopback key, one agent token, and verax.env
21
25
  doctor [--json] check the configuration this process would run with
22
26
  demo [--keep] run a loopback body against a temporary ledger and print what it recorded
23
27
  [--with-conarium] also fetch Conarium with npx, attach it as a child, and put a masked read through the gate
@@ -34,9 +38,9 @@ Usage: verax <command> [options]
34
38
  --help, -h print this
35
39
  --version, -v print the version
36
40
 
37
- Serving needs VERAX_ISSUER, VERAX_JWKS_URL, VERAX_AUDIENCE, VERAX_STATE_DIR and
38
- VERAX_POLICY_FILE; \`verax doctor\` names what is missing. Records stay on this
39
- machine, under VERAX_STATE_DIR.
41
+ Serving needs VERAX_ISSUER, one of VERAX_JWKS_URL or VERAX_JWKS_FILE, VERAX_AUDIENCE,
42
+ VERAX_STATE_DIR and VERAX_POLICY_FILE; \`verax doctor\` names what is missing.
43
+ Records stay on this machine, under VERAX_STATE_DIR.
40
44
  `;
41
45
  function version() {
42
46
  // The published package answers with its own version, not a copy of it.
@@ -96,6 +100,28 @@ if (argv[0] === "unlock") {
96
100
  }
97
101
  process.exit(runUnlock(stateDir, (s) => process.stderr.write(s), { force }));
98
102
  }
103
+ if (argv[0] === "init") {
104
+ process.exit(await runInitLocal(argv.slice(1)));
105
+ }
106
+ if (argv[0] === "serve") {
107
+ const fileFlag = argv.indexOf("--env-file");
108
+ if (fileFlag !== -1) {
109
+ const file = argv[fileFlag + 1];
110
+ if (!file || file.startsWith("-")) {
111
+ process.stderr.write("verax serve --env-file <file>\n");
112
+ process.exit(78);
113
+ }
114
+ const loaded = loadEnvFile(file);
115
+ if (!loaded.ok) {
116
+ process.stderr.write(`${loaded.reason}\n`);
117
+ process.exit(78);
118
+ }
119
+ }
120
+ await main();
121
+ // main() returns once the server is listening. The open socket keeps the
122
+ // process up; this promise stops the rest of the dispatcher from running.
123
+ await new Promise(() => { });
124
+ }
99
125
  if (argv[0] === "doctor") {
100
126
  const json = argv.includes("--json");
101
127
  const checks = runDoctor(process.env, process.argv);
package/dist/config.d.ts CHANGED
@@ -8,6 +8,11 @@ export type BodyConfig = {
8
8
  bindPort: number;
9
9
  policyFile: string;
10
10
  tlsTerminated: boolean;
11
+ /**
12
+ * Path to a JWKS file read once at start. Set instead of `jwksUrl`.
13
+ * Loopback binds only.
14
+ */
15
+ jwksFile?: string | null;
11
16
  /** Path to an inventory JSON file. Missing file is absence, not a fault. */
12
17
  inventoryFile?: string | null;
13
18
  /**
package/dist/config.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "node:fs";
1
2
  export const EX_CONFIG = 78;
2
3
  const LOOPBACK = new Set(["127.0.0.1", "::1", "localhost"]);
3
4
  function parseBind(raw) {
@@ -14,11 +15,28 @@ export function isLoopbackHost(host) {
14
15
  const stripped = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
15
16
  return LOOPBACK.has(stripped);
16
17
  }
18
+ function jwksFileParses(path) {
19
+ try {
20
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
21
+ return Array.isArray(parsed.keys) && parsed.keys.length > 0;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
17
27
  export function loadConfig(env) {
18
28
  const issuer = env.VERAX_ISSUER?.trim() ?? "";
19
29
  const jwksUrl = env.VERAX_JWKS_URL?.trim() ?? "";
30
+ const jwksFile = env.VERAX_JWKS_FILE?.trim() ?? "";
20
31
  const audience = env.VERAX_AUDIENCE?.trim() ?? "";
21
- if (issuer === "" || jwksUrl === "" || audience === "") {
32
+ if (jwksUrl !== "" && jwksFile !== "") {
33
+ return {
34
+ ok: false,
35
+ code: EX_CONFIG,
36
+ reason: "both VERAX_JWKS_URL and VERAX_JWKS_FILE",
37
+ };
38
+ }
39
+ if (issuer === "" || audience === "" || (jwksUrl === "" && jwksFile === "")) {
22
40
  return {
23
41
  ok: false,
24
42
  code: EX_CONFIG,
@@ -27,6 +45,13 @@ export function loadConfig(env) {
27
45
  }
28
46
  const bind = parseBind(env.VERAX_BIND);
29
47
  const tlsTerminated = env.VERAX_TLS_TERMINATED === "1";
48
+ if (jwksFile !== "" && !isLoopbackHost(bind.host)) {
49
+ return {
50
+ ok: false,
51
+ code: EX_CONFIG,
52
+ reason: "VERAX_JWKS_FILE needs a loopback bind",
53
+ };
54
+ }
30
55
  if (!isLoopbackHost(bind.host) && !tlsTerminated) {
31
56
  return {
32
57
  ok: false,
@@ -34,6 +59,13 @@ export function loadConfig(env) {
34
59
  reason: "non-loopback bind without VERAX_TLS_TERMINATED=1",
35
60
  };
36
61
  }
62
+ if (jwksFile !== "" && !jwksFileParses(jwksFile)) {
63
+ return {
64
+ ok: false,
65
+ code: EX_CONFIG,
66
+ reason: "VERAX_JWKS_FILE is missing or unparsable",
67
+ };
68
+ }
37
69
  const stateDir = env.VERAX_STATE_DIR?.trim() ?? "";
38
70
  if (stateDir === "") {
39
71
  return { ok: false, code: EX_CONFIG, reason: "missing VERAX_STATE_DIR" };
@@ -49,6 +81,7 @@ export function loadConfig(env) {
49
81
  value: {
50
82
  issuer,
51
83
  jwksUrl,
84
+ jwksFile: jwksFile === "" ? null : jwksFile,
52
85
  audience,
53
86
  stateDir,
54
87
  bindHost: bind.host,
package/dist/desktop.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ export declare function resolveRepoRoot(): string;
2
+ export declare const DESKTOP_CLONE_ONLY = "verax desktop runs from a clone of github.com/verax-ai/verax; it is not in the npm package\n";
3
+ export declare function desktopCloneError(root: string): string | null;
1
4
  export type DesktopOpts = {
2
5
  stateDir: string;
3
6
  panelPort: number;
package/dist/desktop.js CHANGED
@@ -6,7 +6,16 @@ import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { pidAlive, readLockFile } from "./unlock.js";
8
8
  const here = dirname(fileURLToPath(import.meta.url));
9
- const repoRoot = join(here, "..", "..", "..");
9
+ export function resolveRepoRoot() {
10
+ return join(here, "..", "..", "..");
11
+ }
12
+ export const DESKTOP_CLONE_ONLY = "verax desktop runs from a clone of github.com/verax-ai/verax; it is not in the npm package\n";
13
+ export function desktopCloneError(root) {
14
+ if (!existsSync(join(root, "scripts", "dev-issuer.mjs")) || !existsSync(join(root, "apps", "panel"))) {
15
+ return DESKTOP_CLONE_ONLY;
16
+ }
17
+ return null;
18
+ }
10
19
  /** Newest modification time under a file or directory, ignoring build output. */
11
20
  function newestUnder(path, since) {
12
21
  let entry;
@@ -228,6 +237,12 @@ function collectOutput(child, sink) {
228
237
  child.stderr?.on("data", feed);
229
238
  }
230
239
  export async function runDesktop(opts, writeErr = (s) => process.stderr.write(s)) {
240
+ const repoRoot = resolveRepoRoot();
241
+ const cloneOnly = desktopCloneError(repoRoot);
242
+ if (cloneOnly) {
243
+ writeErr(cloneOnly);
244
+ return 78;
245
+ }
231
246
  mkdirSync(opts.stateDir, { recursive: true });
232
247
  const tokenPath = join(opts.stateDir, "dev-token");
233
248
  const issuerScript = join(repoRoot, "scripts", "dev-issuer.mjs");
package/dist/doctor.js CHANGED
@@ -9,13 +9,18 @@ const SECRET_NAME = /^(VERAX_DEV_TOKEN|.*_(TOKEN|SECRET|KEY))$/;
9
9
  export function runDoctor(env, argv) {
10
10
  const checks = [];
11
11
  const issuer = Boolean(env.VERAX_ISSUER?.trim());
12
- const jwks = Boolean(env.VERAX_JWKS_URL?.trim());
12
+ const jwksUrl = Boolean(env.VERAX_JWKS_URL?.trim());
13
+ const jwksFileSet = Boolean(env.VERAX_JWKS_FILE?.trim());
14
+ const jwks = jwksUrl || jwksFileSet;
13
15
  const audience = Boolean(env.VERAX_AUDIENCE?.trim());
16
+ const tripleReady = issuer && jwks && audience;
14
17
  checks.push({
15
18
  id: "issuer-jwks-audience",
16
- level: issuer && jwks && audience ? "ok" : "fail",
17
- detail: issuer && jwks && audience
18
- ? "VERAX_ISSUER, VERAX_JWKS_URL, and VERAX_AUDIENCE are set"
19
+ level: tripleReady ? "ok" : "fail",
20
+ detail: tripleReady
21
+ ? jwksFileSet && !jwksUrl
22
+ ? "VERAX_ISSUER, VERAX_JWKS_FILE, and VERAX_AUDIENCE are set"
23
+ : "VERAX_ISSUER, VERAX_JWKS_URL, and VERAX_AUDIENCE are set"
19
24
  : "one of VERAX_ISSUER, VERAX_JWKS_URL, VERAX_AUDIENCE is missing",
20
25
  });
21
26
  const loaded = loadConfig({
@@ -43,7 +48,7 @@ export function runDoctor(env, argv) {
43
48
  });
44
49
  }
45
50
  }
46
- else if (loaded.reason.includes("non-loopback")) {
51
+ else if (loaded.reason.includes("non-loopback") || loaded.reason.includes("loopback bind")) {
47
52
  checks.push({ id: "bind", level: "fail", detail: loaded.reason });
48
53
  }
49
54
  else {
@@ -212,8 +217,61 @@ export function runDoctor(env, argv) {
212
217
  });
213
218
  }
214
219
  }
220
+ checks.push(...jwksFileChecks(env, stateDir));
215
221
  return checks;
216
222
  }
223
+ function jwksFileChecks(env, stateDir) {
224
+ const file = env.VERAX_JWKS_FILE?.trim() ?? "";
225
+ if (file === "")
226
+ return [];
227
+ const out = [];
228
+ let kid = "";
229
+ let parses = false;
230
+ try {
231
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
232
+ const first = parsed.keys?.[0];
233
+ if (first && typeof first.kid === "string" && first.kid !== "") {
234
+ parses = true;
235
+ kid = first.kid;
236
+ }
237
+ }
238
+ catch {
239
+ parses = false;
240
+ }
241
+ out.push({
242
+ id: "jwks-file",
243
+ level: parses ? "ok" : "fail",
244
+ detail: parses ? `VERAX_JWKS_FILE ${file} parses kid ${kid}` : `VERAX_JWKS_FILE ${file} does not parse`,
245
+ });
246
+ if (stateDir === "")
247
+ return out;
248
+ const tokenPath = join(stateDir, "local-issuer", "agent.token");
249
+ if (!existsSync(tokenPath))
250
+ return out;
251
+ const exp = expFromToken(readFileSync(tokenPath, "utf8"));
252
+ const weekSec = 7 * 24 * 60 * 60;
253
+ if (exp !== null && exp <= Math.floor(Date.now() / 1000) + weekSec) {
254
+ out.push({
255
+ id: "local-agent-token",
256
+ level: "warn",
257
+ detail: `${tokenPath} expires within 7 days`,
258
+ });
259
+ }
260
+ return out;
261
+ }
262
+ function expFromToken(raw) {
263
+ const parts = raw.trim().split(".");
264
+ if (parts.length < 2)
265
+ return null;
266
+ try {
267
+ const json = Buffer.from(parts[1], "base64url").toString("utf8");
268
+ const payload = JSON.parse(json);
269
+ return typeof payload.exp === "number" ? payload.exp : null;
270
+ }
271
+ catch {
272
+ return null;
273
+ }
274
+ }
217
275
  const DEFAULT_REDIRECTS = ["http://127.0.0.1:5173/", "http://127.0.0.1:4173/"];
218
276
  function stripSlash(s) {
219
277
  return s.replace(/\/$/, "");
@@ -0,0 +1,15 @@
1
+ export type InitIo = {
2
+ stdout: {
3
+ write(s: string): unknown;
4
+ };
5
+ stderr: {
6
+ write(s: string): unknown;
7
+ };
8
+ };
9
+ export declare function loadEnvFile(path: string, env?: NodeJS.ProcessEnv): {
10
+ ok: true;
11
+ } | {
12
+ ok: false;
13
+ reason: string;
14
+ };
15
+ export declare function runInitLocal(argv: readonly string[], io?: InitIo): Promise<number>;
@@ -0,0 +1,219 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { calculateJwkThumbprint, exportJWK, exportPKCS8, generateKeyPair, importPKCS8, SignJWT, } from "jose";
6
+ import { EX_CONFIG } from "./config.js";
7
+ const ISSUER = "verax-local";
8
+ const SUBJECT = "local-agent";
9
+ const SCOPE = "verax:read verax:memory";
10
+ const DEFAULT_DAYS = 30;
11
+ const MAX_DAYS = 90;
12
+ const DEFAULT_PORT = 8787;
13
+ const MIN_PORT = 1024;
14
+ const MAX_PORT = 65535;
15
+ export function loadEnvFile(path, env = process.env) {
16
+ let text;
17
+ try {
18
+ text = readFileSync(path, "utf8");
19
+ }
20
+ catch {
21
+ return { ok: false, reason: "env file is missing or unreadable" };
22
+ }
23
+ for (const line of text.split(/\r?\n/)) {
24
+ const trimmed = line.trim();
25
+ if (trimmed === "" || trimmed.startsWith("#"))
26
+ continue;
27
+ const eq = trimmed.indexOf("=");
28
+ if (eq <= 0)
29
+ continue;
30
+ const key = trimmed.slice(0, eq).trim();
31
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
32
+ continue;
33
+ let value = trimmed.slice(eq + 1).trim();
34
+ if ((value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
35
+ (value.startsWith("'") && value.endsWith("'") && value.length >= 2)) {
36
+ value = value.slice(1, -1);
37
+ }
38
+ env[key] = value;
39
+ }
40
+ return { ok: true };
41
+ }
42
+ function shippedPolicyPath() {
43
+ const here = dirname(fileURLToPath(import.meta.url));
44
+ let resolved = null;
45
+ try {
46
+ resolved = join(dirname(fileURLToPath(import.meta.resolve("@verax-ai/proxy/package.json"))), "policy", "default.json");
47
+ }
48
+ catch {
49
+ resolved = null;
50
+ }
51
+ const candidates = [
52
+ resolved,
53
+ join(here, "..", "..", "proxy", "policy", "default.json"),
54
+ join(here, "..", "..", "@verax-ai", "proxy", "policy", "default.json"),
55
+ ];
56
+ for (const path of candidates) {
57
+ if (path && existsSync(path))
58
+ return path;
59
+ }
60
+ return null;
61
+ }
62
+ function ownerOnly(path) {
63
+ try {
64
+ chmodSync(path, 0o600);
65
+ }
66
+ catch {
67
+ // The platform does not honour the mode bit.
68
+ }
69
+ }
70
+ function slashPath(path) {
71
+ return path.replaceAll("\\", "/");
72
+ }
73
+ function shQuote(path) {
74
+ return `'${slashPath(path).replaceAll("'", "'\\''")}'`;
75
+ }
76
+ function psQuote(path) {
77
+ return `'${slashPath(path).replaceAll("'", "''")}'`;
78
+ }
79
+ function parseInitArgs(argv) {
80
+ let stateDir = "";
81
+ let force = false;
82
+ let days = DEFAULT_DAYS;
83
+ let port = DEFAULT_PORT;
84
+ let sawDays = false;
85
+ let sawPort = false;
86
+ for (let i = 0; i < argv.length; i += 1) {
87
+ const a = argv[i];
88
+ if (a === "--local") {
89
+ stateDir = argv[i + 1] ?? "";
90
+ i += 1;
91
+ continue;
92
+ }
93
+ if (a === "--force") {
94
+ force = true;
95
+ continue;
96
+ }
97
+ if (a === "--days") {
98
+ sawDays = true;
99
+ const raw = argv[i + 1] ?? "";
100
+ i += 1;
101
+ if (!/^[0-9]+$/.test(raw))
102
+ return { error: "--days wants an integer from 1 to 90" };
103
+ days = Number(raw);
104
+ continue;
105
+ }
106
+ if (a === "--port") {
107
+ sawPort = true;
108
+ const raw = argv[i + 1] ?? "";
109
+ i += 1;
110
+ if (!/^[0-9]+$/.test(raw))
111
+ return { error: "--port wants an integer from 1024 to 65535" };
112
+ port = Number(raw);
113
+ continue;
114
+ }
115
+ return { error: `flag-unknown:${a}` };
116
+ }
117
+ if (stateDir === "" || stateDir.startsWith("-")) {
118
+ return { error: "verax init --local <stateDir> [--force] [--days N] [--port N]" };
119
+ }
120
+ if (sawDays && (!Number.isInteger(days) || days < 1 || days > MAX_DAYS)) {
121
+ return { error: "--days wants an integer from 1 to 90" };
122
+ }
123
+ if (sawPort && (!Number.isInteger(port) || port < MIN_PORT || port > MAX_PORT)) {
124
+ return { error: "--port wants an integer from 1024 to 65535" };
125
+ }
126
+ return { stateDir: resolve(stateDir), force, days, port };
127
+ }
128
+ export async function runInitLocal(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
129
+ const parsed = parseInitArgs(argv);
130
+ if ("error" in parsed) {
131
+ io.stderr.write(`${parsed.error}\n`);
132
+ return EX_CONFIG;
133
+ }
134
+ const stateDir = parsed.stateDir;
135
+ const issuerDir = join(stateDir, "local-issuer");
136
+ if (existsSync(issuerDir) && !parsed.force) {
137
+ io.stderr.write(`refusing: ${issuerDir} already exists\n`);
138
+ return EX_CONFIG;
139
+ }
140
+ const policyDest = join(stateDir, "policy.json");
141
+ const policySrc = existsSync(policyDest) ? null : shippedPolicyPath();
142
+ if (!existsSync(policyDest) && !policySrc) {
143
+ io.stderr.write("shipped policy file not found\n");
144
+ return EX_CONFIG;
145
+ }
146
+ const { privateKey, publicKey } = await generateKeyPair("ES256", { extractable: true });
147
+ const pem = await exportPKCS8(privateKey);
148
+ const jwk = await exportJWK(publicKey);
149
+ const thumb = await calculateJwkThumbprint(jwk, "sha256");
150
+ const hex = Buffer.from(thumb, "base64url").toString("hex").slice(0, 8);
151
+ const kid = `verax-local-${hex}`;
152
+ const publicJwk = { ...jwk, alg: "ES256", use: "sig", kid };
153
+ const key = await importPKCS8(pem, "ES256");
154
+ const now = Math.floor(Date.now() / 1000);
155
+ const origin = `http://127.0.0.1:${parsed.port}`;
156
+ const token = await new SignJWT({ scope: SCOPE })
157
+ .setProtectedHeader({ alg: "ES256", kid })
158
+ .setIssuer(ISSUER)
159
+ .setAudience(origin)
160
+ .setSubject(SUBJECT)
161
+ .setJti(randomUUID())
162
+ .setIssuedAt(now)
163
+ .setExpirationTime(now + parsed.days * 24 * 60 * 60)
164
+ .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
+ `{
205
+ "mcpServers": {
206
+ "verax": {
207
+ "url": "${origin}/mcp",
208
+ "headers": { "Authorization": "Bearer <token from your issuer>" }
209
+ }
210
+ }
211
+ }`,
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;
219
+ }
package/dist/main.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { JwksFileError } from "./auth.js";
1
2
  import { EX_CONFIG, loadConfig, overlayInventoryArg } from "./config.js";
2
3
  import { KeysPartialError } from "./keys.js";
3
4
  import { listen } from "./server.js";
@@ -25,6 +26,10 @@ export async function main(env = process.env, argv = process.argv) {
25
26
  process.stderr.write("keys-partial\n");
26
27
  process.exit(err.code);
27
28
  }
29
+ if (err instanceof JwksFileError) {
30
+ process.stderr.write(`${err.message}\n`);
31
+ process.exit(err.code);
32
+ }
28
33
  throw err;
29
34
  }
30
35
  }
package/dist/server.js CHANGED
@@ -308,7 +308,7 @@ export async function listen(config) {
308
308
  throw err;
309
309
  }
310
310
  const toolMeta = [...TOOL_META, ...extraTools.map(downstreamMeta)];
311
- const verify = createVerifier(config.jwksUrl, config.issuer, config.audience);
311
+ const verify = createVerifier(config.jwksUrl, config.issuer, config.audience, config.jwksFile);
312
312
  const attachHandlers = (mcp) => {
313
313
  mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolMeta }));
314
314
  mcp.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verax-ai/body",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "engines": {
@@ -22,8 +22,8 @@
22
22
  ],
23
23
  "dependencies": {
24
24
  "@modelcontextprotocol/sdk": "^1.30.0",
25
- "@verax-ai/inventory": "^0.2.2",
26
- "@verax-ai/proxy": "^0.2.2",
25
+ "@verax-ai/inventory": "^0.3.0",
26
+ "@verax-ai/proxy": "^0.3.0",
27
27
  "jose": "^6.2.11"
28
28
  },
29
29
  "description": "VERAX body: an MCP server that decides, records and proves what an agent did.",