@verax-ai/body 0.2.2 → 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/README.md +9 -3
- package/dist/approve-cli.d.ts +22 -1
- package/dist/approve-cli.js +209 -22
- package/dist/auth.d.ts +13 -1
- package/dist/auth.js +37 -3
- package/dist/body-log.d.ts +14 -0
- package/dist/body-log.js +61 -0
- package/dist/cli.d.ts +23 -1
- package/dist/cli.js +236 -77
- package/dist/config.d.ts +31 -0
- package/dist/config.js +81 -1
- package/dist/demo.d.ts +2 -0
- package/dist/demo.js +8 -2
- package/dist/desktop-parent.d.ts +19 -0
- package/dist/desktop-parent.js +44 -0
- package/dist/desktop.d.ts +143 -11
- package/dist/desktop.js +947 -104
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +155 -14
- package/dist/health-extras.d.ts +4 -0
- package/dist/health-extras.js +14 -0
- package/dist/init-local.d.ts +39 -0
- package/dist/init-local.js +316 -0
- package/dist/install.d.ts +745 -0
- package/dist/install.js +5738 -0
- package/dist/keys.d.ts +6 -0
- package/dist/keys.js +43 -20
- package/dist/main.js +16 -1
- package/dist/no-bypass-scan.js +3 -2
- package/dist/reconcile-cli.js +3 -1
- package/dist/server.d.ts +14 -1
- package/dist/server.js +125 -3
- package/dist/tools/audit.d.ts +5 -2
- package/dist/tools/audit.js +12 -2
- package/dist/tools/memory.js +41 -6
- package/dist/tools/message.js +38 -9
- package/dist/unlock.d.ts +5 -2
- package/dist/unlock.js +6 -1
- package/dist/verify-cli.js +135 -15
- package/dist/wiring.js +1 -1
- package/dist/witness.js +154 -24
- package/package.json +7 -3
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import path, { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { runApprove } from "./approve-cli.js";
|
|
6
|
+
import { attachBodyLog } from "./body-log.js";
|
|
6
7
|
import { runDemo } from "./demo.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import { doctorBodyLog, doctorExit, runDoctor } from "./doctor.js";
|
|
9
|
+
import { clearVeraxEnv, envFileJwksMissing, loadEnvFile, runInitLocal } from "./init-local.js";
|
|
10
|
+
import { clearCliCodeCheckPassed, defaultElevated, directoryAccess, doctorStateTarget, elevatedCommandCodeRefusal, linuxNodeLabelCheck, linuxSelinuxCheck, liveInstalledChecks, markCliCodeCheckPassed, runInstall, runUninstall, SystemToolError, unreadableSentence } from "./install.js";
|
|
9
11
|
import { runHalt } from "./halt.js";
|
|
10
12
|
import { main } from "./main.js";
|
|
11
13
|
import { runOperator } from "./operator-cli.js";
|
|
@@ -18,10 +20,31 @@ const HELP = `verax - the body an agent asks before it acts, and the ledger it a
|
|
|
18
20
|
Usage: verax <command> [options]
|
|
19
21
|
|
|
20
22
|
(no command) serve MCP over Streamable HTTP at /mcp on VERAX_BIND (default 127.0.0.1:8787)
|
|
23
|
+
serve [--env-file <file>] [--log-file <file>]
|
|
24
|
+
same as serving, after loading KEY=VALUE lines from that file.
|
|
25
|
+
--log-file appends this process's stdout and stderr
|
|
26
|
+
init --local <stateDir> [--force] [--days N] [--port N]
|
|
27
|
+
write a loopback key, one agent token, and verax.env
|
|
28
|
+
install [--port N] [--days N] [--force]
|
|
29
|
+
copy this body to an administrator-owned directory and run it
|
|
30
|
+
as another account. Needs an elevated shell, and this program
|
|
31
|
+
must live in a directory only an administrator can change.
|
|
32
|
+
On Windows the agent token is under %ProgramData%\\Verax\\agent-token\\<SID>\\.
|
|
33
|
+
On Linux and macOS a process running as the invoking user writes ~/.verax/agent.token.
|
|
34
|
+
uninstall [--keep-state]
|
|
35
|
+
stop that body and remove its code. Needs an elevated shell
|
|
36
|
+
and the same administrator-owned copy.
|
|
21
37
|
doctor [--json] check the configuration this process would run with
|
|
22
38
|
demo [--keep] run a loopback body against a temporary ledger and print what it recorded
|
|
23
39
|
[--with-conarium] also fetch Conarium with npx, attach it as a child, and put a masked read through the gate
|
|
24
|
-
approve <args> approve a waiting request from this machine
|
|
40
|
+
approve <args> approve a waiting request from this machine.
|
|
41
|
+
On Windows the panel with a passkey is how a held call is approved.
|
|
42
|
+
An elevated CLI approve is a fallback from a separate administrator
|
|
43
|
+
account, not this account elevated:
|
|
44
|
+
"%ProgramFiles%\\verax-cli\\verax.cmd" approve.
|
|
45
|
+
Linux and macOS the root-owned Node, for example
|
|
46
|
+
sudo /opt/verax-node/<dir>/bin/node /opt/verax-cli/lib/node_modules/@verax-ai/body/dist/cli.js approve
|
|
47
|
+
or sudo /usr/bin/node with that same cli.js.
|
|
25
48
|
operator <args> enrol an operator and manage their passkeys
|
|
26
49
|
reconcile <args> compare the ledger against a statement
|
|
27
50
|
verify <stateDir> read a ledger back without a body: signatures, chain,
|
|
@@ -29,14 +52,13 @@ Usage: verax <command> [options]
|
|
|
29
52
|
witness <stateDir> run the witness alongside a body
|
|
30
53
|
halt <stateDir> stop the body from allowing anything further
|
|
31
54
|
unlock [--force] <stateDir> clear a stale ledger lock
|
|
32
|
-
desktop <args> open the local panel, joining the body that holds the ledger if one is up
|
|
33
55
|
|
|
34
56
|
--help, -h print this
|
|
35
57
|
--version, -v print the version
|
|
36
58
|
|
|
37
|
-
Serving needs VERAX_ISSUER, VERAX_JWKS_URL, VERAX_AUDIENCE,
|
|
38
|
-
VERAX_POLICY_FILE; \`verax doctor\` names what is missing.
|
|
39
|
-
machine, under VERAX_STATE_DIR.
|
|
59
|
+
Serving needs VERAX_ISSUER, one of VERAX_JWKS_URL or VERAX_JWKS_FILE, VERAX_AUDIENCE,
|
|
60
|
+
VERAX_STATE_DIR and VERAX_POLICY_FILE; \`verax doctor\` names what is missing.
|
|
61
|
+
Records stay on this machine, under VERAX_STATE_DIR.
|
|
40
62
|
`;
|
|
41
63
|
function version() {
|
|
42
64
|
// The published package answers with its own version, not a copy of it.
|
|
@@ -53,78 +75,215 @@ function version() {
|
|
|
53
75
|
}
|
|
54
76
|
return "unknown";
|
|
55
77
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
if (argv[0] === "demo") {
|
|
69
|
-
process.exit(await runDemo(argv, process.env, {
|
|
70
|
-
stdout: process.stdout,
|
|
71
|
-
stderr: process.stderr,
|
|
72
|
-
stdin: process.stdin,
|
|
73
|
-
isTTY: Boolean(process.stdin.isTTY),
|
|
74
|
-
}));
|
|
75
|
-
}
|
|
76
|
-
if (argv[0] === "approve") {
|
|
77
|
-
process.exit(await runApprove(argv));
|
|
78
|
-
}
|
|
79
|
-
if (argv[0] === "operator") {
|
|
80
|
-
process.exit(await runOperator(argv));
|
|
81
|
-
}
|
|
82
|
-
if (argv[0] === "halt") {
|
|
83
|
-
const stateDir = argv[1];
|
|
84
|
-
if (!stateDir) {
|
|
85
|
-
process.stderr.write("verax halt <stateDir>\n");
|
|
86
|
-
process.exit(78);
|
|
78
|
+
function isCliEntry() {
|
|
79
|
+
const entry = process.argv[1];
|
|
80
|
+
if (!entry)
|
|
81
|
+
return false;
|
|
82
|
+
// npm starts the CLI through a symbolic link (`bin/verax`, `node_modules/.bin/verax`): compare
|
|
83
|
+
// real paths, or a linked start runs nothing and exits 0.
|
|
84
|
+
try {
|
|
85
|
+
return realpathSync(path.resolve(entry)) === realpathSync(fileURLToPath(import.meta.url));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
87
89
|
}
|
|
88
|
-
process.exit(runHalt(stateDir));
|
|
89
|
-
}
|
|
90
|
-
if (argv[0] === "unlock") {
|
|
91
|
-
const force = argv.includes("--force");
|
|
92
|
-
const stateDir = argv.slice(1).find((a) => a !== "--force");
|
|
93
|
-
if (!stateDir) {
|
|
94
|
-
process.stderr.write("verax unlock [--force] <stateDir>\n");
|
|
95
|
-
process.exit(78);
|
|
96
|
-
}
|
|
97
|
-
process.exit(runUnlock(stateDir, (s) => process.stderr.write(s), { force }));
|
|
98
90
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
/**
|
|
92
|
+
* One gate for every command except `--help` and `--version`. An elevated
|
|
93
|
+
* process refuses code the invoking user can change before the command runs.
|
|
94
|
+
* Install and approve keep their own check for direct callers; the flag stops
|
|
95
|
+
* a second check in this process.
|
|
96
|
+
*/
|
|
97
|
+
export async function runCli(argv, hooks = {}) {
|
|
98
|
+
const stdout = hooks.stdout ?? process.stdout;
|
|
99
|
+
const stderr = hooks.stderr ?? process.stderr;
|
|
100
|
+
const platform = hooks.platform ?? process.platform;
|
|
101
|
+
const env = hooks.env ?? process.env;
|
|
102
|
+
if (argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
|
|
103
|
+
stdout.write(HELP);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
107
|
+
stdout.write(`${version()}\n`);
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
let isElevated = false;
|
|
111
|
+
try {
|
|
112
|
+
isElevated = hooks.elevated ? hooks.elevated() : defaultElevated(platform);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
if (err instanceof SystemToolError) {
|
|
116
|
+
stderr.write(`${err.message}\n`);
|
|
117
|
+
return 78;
|
|
108
118
|
}
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
if (isElevated) {
|
|
122
|
+
const refusal = elevatedCommandCodeRefusal(platform, env, hooks.codeProbe, {
|
|
123
|
+
execPathProbe: hooks.execPathProbe,
|
|
124
|
+
execArgv: hooks.execArgv,
|
|
125
|
+
});
|
|
126
|
+
if (refusal) {
|
|
127
|
+
stderr.write(refusal.endsWith("\n") ? refusal : `${refusal}\n`);
|
|
128
|
+
return 78;
|
|
129
|
+
}
|
|
130
|
+
markCliCodeCheckPassed();
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
if (isElevated && argv[0] === "demo" && argv.includes("--with-conarium")) {
|
|
134
|
+
stderr.write("refusing: demo --with-conarium fetches and runs code with npx; run it from a terminal that is not elevated\n");
|
|
135
|
+
return 78;
|
|
136
|
+
}
|
|
137
|
+
return await dispatchCli(argv, stdout, stderr, env);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
clearCliCodeCheckPassed();
|
|
109
141
|
}
|
|
110
|
-
process.exit(doctorExit(checks));
|
|
111
|
-
}
|
|
112
|
-
if (argv[0] === "reconcile") {
|
|
113
|
-
process.exit(runReconcile(argv));
|
|
114
|
-
}
|
|
115
|
-
if (argv[0] === "verify") {
|
|
116
|
-
process.exit(await runVerify(argv.slice(1)));
|
|
117
142
|
}
|
|
118
|
-
|
|
119
|
-
|
|
143
|
+
async function dispatchCli(argv, stdout, stderr, env) {
|
|
144
|
+
if (argv[0] === "demo") {
|
|
145
|
+
return runDemo(argv, env, {
|
|
146
|
+
stdout,
|
|
147
|
+
stderr,
|
|
148
|
+
stdin: process.stdin,
|
|
149
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (argv[0] === "approve") {
|
|
153
|
+
return runApprove(argv);
|
|
154
|
+
}
|
|
155
|
+
if (argv[0] === "operator") {
|
|
156
|
+
return runOperator(argv);
|
|
157
|
+
}
|
|
158
|
+
if (argv[0] === "halt") {
|
|
159
|
+
const stateDir = argv[1];
|
|
160
|
+
if (!stateDir) {
|
|
161
|
+
stderr.write("verax halt <stateDir>\n");
|
|
162
|
+
return 78;
|
|
163
|
+
}
|
|
164
|
+
return runHalt(stateDir, (s) => stderr.write(s));
|
|
165
|
+
}
|
|
166
|
+
if (argv[0] === "unlock") {
|
|
167
|
+
const force = argv.includes("--force");
|
|
168
|
+
const stateDir = argv.slice(1).find((a) => a !== "--force");
|
|
169
|
+
if (!stateDir) {
|
|
170
|
+
stderr.write("verax unlock [--force] <stateDir>\n");
|
|
171
|
+
return 78;
|
|
172
|
+
}
|
|
173
|
+
return runUnlock(stateDir, (s) => stderr.write(s), { force });
|
|
174
|
+
}
|
|
175
|
+
if (argv[0] === "init") {
|
|
176
|
+
return runInitLocal(argv.slice(1));
|
|
177
|
+
}
|
|
178
|
+
if (argv[0] === "install") {
|
|
179
|
+
return runInstall(argv);
|
|
180
|
+
}
|
|
181
|
+
if (argv[0] === "uninstall") {
|
|
182
|
+
return runUninstall(argv);
|
|
183
|
+
}
|
|
184
|
+
if (argv[0] === "serve") {
|
|
185
|
+
const logFlag = argv.indexOf("--log-file");
|
|
186
|
+
if (logFlag !== -1) {
|
|
187
|
+
const logPath = argv[logFlag + 1];
|
|
188
|
+
if (!logPath || logPath.startsWith("-")) {
|
|
189
|
+
stderr.write("verax serve --log-file <file>\n");
|
|
190
|
+
return 78;
|
|
191
|
+
}
|
|
192
|
+
const attached = attachBodyLog(logPath);
|
|
193
|
+
if (!attached.ok) {
|
|
194
|
+
stderr.write(`${attached.reason}\n`);
|
|
195
|
+
return 78;
|
|
196
|
+
}
|
|
197
|
+
stderr.write("verax serve starting\n");
|
|
198
|
+
}
|
|
199
|
+
const fileFlag = argv.indexOf("--env-file");
|
|
200
|
+
if (fileFlag !== -1) {
|
|
201
|
+
const file = argv[fileFlag + 1];
|
|
202
|
+
if (!file || file.startsWith("-")) {
|
|
203
|
+
stderr.write("verax serve --env-file <file>\n");
|
|
204
|
+
return 78;
|
|
205
|
+
}
|
|
206
|
+
clearVeraxEnv(process.env);
|
|
207
|
+
const loaded = loadEnvFile(file);
|
|
208
|
+
if (!loaded.ok) {
|
|
209
|
+
stderr.write(`${loaded.reason}\n`);
|
|
210
|
+
return 78;
|
|
211
|
+
}
|
|
212
|
+
const missingJwks = envFileJwksMissing(process.env);
|
|
213
|
+
if (missingJwks) {
|
|
214
|
+
stderr.write(`${missingJwks}\n`);
|
|
215
|
+
return 78;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
await main();
|
|
219
|
+
// main() returns once the server is listening. The open socket keeps the
|
|
220
|
+
// process up; this promise stops the rest of the dispatcher from running.
|
|
221
|
+
await new Promise(() => { });
|
|
222
|
+
}
|
|
223
|
+
if (argv[0] === "doctor") {
|
|
224
|
+
const json = argv.includes("--json");
|
|
225
|
+
const target = doctorStateTarget(env);
|
|
226
|
+
if (target && directoryAccess(target) === "unreadable") {
|
|
227
|
+
stderr.write(`${unreadableSentence(target, "doctor")}\n`);
|
|
228
|
+
return 77;
|
|
229
|
+
}
|
|
230
|
+
const nodeLabel = process.platform === "linux" ? linuxNodeLabelCheck() : null;
|
|
231
|
+
const checks = [
|
|
232
|
+
...runDoctor(env, process.argv),
|
|
233
|
+
...liveInstalledChecks(),
|
|
234
|
+
...(process.platform === "linux" ? [linuxSelinuxCheck()] : []),
|
|
235
|
+
...(nodeLabel ? [nodeLabel] : []),
|
|
236
|
+
];
|
|
237
|
+
const bodyLog = target ? doctorBodyLog(target) : "";
|
|
238
|
+
if (json) {
|
|
239
|
+
stdout.write(`${JSON.stringify(target ? { checks, bodyLog } : { checks })}\n`);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
for (const c of checks) {
|
|
243
|
+
stdout.write(`${c.level}\t${c.id}\t${c.detail}\n`);
|
|
244
|
+
}
|
|
245
|
+
if (bodyLog !== "")
|
|
246
|
+
stdout.write(bodyLog);
|
|
247
|
+
}
|
|
248
|
+
return doctorExit(checks);
|
|
249
|
+
}
|
|
250
|
+
if (argv[0] === "reconcile") {
|
|
251
|
+
return runReconcile(argv, (s) => stderr.write(s));
|
|
252
|
+
}
|
|
253
|
+
if (argv[0] === "verify") {
|
|
254
|
+
return runVerify(argv.slice(1), (s) => stdout.write(`${s}\n`));
|
|
255
|
+
}
|
|
256
|
+
if (argv[0] === "desktop") {
|
|
257
|
+
stderr.write("desktop-not-in-0.4.0: the desktop panel ships in 0.4.1\n");
|
|
258
|
+
return 64;
|
|
259
|
+
}
|
|
260
|
+
if (argv[0] === "witness") {
|
|
261
|
+
const stateDir = argv[1];
|
|
262
|
+
if (!stateDir) {
|
|
263
|
+
stderr.write("verax witness <stateDir>\n");
|
|
264
|
+
return 78;
|
|
265
|
+
}
|
|
266
|
+
await runWitness(stateDir);
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
const head = argv[0];
|
|
270
|
+
if (head !== undefined && !head.startsWith("--")) {
|
|
271
|
+
stderr.write(`unknown command: ${head}\n`);
|
|
272
|
+
stderr.write(HELP);
|
|
273
|
+
return 64;
|
|
274
|
+
}
|
|
275
|
+
// `main()` reads `--inventory`. Any other leading flag is not a serve option.
|
|
276
|
+
if (head !== undefined && head.startsWith("--") && head !== "--inventory") {
|
|
277
|
+
stderr.write(`unknown option: ${head}\n`);
|
|
278
|
+
stderr.write(HELP);
|
|
279
|
+
return 64;
|
|
280
|
+
}
|
|
281
|
+
await main();
|
|
282
|
+
// main() returns once the server is listening. The open socket keeps the
|
|
283
|
+
// process up, the same way as `serve`.
|
|
284
|
+
await new Promise(() => { });
|
|
285
|
+
return 0;
|
|
120
286
|
}
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
if (!stateDir) {
|
|
124
|
-
process.stderr.write("verax witness <stateDir>\n");
|
|
125
|
-
process.exit(78);
|
|
126
|
-
}
|
|
127
|
-
await runWitness(stateDir);
|
|
128
|
-
process.exit(0);
|
|
287
|
+
if (isCliEntry()) {
|
|
288
|
+
process.exit(await runCli(process.argv.slice(2)));
|
|
129
289
|
}
|
|
130
|
-
await main();
|
package/dist/config.d.ts
CHANGED
|
@@ -8,6 +8,19 @@ 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;
|
|
16
|
+
/**
|
|
17
|
+
* JWKS captured once at desktop start. Verification uses this set and does
|
|
18
|
+
* not refetch `jwksUrl`. Null when the body should use the remote set.
|
|
19
|
+
* Not a substitute for `jwksFile` (that mode refuses operator scopes).
|
|
20
|
+
*/
|
|
21
|
+
jwksPin?: {
|
|
22
|
+
keys: Record<string, unknown>[];
|
|
23
|
+
} | null;
|
|
11
24
|
/** Path to an inventory JSON file. Missing file is absence, not a fault. */
|
|
12
25
|
inventoryFile?: string | null;
|
|
13
26
|
/**
|
|
@@ -17,7 +30,25 @@ export type BodyConfig = {
|
|
|
17
30
|
* no downstream; a named file that cannot be attached stops the body.
|
|
18
31
|
*/
|
|
19
32
|
downstreamFile?: string | null;
|
|
33
|
+
/** Origins allowed to call this HTTP server. Empty means no browser origin. */
|
|
34
|
+
allowedOrigins?: readonly string[];
|
|
35
|
+
/**
|
|
36
|
+
* Bytes one tenant may keep under `memory/`, summed across stored versions.
|
|
37
|
+
* Default is 1 MiB. `VERAX_MEMORY_QUOTA_BYTES` overrides it.
|
|
38
|
+
*/
|
|
39
|
+
memoryQuotaBytes?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Bytes one tenant may append to `outbox.jsonl`.
|
|
42
|
+
* Default is 1 MiB. `VERAX_MESSAGE_QUOTA_BYTES` overrides it.
|
|
43
|
+
*/
|
|
44
|
+
messageQuotaBytes?: number;
|
|
20
45
|
};
|
|
46
|
+
/** Per-tenant `memory.put` cap. The HTTP body cap does not reset this total. */
|
|
47
|
+
export declare const DEFAULT_MEMORY_QUOTA_BYTES: number;
|
|
48
|
+
/** Per-tenant `message.send` cap. The HTTP body cap does not reset this total. */
|
|
49
|
+
export declare const DEFAULT_MESSAGE_QUOTA_BYTES: number;
|
|
50
|
+
export declare function memoryQuotaBytes(env?: NodeJS.ProcessEnv): number;
|
|
51
|
+
export declare function messageQuotaBytes(env?: NodeJS.ProcessEnv): number;
|
|
21
52
|
export type ConfigResult = {
|
|
22
53
|
ok: true;
|
|
23
54
|
value: BodyConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
export const EX_CONFIG = 78;
|
|
3
|
+
/** Per-tenant `memory.put` cap. The HTTP body cap does not reset this total. */
|
|
4
|
+
export const DEFAULT_MEMORY_QUOTA_BYTES = 1024 * 1024;
|
|
5
|
+
/** Per-tenant `message.send` cap. The HTTP body cap does not reset this total. */
|
|
6
|
+
export const DEFAULT_MESSAGE_QUOTA_BYTES = 1024 * 1024;
|
|
7
|
+
function positiveIntQuota(raw, fallback) {
|
|
8
|
+
const text = raw?.trim() ?? "";
|
|
9
|
+
if (text === "")
|
|
10
|
+
return fallback;
|
|
11
|
+
const n = Number(text);
|
|
12
|
+
if (!Number.isInteger(n) || n <= 0)
|
|
13
|
+
return fallback;
|
|
14
|
+
return n;
|
|
15
|
+
}
|
|
16
|
+
export function memoryQuotaBytes(env = process.env) {
|
|
17
|
+
return positiveIntQuota(env.VERAX_MEMORY_QUOTA_BYTES, DEFAULT_MEMORY_QUOTA_BYTES);
|
|
18
|
+
}
|
|
19
|
+
export function messageQuotaBytes(env = process.env) {
|
|
20
|
+
return positiveIntQuota(env.VERAX_MESSAGE_QUOTA_BYTES, DEFAULT_MESSAGE_QUOTA_BYTES);
|
|
21
|
+
}
|
|
2
22
|
const LOOPBACK = new Set(["127.0.0.1", "::1", "localhost"]);
|
|
3
23
|
function parseBind(raw) {
|
|
4
24
|
const text = raw && raw.trim() !== "" ? raw.trim() : "127.0.0.1:8787";
|
|
@@ -14,11 +34,49 @@ export function isLoopbackHost(host) {
|
|
|
14
34
|
const stripped = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
15
35
|
return LOOPBACK.has(stripped);
|
|
16
36
|
}
|
|
37
|
+
function jwksPinParses(raw) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.keys) || parsed.keys.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
if (!parsed.keys.every((key) => key !== null && typeof key === "object" && !Array.isArray(key)))
|
|
43
|
+
return null;
|
|
44
|
+
return { keys: parsed.keys };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function jwksFileParses(path) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
53
|
+
return Array.isArray(parsed.keys) && parsed.keys.length > 0;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
17
59
|
export function loadConfig(env) {
|
|
18
60
|
const issuer = env.VERAX_ISSUER?.trim() ?? "";
|
|
19
61
|
const jwksUrl = env.VERAX_JWKS_URL?.trim() ?? "";
|
|
62
|
+
const jwksFile = env.VERAX_JWKS_FILE?.trim() ?? "";
|
|
63
|
+
const jwksPinRaw = env.VERAX_JWKS_PIN?.trim() ?? "";
|
|
20
64
|
const audience = env.VERAX_AUDIENCE?.trim() ?? "";
|
|
21
|
-
|
|
65
|
+
let jwksPin = null;
|
|
66
|
+
if (jwksPinRaw !== "") {
|
|
67
|
+
jwksPin = jwksPinParses(jwksPinRaw);
|
|
68
|
+
if (!jwksPin) {
|
|
69
|
+
return { ok: false, code: EX_CONFIG, reason: "VERAX_JWKS_PIN is unparsable" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (jwksUrl !== "" && jwksFile !== "") {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
code: EX_CONFIG,
|
|
76
|
+
reason: "both VERAX_JWKS_URL and VERAX_JWKS_FILE",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (issuer === "" || audience === "" || (jwksUrl === "" && jwksFile === "")) {
|
|
22
80
|
return {
|
|
23
81
|
ok: false,
|
|
24
82
|
code: EX_CONFIG,
|
|
@@ -27,6 +85,13 @@ export function loadConfig(env) {
|
|
|
27
85
|
}
|
|
28
86
|
const bind = parseBind(env.VERAX_BIND);
|
|
29
87
|
const tlsTerminated = env.VERAX_TLS_TERMINATED === "1";
|
|
88
|
+
if (jwksFile !== "" && !isLoopbackHost(bind.host)) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
code: EX_CONFIG,
|
|
92
|
+
reason: "VERAX_JWKS_FILE needs a loopback bind",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
30
95
|
if (!isLoopbackHost(bind.host) && !tlsTerminated) {
|
|
31
96
|
return {
|
|
32
97
|
ok: false,
|
|
@@ -34,6 +99,13 @@ export function loadConfig(env) {
|
|
|
34
99
|
reason: "non-loopback bind without VERAX_TLS_TERMINATED=1",
|
|
35
100
|
};
|
|
36
101
|
}
|
|
102
|
+
if (jwksFile !== "" && !jwksFileParses(jwksFile)) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
code: EX_CONFIG,
|
|
106
|
+
reason: "VERAX_JWKS_FILE is missing or unparsable",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
37
109
|
const stateDir = env.VERAX_STATE_DIR?.trim() ?? "";
|
|
38
110
|
if (stateDir === "") {
|
|
39
111
|
return { ok: false, code: EX_CONFIG, reason: "missing VERAX_STATE_DIR" };
|
|
@@ -49,6 +121,8 @@ export function loadConfig(env) {
|
|
|
49
121
|
value: {
|
|
50
122
|
issuer,
|
|
51
123
|
jwksUrl,
|
|
124
|
+
jwksFile: jwksFile === "" ? null : jwksFile,
|
|
125
|
+
jwksPin,
|
|
52
126
|
audience,
|
|
53
127
|
stateDir,
|
|
54
128
|
bindHost: bind.host,
|
|
@@ -57,6 +131,12 @@ export function loadConfig(env) {
|
|
|
57
131
|
tlsTerminated,
|
|
58
132
|
inventoryFile: inventoryRaw === "" ? null : inventoryRaw,
|
|
59
133
|
downstreamFile: downstreamRaw === "" ? null : downstreamRaw,
|
|
134
|
+
allowedOrigins: (env.VERAX_ALLOWED_ORIGINS ?? "")
|
|
135
|
+
.split(",")
|
|
136
|
+
.map((item) => item.trim())
|
|
137
|
+
.filter((item) => item !== ""),
|
|
138
|
+
memoryQuotaBytes: memoryQuotaBytes(env),
|
|
139
|
+
messageQuotaBytes: messageQuotaBytes(env),
|
|
60
140
|
},
|
|
61
141
|
};
|
|
62
142
|
}
|
package/dist/demo.d.ts
CHANGED
|
@@ -14,5 +14,7 @@ export type DemoOpts = {
|
|
|
14
14
|
command: string;
|
|
15
15
|
args: string[];
|
|
16
16
|
};
|
|
17
|
+
/** mkdtemp prefix. Tests pass a unique one so parallel files do not share verax-demo-. */
|
|
18
|
+
statePrefix?: string;
|
|
17
19
|
};
|
|
18
20
|
export declare function runDemo(argv: string[], env: NodeJS.ProcessEnv, io: DemoIo, opts?: DemoOpts): Promise<number>;
|
package/dist/demo.js
CHANGED
|
@@ -229,6 +229,8 @@ async function readAnswer(stdin) {
|
|
|
229
229
|
resolve(value);
|
|
230
230
|
};
|
|
231
231
|
const onData = (chunk) => {
|
|
232
|
+
if (typeof stdin.pause === "function")
|
|
233
|
+
stdin.pause();
|
|
232
234
|
done(String(chunk).split(/\r?\n/)[0] ?? "");
|
|
233
235
|
};
|
|
234
236
|
const onEnd = () => done("");
|
|
@@ -289,7 +291,8 @@ export async function runDemo(argv, env, io, opts) {
|
|
|
289
291
|
process.once("SIGINT", onStop);
|
|
290
292
|
process.once("SIGTERM", onStop);
|
|
291
293
|
try {
|
|
292
|
-
|
|
294
|
+
const statePrefix = opts?.statePrefix && opts.statePrefix.length > 0 ? opts.statePrefix : "verax-demo-";
|
|
295
|
+
stateDir = mkdtempSync(join(tmpdir(), statePrefix));
|
|
293
296
|
const policyFile = writeDemoPolicy(stateDir, withConarium);
|
|
294
297
|
const { privateKey, publicKey } = await generateKeyPair("ES256");
|
|
295
298
|
const jwk = { ...(await exportJWK(publicKey)), alg: "ES256", use: "sig", kid: KID };
|
|
@@ -369,7 +372,10 @@ export async function runDemo(argv, env, io, opts) {
|
|
|
369
372
|
writeOut("Approve this payment as the operator on this machine? [y/N]\n");
|
|
370
373
|
const answer = await readAnswer(io.stdin);
|
|
371
374
|
if (answer.trim().toLowerCase() === "y") {
|
|
372
|
-
const code = await runApprove(["approve", stateDir, held.ref],
|
|
375
|
+
const code = await runApprove(["approve", stateDir, held.ref], writeErr, () => undefined, {
|
|
376
|
+
isTTY: io.isTTY,
|
|
377
|
+
ask: async () => readAnswer(io.stdin),
|
|
378
|
+
}, { elevated: () => false });
|
|
373
379
|
if (code !== 0)
|
|
374
380
|
throw new Error("demo: approve failed");
|
|
375
381
|
approved = true;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Env the desktop launcher sets on the issuer and the body. Absent outside that launch. */
|
|
2
|
+
export declare const DESKTOP_PARENT_PID = "VERAX_DESKTOP_PARENT_PID";
|
|
3
|
+
export type DesktopParentWatch = {
|
|
4
|
+
env?: NodeJS.ProcessEnv;
|
|
5
|
+
/** True when `pid` is still a process. Default is `process.kill(pid, 0)`. */
|
|
6
|
+
pidAlive?: (pid: number) => boolean;
|
|
7
|
+
exit?: (code: number) => void;
|
|
8
|
+
schedule?: (fn: () => void, ms: number) => {
|
|
9
|
+
unref?: () => void;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
/** `process.kill(pid, 0)` — EPERM means the process exists. */
|
|
13
|
+
export declare function desktopParentAlive(pid: number): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* When `VERAX_DESKTOP_PARENT_PID` is set, exit once that pid is gone.
|
|
16
|
+
* Polls every 2s. No-op when the variable is unset, so a normal body or issuer
|
|
17
|
+
* start does not watch anything.
|
|
18
|
+
*/
|
|
19
|
+
export declare function watchDesktopParent(hooks?: DesktopParentWatch): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Env the desktop launcher sets on the issuer and the body. Absent outside that launch. */
|
|
2
|
+
export const DESKTOP_PARENT_PID = "VERAX_DESKTOP_PARENT_PID";
|
|
3
|
+
/** `process.kill(pid, 0)` — EPERM means the process exists. */
|
|
4
|
+
export function desktopParentAlive(pid) {
|
|
5
|
+
try {
|
|
6
|
+
process.kill(pid, 0);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
return err.code === "EPERM";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* When `VERAX_DESKTOP_PARENT_PID` is set, exit once that pid is gone.
|
|
15
|
+
* Polls every 2s. No-op when the variable is unset, so a normal body or issuer
|
|
16
|
+
* start does not watch anything.
|
|
17
|
+
*/
|
|
18
|
+
export function watchDesktopParent(hooks = {}) {
|
|
19
|
+
const env = hooks.env ?? process.env;
|
|
20
|
+
const raw = env[DESKTOP_PARENT_PID]?.trim() ?? "";
|
|
21
|
+
if (raw === "")
|
|
22
|
+
return;
|
|
23
|
+
const exit = hooks.exit ?? ((code) => process.exit(code));
|
|
24
|
+
const pid = Number(raw);
|
|
25
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
26
|
+
exit(1);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const alive = hooks.pidAlive ?? desktopParentAlive;
|
|
30
|
+
if (!alive(pid)) {
|
|
31
|
+
exit(1);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const schedule = hooks.schedule ??
|
|
35
|
+
((fn, ms) => {
|
|
36
|
+
const timer = setInterval(fn, ms);
|
|
37
|
+
timer.unref();
|
|
38
|
+
return timer;
|
|
39
|
+
});
|
|
40
|
+
schedule(() => {
|
|
41
|
+
if (!alive(pid))
|
|
42
|
+
exit(1);
|
|
43
|
+
}, 2_000);
|
|
44
|
+
}
|