@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/README.md +7 -2
- package/dist/approve-cli.d.ts +22 -1
- package/dist/approve-cli.js +209 -22
- package/dist/auth.d.ts +9 -1
- package/dist/auth.js +12 -2
- 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 +227 -94
- package/dist/config.d.ts +26 -0
- package/dist/config.js +47 -0
- 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 +140 -11
- package/dist/desktop.js +931 -103
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +94 -11
- package/dist/health-extras.d.ts +4 -0
- package/dist/health-extras.js +14 -0
- package/dist/init-local.d.ts +25 -1
- package/dist/init-local.js +151 -54
- 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 +11 -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/README.md
CHANGED
|
@@ -54,8 +54,13 @@ your issuer. The tools the body offers are `memory.get`, `memory.put`,
|
|
|
54
54
|
decides which of them a token's scopes may call, and a call can be held
|
|
55
55
|
until an operator on this machine approves it.
|
|
56
56
|
|
|
57
|
-
Other commands: `
|
|
58
|
-
`unlock`, `desktop`. `verax
|
|
57
|
+
Other commands: `install`, `uninstall`, `approve`, `operator`, `reconcile`,
|
|
58
|
+
`witness`, `halt`, `unlock`, `desktop`. `verax install` and an elevated
|
|
59
|
+
`verax approve` need an administrator-owned copy of this program (Windows,
|
|
60
|
+
Administrator PowerShell: `npm install -g --prefix "$env:ProgramFiles\verax-cli" @verax-ai/body`;
|
|
61
|
+
Linux and macOS: `sudo npm install -g --prefix /opt/verax-cli @verax-ai/body`). That
|
|
62
|
+
install is the boundary against an agent that has a shell as your user;
|
|
63
|
+
`verax init --local` is not that boundary. `verax --help` lists them.
|
|
59
64
|
|
|
60
65
|
## License
|
|
61
66
|
|
package/dist/approve-cli.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ToolExec, type WindowsMachineRoots } from "./install.ts";
|
|
1
2
|
export declare function resolveApproveRef(pending: {
|
|
2
3
|
ref: string;
|
|
3
4
|
status?: string;
|
|
@@ -9,4 +10,24 @@ export declare function resolveApproveRef(pending: {
|
|
|
9
10
|
reason: "unknown-ref" | "ambiguous-ref";
|
|
10
11
|
candidates: string[];
|
|
11
12
|
};
|
|
12
|
-
export
|
|
13
|
+
export type ApproveIo = {
|
|
14
|
+
isTTY: boolean;
|
|
15
|
+
ask: (prompt: string) => Promise<string>;
|
|
16
|
+
};
|
|
17
|
+
export type ApproveHooks = {
|
|
18
|
+
elevated?: () => boolean;
|
|
19
|
+
codeProbe?: (dir: string) => boolean;
|
|
20
|
+
execPathProbe?: (file: string) => boolean;
|
|
21
|
+
execArgv?: readonly string[];
|
|
22
|
+
/**
|
|
23
|
+
* True when the invoking user can change that state directory.
|
|
24
|
+
* Absent means the real ACL or mode check. Ignored when the process is not elevated.
|
|
25
|
+
*/
|
|
26
|
+
stateProbe?: (dir: string) => boolean;
|
|
27
|
+
platform?: NodeJS.Platform;
|
|
28
|
+
env?: NodeJS.ProcessEnv;
|
|
29
|
+
exec?: ToolExec;
|
|
30
|
+
/** Machine ProgramData and Program Files. When omitted, approve reads HKLM. */
|
|
31
|
+
windowsMachineRoots?: WindowsMachineRoots;
|
|
32
|
+
};
|
|
33
|
+
export declare function runApprove(argv: string[], writeErr?: (s: string) => void, writeOut?: (s: string) => void, io?: ApproveIo, hooks?: ApproveHooks): Promise<number>;
|
package/dist/approve-cli.js
CHANGED
|
@@ -1,28 +1,46 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { userInfo } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { approvePending, approvalsLogFor, createApprovalBudgetGuard, enqueueApprovalCommand, FileLedger, loadApprovalsFromDir, loadPolicy, TERMINAL_CONTROL_CLASS, } from "@verax-ai/proxy";
|
|
6
|
+
import { EX_CONFIG } from "./config.js";
|
|
7
|
+
import { cliCodeCheckPassedAlready, defaultElevated, directoryAccess, elevatedCommandCodeRefusal, elevatedStateDirRefusal, readWindowsMachineRoots, stateDirFor, SystemToolError, unreadableSentence, windowsProgramDataRefusal } from "./install.js";
|
|
5
8
|
import { loadOrCreateSigners } from "./keys.js";
|
|
6
|
-
function
|
|
7
|
-
const fromEnv = process.env.VERAX_POLICY_FILE?.trim();
|
|
8
|
-
if (fromEnv) {
|
|
9
|
-
try {
|
|
10
|
-
return loadPolicy(readFileSync(fromEnv, "utf8"));
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
// Fall through to the snapshot the body wrote for this hash.
|
|
14
|
-
}
|
|
15
|
-
}
|
|
9
|
+
function policyFromSnapshot(stateDir, policyHash) {
|
|
16
10
|
const snap = join(stateDir, "policies", `${policyHash}.json`);
|
|
17
11
|
if (!existsSync(snap))
|
|
18
12
|
return null;
|
|
19
13
|
try {
|
|
20
|
-
|
|
14
|
+
const loaded = loadPolicy(JSON.parse(readFileSync(snap, "utf8")));
|
|
15
|
+
if (loaded.hash !== policyHash)
|
|
16
|
+
return null;
|
|
17
|
+
return loaded;
|
|
21
18
|
}
|
|
22
19
|
catch {
|
|
23
20
|
return null;
|
|
24
21
|
}
|
|
25
22
|
}
|
|
23
|
+
function policyForApprove(stateDir, policyHash, env, elevated) {
|
|
24
|
+
// An elevated approve inherits the user's environment. The snapshot the body
|
|
25
|
+
// wrote is the policy that held the call. The environment is not.
|
|
26
|
+
// When not elevated, the environment file is that policy only when its
|
|
27
|
+
// canonical hash is the defer's policyHash. Any other file falls through
|
|
28
|
+
// to the snapshot.
|
|
29
|
+
if (!elevated) {
|
|
30
|
+
const fromEnv = env.VERAX_POLICY_FILE?.trim();
|
|
31
|
+
if (fromEnv) {
|
|
32
|
+
try {
|
|
33
|
+
const loaded = loadPolicy(readFileSync(fromEnv, "utf8"));
|
|
34
|
+
if (loaded.hash === policyHash)
|
|
35
|
+
return loaded;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Fall through to the snapshot the body wrote for this hash.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return policyFromSnapshot(stateDir, policyHash);
|
|
43
|
+
}
|
|
26
44
|
function operatorName() {
|
|
27
45
|
try {
|
|
28
46
|
const name = userInfo().username;
|
|
@@ -46,15 +64,146 @@ export function resolveApproveRef(pending, given) {
|
|
|
46
64
|
return { ok: false, reason: "ambiguous-ref", candidates: unique };
|
|
47
65
|
return { ok: false, reason: "unknown-ref", candidates: [] };
|
|
48
66
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
const NEEDS_TERMINAL = "verax approve needs a terminal: it shows what is waiting and asks you to type the amount back\n";
|
|
68
|
+
function integerMinor(value) {
|
|
69
|
+
return typeof value === "number" && Number.isInteger(value) ? value : null;
|
|
70
|
+
}
|
|
71
|
+
/** An integer minor-unit amount, or null when the stored amount cannot be read as one. */
|
|
72
|
+
function minorUnits(row) {
|
|
73
|
+
if (typeof row.amount === "number")
|
|
74
|
+
return integerMinor(row.amount);
|
|
75
|
+
return integerMinor(row.args.amountMinor);
|
|
76
|
+
}
|
|
77
|
+
const HELD_CONTROL = new RegExp(`[${TERMINAL_CONTROL_CLASS}]`, "gu");
|
|
78
|
+
/**
|
|
79
|
+
* One argument per line, name and value both JSON-quoted, so neither can look like another field.
|
|
80
|
+
* JSON leaves U+0085, U+FEFF, U+2028 and U+2029 as they are, so every terminal-control code point
|
|
81
|
+
* is written as a \u escape as well.
|
|
82
|
+
*/
|
|
83
|
+
function escapeHeld(text) {
|
|
84
|
+
return text.replace(HELD_CONTROL, (ch) => `\\u${ch.codePointAt(0).toString(16).padStart(4, "0")}`);
|
|
85
|
+
}
|
|
86
|
+
function quotedField(name, value) {
|
|
87
|
+
const text = typeof value === "string" ? value : value === undefined || value === null ? "" : JSON.stringify(value);
|
|
88
|
+
const quotedName = escapeHeld(JSON.stringify(name));
|
|
89
|
+
const quoted = escapeHeld(JSON.stringify(text));
|
|
90
|
+
return `${quotedName}: ${quoted}\n`;
|
|
91
|
+
}
|
|
92
|
+
function spendPrompt(row, amount) {
|
|
93
|
+
const payee = typeof row.payee === "string" ? row.payee : String(row.args.payee ?? "");
|
|
94
|
+
const currency = typeof row.currency === "string" ? row.currency : String(row.args.currency ?? "");
|
|
95
|
+
const reference = typeof row.args.reference === "string" ? row.args.reference : "";
|
|
96
|
+
return [
|
|
97
|
+
quotedField("tool", row.subject),
|
|
98
|
+
quotedField("payee", payee),
|
|
99
|
+
quotedField("amount", String(amount)),
|
|
100
|
+
quotedField("currency", currency),
|
|
101
|
+
quotedField("reference", reference),
|
|
102
|
+
quotedField("requestHash", row.requestHash.slice(0, 12)),
|
|
103
|
+
].join("");
|
|
104
|
+
}
|
|
105
|
+
function argumentPrompt(row) {
|
|
106
|
+
const lines = [quotedField("tool", row.subject)];
|
|
107
|
+
for (const key of Object.keys(row.args).sort())
|
|
108
|
+
lines.push(quotedField(key, row.args[key]));
|
|
109
|
+
return lines.join("");
|
|
110
|
+
}
|
|
111
|
+
function readTypedAmount() {
|
|
112
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
rl.once("line", (line) => {
|
|
115
|
+
rl.close();
|
|
116
|
+
resolve(line);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function defaultApproveIo() {
|
|
121
|
+
return {
|
|
122
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
123
|
+
ask: () => readTypedAmount(),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export async function runApprove(argv, writeErr = (s) => process.stderr.write(s), writeOut = (s) => process.stdout.write(s), io, hooks) {
|
|
127
|
+
const platform = hooks?.platform ?? process.platform;
|
|
128
|
+
const env = hooks?.env ?? process.env;
|
|
129
|
+
let isElevated = false;
|
|
130
|
+
try {
|
|
131
|
+
isElevated = hooks?.elevated ? hooks.elevated() : defaultElevated(platform);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (err instanceof SystemToolError) {
|
|
135
|
+
writeErr(`${err.message}\n`);
|
|
136
|
+
return EX_CONFIG;
|
|
137
|
+
}
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
if (isElevated && !cliCodeCheckPassedAlready()) {
|
|
141
|
+
const refusal = elevatedCommandCodeRefusal(platform, env, hooks?.codeProbe, {
|
|
142
|
+
execPathProbe: hooks?.execPathProbe,
|
|
143
|
+
execArgv: hooks?.execArgv,
|
|
144
|
+
});
|
|
145
|
+
if (refusal) {
|
|
146
|
+
writeErr(refusal.endsWith("\n") ? refusal : `${refusal}\n`);
|
|
147
|
+
return EX_CONFIG;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const terminal = io ?? defaultApproveIo();
|
|
151
|
+
const rest = argv.slice(1).filter((a) => a !== "--from-script");
|
|
152
|
+
const fromScript = argv.includes("--from-script");
|
|
153
|
+
let stateDir = rest[0];
|
|
154
|
+
let given = rest[1];
|
|
155
|
+
if (rest.length === 1 && stateDir) {
|
|
156
|
+
let installed;
|
|
157
|
+
try {
|
|
158
|
+
let machine = hooks?.windowsMachineRoots;
|
|
159
|
+
if (platform === "win32" && !machine)
|
|
160
|
+
machine = readWindowsMachineRoots(hooks?.exec);
|
|
161
|
+
if (platform === "win32") {
|
|
162
|
+
const rootRefusal = windowsProgramDataRefusal(env, hooks?.exec, machine);
|
|
163
|
+
if (rootRefusal) {
|
|
164
|
+
writeErr(`${rootRefusal}\n`);
|
|
165
|
+
return 78;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
installed = stateDirFor(platform, env, undefined, machine);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
writeErr(`${err instanceof SystemToolError ? err.message : "refusing install root"}\n`);
|
|
172
|
+
return 78;
|
|
173
|
+
}
|
|
174
|
+
const access = directoryAccess(installed);
|
|
175
|
+
if (access === "unreadable") {
|
|
176
|
+
writeErr(`${unreadableSentence(installed, "approve")}\n`);
|
|
177
|
+
return 77;
|
|
178
|
+
}
|
|
179
|
+
if (access === "ok") {
|
|
180
|
+
given = stateDir;
|
|
181
|
+
stateDir = installed;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (!stateDir || !given || (rest.length !== 2 && !(rest.length === 1 && stateDir && given))) {
|
|
54
185
|
writeErr("verax approve <stateDir> <ref>\n");
|
|
55
186
|
return 78;
|
|
56
187
|
}
|
|
57
|
-
|
|
188
|
+
if (isElevated) {
|
|
189
|
+
const owned = elevatedStateDirRefusal(stateDir, platform, env, hooks?.stateProbe);
|
|
190
|
+
if (owned) {
|
|
191
|
+
writeErr(`${owned}\n`);
|
|
192
|
+
return 78;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (directoryAccess(stateDir) === "unreadable") {
|
|
196
|
+
writeErr(`${unreadableSentence(stateDir, "approve")}\n`);
|
|
197
|
+
return 77;
|
|
198
|
+
}
|
|
199
|
+
// A non-interactive shell can read the state directory. Without a person
|
|
200
|
+
// at a terminal, or an explicit --from-script, nothing is approved.
|
|
201
|
+
if (!fromScript && !terminal.isTTY) {
|
|
202
|
+
writeErr(NEEDS_TERMINAL);
|
|
203
|
+
return 78;
|
|
204
|
+
}
|
|
205
|
+
const rows = loadApprovalsFromDir(stateDir);
|
|
206
|
+
const resolved = resolveApproveRef(rows, given);
|
|
58
207
|
if (!resolved.ok) {
|
|
59
208
|
if (resolved.reason === "ambiguous-ref") {
|
|
60
209
|
writeErr(`ambiguous-ref\n${resolved.candidates.join("\n")}\n`);
|
|
@@ -64,6 +213,39 @@ export async function runApprove(argv, writeErr = (s) => process.stderr.write(s)
|
|
|
64
213
|
return 78;
|
|
65
214
|
}
|
|
66
215
|
const ref = resolved.ref;
|
|
216
|
+
const via = fromScript ? "cli-script" : "cli";
|
|
217
|
+
// The amount is checked before the ledger is opened. A running body holds
|
|
218
|
+
// the lock, and queueing first would approve without the person ever typing it.
|
|
219
|
+
if (!fromScript) {
|
|
220
|
+
const waiting = rows.find((row) => row.ref === ref);
|
|
221
|
+
if (!waiting) {
|
|
222
|
+
writeErr("approve-unknown-ref\n");
|
|
223
|
+
return 78;
|
|
224
|
+
}
|
|
225
|
+
if (waiting.subject === "spend") {
|
|
226
|
+
const expected = minorUnits(waiting);
|
|
227
|
+
if (expected === null) {
|
|
228
|
+
writeErr("approve-amount-unreadable\n");
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
writeOut(spendPrompt(waiting, expected));
|
|
232
|
+
writeOut("Type the amount in minor units:\n");
|
|
233
|
+
const typed = (await terminal.ask("Type the amount in minor units:\n")).trim();
|
|
234
|
+
if (typed !== String(expected)) {
|
|
235
|
+
writeErr("approve-amount-mismatch\n");
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
writeOut(argumentPrompt(waiting));
|
|
241
|
+
writeOut("Type yes:\n");
|
|
242
|
+
const typed = (await terminal.ask("Type yes:\n")).trim();
|
|
243
|
+
if (typed !== "yes") {
|
|
244
|
+
writeErr("approve-confirm-mismatch\n");
|
|
245
|
+
return 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
67
249
|
let ledger;
|
|
68
250
|
try {
|
|
69
251
|
ledger = new FileLedger(stateDir);
|
|
@@ -71,7 +253,7 @@ export async function runApprove(argv, writeErr = (s) => process.stderr.write(s)
|
|
|
71
253
|
catch (err) {
|
|
72
254
|
const msg = err instanceof Error ? err.message : "";
|
|
73
255
|
if (msg.startsWith("ledger-locked")) {
|
|
74
|
-
enqueueApprovalCommand(stateDir, { ref, approverId: operatorName(), atMs: Date.now() });
|
|
256
|
+
enqueueApprovalCommand(stateDir, { ref, approverId: operatorName(), atMs: Date.now(), via });
|
|
75
257
|
writeOut("approve-queued\n");
|
|
76
258
|
return 0;
|
|
77
259
|
}
|
|
@@ -87,7 +269,12 @@ export async function runApprove(argv, writeErr = (s) => process.stderr.write(s)
|
|
|
87
269
|
writeErr("approve-unknown-ref\n");
|
|
88
270
|
return 78;
|
|
89
271
|
}
|
|
90
|
-
const policy = policyForApprove(stateDir, defer.claims.policyHash);
|
|
272
|
+
const policy = policyForApprove(stateDir, defer.claims.policyHash, env, isElevated);
|
|
273
|
+
const waitingSubject = rows.find((row) => row.ref === ref)?.subject;
|
|
274
|
+
if (!policy && waitingSubject === "spend") {
|
|
275
|
+
writeErr("approve-policy-missing\n");
|
|
276
|
+
return 1;
|
|
277
|
+
}
|
|
91
278
|
const result = await approvePending({
|
|
92
279
|
ledger,
|
|
93
280
|
recordSigner: signers.recordSigner,
|
|
@@ -95,11 +282,11 @@ export async function runApprove(argv, writeErr = (s) => process.stderr.write(s)
|
|
|
95
282
|
nonce: () => crypto.randomUUID(),
|
|
96
283
|
ref,
|
|
97
284
|
approverId: operatorName(),
|
|
98
|
-
via
|
|
285
|
+
via,
|
|
99
286
|
policyHash: defer.claims.policyHash,
|
|
100
287
|
approvals,
|
|
101
288
|
...(policy
|
|
102
|
-
? { budgetGuard: createApprovalBudgetGuard({ policy, approvals, now: () => Date.now() }) }
|
|
289
|
+
? { budgetGuard: createApprovalBudgetGuard({ policy, approvals, now: () => Date.now(), ledger }) }
|
|
103
290
|
: {}),
|
|
104
291
|
});
|
|
105
292
|
if (!result.ok) {
|
package/dist/auth.d.ts
CHANGED
|
@@ -10,5 +10,13 @@ export declare class JwksFileError extends Error {
|
|
|
10
10
|
readonly code = 78;
|
|
11
11
|
constructor(message?: string);
|
|
12
12
|
}
|
|
13
|
-
export declare function createVerifier(jwksUrl: string, issuer: string, audience: string, jwksFile?: string | null
|
|
13
|
+
export declare function createVerifier(jwksUrl: string, issuer: string, audience: string, jwksFile?: string | null,
|
|
14
|
+
/**
|
|
15
|
+
* Keys fetched once, before this process started accepting tokens.
|
|
16
|
+
* `createLocalJWKSet` does not refetch. A `kid` that was not in this set
|
|
17
|
+
* fails closed. This is not `VERAX_JWKS_FILE`: that mode refuses operator scopes.
|
|
18
|
+
*/
|
|
19
|
+
jwksPin?: {
|
|
20
|
+
keys: Record<string, unknown>[];
|
|
21
|
+
} | null): (token: string) => Promise<VerifiedBearer>;
|
|
14
22
|
export declare function readBearer(header: string | undefined): string | null;
|
package/dist/auth.js
CHANGED
|
@@ -35,8 +35,18 @@ export class JwksFileError extends Error {
|
|
|
35
35
|
this.name = "JwksFileError";
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
export function createVerifier(jwksUrl, issuer, audience, jwksFile
|
|
39
|
-
|
|
38
|
+
export function createVerifier(jwksUrl, issuer, audience, jwksFile,
|
|
39
|
+
/**
|
|
40
|
+
* Keys fetched once, before this process started accepting tokens.
|
|
41
|
+
* `createLocalJWKSet` does not refetch. A `kid` that was not in this set
|
|
42
|
+
* fails closed. This is not `VERAX_JWKS_FILE`: that mode refuses operator scopes.
|
|
43
|
+
*/
|
|
44
|
+
jwksPin) {
|
|
45
|
+
const jwks = jwksFile
|
|
46
|
+
? localJwks(jwksFile)
|
|
47
|
+
: jwksPin
|
|
48
|
+
? createLocalJWKSet(jwksPin)
|
|
49
|
+
: createRemoteJWKSet(new URL(jwksUrl));
|
|
40
50
|
return async (token) => {
|
|
41
51
|
const { payload } = await jwtVerify(token, jwks, {
|
|
42
52
|
issuer,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append this process's stdout and stderr to `file`.
|
|
3
|
+
* POSIX: the file is mode 0600 (umask cannot widen it).
|
|
4
|
+
* Windows: CreateFile sets no explicit DACL, so the new file inherits the
|
|
5
|
+
* parent directory ACL. The state directory's service ACL is that parent.
|
|
6
|
+
*/
|
|
7
|
+
export declare function attachBodyLog(file: string): {
|
|
8
|
+
ok: true;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
reason: string;
|
|
12
|
+
};
|
|
13
|
+
/** Last `n` lines, or null when the file cannot be read. A trailing newline is not an extra line. */
|
|
14
|
+
export declare function readLogTail(file: string, n: number): string | null;
|
package/dist/body-log.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { chmodSync, openSync, readFileSync, writeSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Append this process's stdout and stderr to `file`.
|
|
4
|
+
* POSIX: the file is mode 0600 (umask cannot widen it).
|
|
5
|
+
* Windows: CreateFile sets no explicit DACL, so the new file inherits the
|
|
6
|
+
* parent directory ACL. The state directory's service ACL is that parent.
|
|
7
|
+
*/
|
|
8
|
+
export function attachBodyLog(file) {
|
|
9
|
+
let fd;
|
|
10
|
+
try {
|
|
11
|
+
fd = openSync(file, "a", 0o600);
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
return { ok: false, reason: `cannot open log file ${file}: ${err.message}` };
|
|
15
|
+
}
|
|
16
|
+
if (process.platform !== "win32") {
|
|
17
|
+
try {
|
|
18
|
+
chmodSync(file, 0o600);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// The platform does not honour the mode bit.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const append = (chunk, encoding) => {
|
|
25
|
+
try {
|
|
26
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk, encoding ?? "utf8") : Buffer.from(chunk);
|
|
27
|
+
writeSync(fd, buf);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// A full disk must not hide the line already going to the console.
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
tee(process.stdout, append);
|
|
34
|
+
tee(process.stderr, append);
|
|
35
|
+
return { ok: true };
|
|
36
|
+
}
|
|
37
|
+
function tee(stream, append) {
|
|
38
|
+
const original = stream.write.bind(stream);
|
|
39
|
+
stream.write = ((chunk, encoding, cb) => {
|
|
40
|
+
if (typeof encoding === "function") {
|
|
41
|
+
append(chunk);
|
|
42
|
+
return original(chunk, encoding);
|
|
43
|
+
}
|
|
44
|
+
append(chunk, encoding);
|
|
45
|
+
return original(chunk, encoding, cb);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Last `n` lines, or null when the file cannot be read. A trailing newline is not an extra line. */
|
|
49
|
+
export function readLogTail(file, n) {
|
|
50
|
+
try {
|
|
51
|
+
const text = readFileSync(file, "utf8");
|
|
52
|
+
const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
53
|
+
if (lines.length > 0 && lines[lines.length - 1] === "")
|
|
54
|
+
lines.pop();
|
|
55
|
+
const tail = lines.slice(-n).join("\n");
|
|
56
|
+
return tail === "" ? "" : `${tail}\n`;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,2 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export {
|
|
2
|
+
export type CliHooks = {
|
|
3
|
+
elevated?: () => boolean;
|
|
4
|
+
codeProbe?: (dir: string) => boolean;
|
|
5
|
+
/** True when that Node path can be changed by the invoking user. See `NodeTrustProbe`. */
|
|
6
|
+
execPathProbe?: (file: string) => boolean;
|
|
7
|
+
/** Defaults to `process.execArgv` for the elevated preload check. */
|
|
8
|
+
execArgv?: readonly string[];
|
|
9
|
+
platform?: NodeJS.Platform;
|
|
10
|
+
env?: NodeJS.ProcessEnv;
|
|
11
|
+
stdout?: {
|
|
12
|
+
write(s: string): unknown;
|
|
13
|
+
};
|
|
14
|
+
stderr?: {
|
|
15
|
+
write(s: string): unknown;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* One gate for every command except `--help` and `--version`. An elevated
|
|
20
|
+
* process refuses code the invoking user can change before the command runs.
|
|
21
|
+
* Install and approve keep their own check for direct callers; the flag stops
|
|
22
|
+
* a second check in this process.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runCli(argv: string[], hooks?: CliHooks): Promise<number>;
|