@trucore/atf 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +260 -0
  3. package/dist/index.js +1073 -0
  4. package/package.json +47 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TruCore AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,260 @@
1
+ # @trucore/atf
2
+
3
+ > Agent Transaction Firewall CLI — simulate, verify, and audit on-chain transactions trustlessly.
4
+
5
+ ## One-Liner
6
+
7
+ ```bash
8
+ npx @trucore/atf@1.0.0 simulate --preset swap_small --verify --pretty
9
+ ```
10
+
11
+ That's it. No install, no config, no API key required.
12
+
13
+ ## What It Does
14
+
15
+ 1. Sends a deterministic preset transaction to the ATF public API
16
+ 2. Returns **allowed** or **denied** with a reason and receipt hash
17
+ 3. Verifies **content_hash** integrity client-side — don't trust TruCore, verify
18
+
19
+ ## Install (optional)
20
+
21
+ ```bash
22
+ npm install -g @trucore/atf@1.0.0
23
+ ```
24
+
25
+ Or use directly with `npx` (always pin the version):
26
+
27
+ ```bash
28
+ npx @trucore/atf@1.0.0 simulate --preset swap_small
29
+ ```
30
+
31
+ ## Commands
32
+
33
+ ### `health`
34
+
35
+ Check API health and round-trip latency.
36
+
37
+ ```bash
38
+ npx @trucore/atf@1.0.0 health
39
+ npx @trucore/atf@1.0.0 health --pretty
40
+ npx @trucore/atf@1.0.0 health --base-url http://localhost:3000
41
+ ```
42
+
43
+ **Output shape:**
44
+
45
+ ```json
46
+ { "ok": true, "baseUrl": "https://api.trucore.xyz", "latency_ms": 42, "response": { "status": "ok" } }
47
+ ```
48
+
49
+ ### `simulate`
50
+
51
+ Run a transaction simulation against the ATF API.
52
+
53
+ ```bash
54
+ npx @trucore/atf@1.0.0 simulate --preset swap_small --verify
55
+ npx @trucore/atf@1.0.0 simulate --preset swap_too_large --pretty
56
+ npx @trucore/atf@1.0.0 simulate --json '{"chain_id":1,"value_eth":"0.5"}' --base-url http://localhost:3000
57
+ ```
58
+
59
+ **Output shape:**
60
+
61
+ ```json
62
+ { "ok": true, "baseUrl": "https://api.trucore.xyz", "preset": "swap_small", "verified": true, "response": { "decision": "allowed", "reasons": [], "receipt_hash": "a1b2...64hex", "content_hash": "c3d4...64hex" } }
63
+ ```
64
+
65
+ ### `approve`
66
+
67
+ Approve a pending intent (requires authentication).
68
+
69
+ ```bash
70
+ npx @trucore/atf@1.0.0 approve --intent abc123 --token mytoken
71
+ npx @trucore/atf@1.0.0 approve --intent abc123 --token mytoken --pretty
72
+ ```
73
+
74
+ **Output shape:**
75
+
76
+ ```json
77
+ { "ok": true, "baseUrl": "https://api.trucore.xyz", "intent": "abc123", "response": { "intent_id": "abc123", "status": "approved" } }
78
+ ```
79
+
80
+ ### `version`
81
+
82
+ Show rich version information (CLI version, Node version, build info).
83
+
84
+ ```bash
85
+ npx @trucore/atf@1.0.0 version
86
+ npx @trucore/atf@1.0.0 version --pretty
87
+ ```
88
+
89
+ **Output shape:**
90
+
91
+ ```json
92
+ { "ok": true, "cli_version": "1.0.0", "node_version": "v22.0.0", "platform": "linux", "arch": "x64", "default_base_url": "https://api.trucore.xyz", "build_commit": "abc1234", "build_date": "2025-01-01T00:00:00Z" }
93
+ ```
94
+
95
+ ## Global Options
96
+
97
+ | Flag | Description | Default |
98
+ |------|-------------|---------|
99
+ | `--base-url <url>` | API base URL | `https://api.trucore.xyz` |
100
+ | `--timeout-ms <ms>` | Request timeout in milliseconds | `20000` |
101
+ | `--format <fmt>` | Output: `json` or `pretty` | `json` |
102
+ | `--pretty` | Shorthand for `--format pretty` | `false` |
103
+ | `--no-color` | Disable ANSI colors | `false` |
104
+ | `--api-key <key>` | API key (also: `ATF_API_KEY` env var) | — |
105
+
106
+ ## Simulate Options
107
+
108
+ | Flag | Description | Default |
109
+ |------|-------------|---------|
110
+ | `--preset <name>` | Built-in preset: `swap_small`, `swap_too_large`, `ttl_too_high` | — |
111
+ | `--json '<json>'` | Raw JSON transaction body | — |
112
+ | `--verify` | Verify content_hash integrity | `false` |
113
+ | `--quiet` | Suppress non-essential output | `false` |
114
+
115
+ ## Approve Options
116
+
117
+ | Flag | Description | Default |
118
+ |------|-------------|---------|
119
+ | `--intent <id>` | Intent ID to approve (required) | — |
120
+ | `--intent-id <id>` | Alias for `--intent` (backward compat) | — |
121
+ | `--token <bearer>` | Bearer token for auth (also: `ATF_API_KEY` env var) | — |
122
+
123
+ ### Presets
124
+
125
+ | Preset | Description | Expected |
126
+ |--------|-------------|----------|
127
+ | `swap_small` | Small 0.01 ETH swap | ALLOWED |
128
+ | `swap_too_large` | 999,999 ETH swap | DENIED (value limit) |
129
+ | `ttl_too_high` | Swap with 24h TTL | DENIED (TTL policy) |
130
+
131
+ ## Output
132
+
133
+ All commands return a JSON envelope with `"ok": true/false`.
134
+
135
+ ### Success (JSON, default)
136
+
137
+ ```json
138
+ {
139
+ "ok": true,
140
+ "baseUrl": "https://api.trucore.xyz",
141
+ "preset": "swap_small",
142
+ "verified": true,
143
+ "response": {
144
+ "decision": "allowed",
145
+ "reasons": [],
146
+ "receipt_hash": "a1b2c3...64hex",
147
+ "content_hash": "d4e5f6...64hex"
148
+ }
149
+ }
150
+ ```
151
+
152
+ When `--verify` is not used, the `verified` field is `null`.
153
+ When a preset is not used, the `preset` field is `null`.
154
+ ```
155
+
156
+ ### Success (Pretty, `--pretty`)
157
+
158
+ ```
159
+ ✓ ALLOWED
160
+ Reason: All policies passed
161
+ Receipt: a1b2c3...64hex
162
+
163
+ ✓ Content hash verified
164
+ ```
165
+
166
+ ### Errors
167
+
168
+ All errors return a structured JSON envelope:
169
+
170
+ ```json
171
+ {
172
+ "ok": false,
173
+ "error": {
174
+ "code": "NETWORK_ERROR",
175
+ "message": "Network failure — fetch failed",
176
+ "details": {
177
+ "hint": "Check your network connection and base URL."
178
+ }
179
+ }
180
+ }
181
+ ```
182
+
183
+ Error codes: `USER_ERROR`, `DENIED`, `NETWORK_ERROR`, `SERVER_ERROR`, `VALIDATION_ERROR`, `AUTH_ERROR`, `VERIFY_FAILED`.
184
+
185
+ ## Exit Codes
186
+
187
+ | Code | Meaning |
188
+ |------|---------|
189
+ | 0 | Success (allowed, healthy, approved, etc.) |
190
+ | 1 | User error or denied (bad input, auth failure, transaction denied, verify failed) |
191
+ | 2 | Network / server error (unreachable, 5xx, timeout, rate limited) |
192
+
193
+ ## Trustless Verification
194
+
195
+ **Don't trust TruCore — verify.**
196
+
197
+ Every simulation produces a `content_hash` (SHA-256). You can:
198
+
199
+ 1. **Use `--verify` flag:** Automatically recomputes and checks content_hash integrity client-side.
200
+ 2. **Recompute locally:** The policy engine is deterministic. Same input → same hash.
201
+ 3. **Check receipt signatures** (when available): `GET /api/receipt-signing-key`
202
+
203
+ The `--verify` flag checks content_hash integrity automatically. On mismatch it exits 1 with `VERIFY_FAILED`.
204
+
205
+ ## Token Redaction
206
+
207
+ Bearer tokens are **never** emitted in CLI output. All stdout/stderr is scrubbed
208
+ for token values and `Bearer <token>` patterns before printing.
209
+
210
+ ## Environment Variables
211
+
212
+ | Variable | Description |
213
+ |----------|-------------|
214
+ | `ATF_API_KEY` | API key (sent as `x-api-key` header or Bearer token) |
215
+ | `ATF_BASE_URL` | Override default base URL |
216
+ | `ATF_TIMEOUT_MS` | Override default timeout (milliseconds, default: `20000`) |
217
+ | `NO_COLOR` | Disable ANSI colors when set |
218
+
219
+ ## Requirements
220
+
221
+ - Node.js 18+
222
+ - **Zero** runtime dependencies
223
+
224
+ ## Development
225
+
226
+ ```bash
227
+ # Build
228
+ node build.mjs
229
+
230
+ # Test (all offline — no network calls)
231
+ node --test tests/*.mjs
232
+
233
+ # Pack (dry run — verify included files)
234
+ npm pack --dry-run
235
+ ```
236
+
237
+ ## Publishing
238
+
239
+ ```bash
240
+ # 1. Build
241
+ node build.mjs
242
+
243
+ # 2. Run all tests
244
+ node --test tests/*.mjs
245
+
246
+ # 3. Inspect the tarball
247
+ npm pack --dry-run
248
+ # Expected files: LICENSE, README.md, dist/index.js, package.json
249
+
250
+ # 4. Publish to npm (requires npm login)
251
+ npm publish --access public
252
+
253
+ # 5. Verify install (from a clean machine or CI)
254
+ npx @trucore/atf@1.0.0 version
255
+ npx @trucore/atf@1.0.0 health
256
+ ```
257
+
258
+ ## License
259
+
260
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,1073 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // @trucore/atf v1.0.0 — Agent Transaction Firewall CLI
5
+ // Built: 2026-02-27T22:14:44.003Z
6
+ // Commit: 809c30e
7
+
8
+ // ---- src/constants.mjs ----
9
+ /**
10
+ * constants.mjs — shared constants for the ATF CLI
11
+ *
12
+ * Build-time placeholders (__BUILD_COMMIT__, __BUILD_DATE__) are replaced
13
+ * by build.mjs during the bundling step.
14
+ */
15
+
16
+ const VERSION = "1.0.0";
17
+ const DEFAULT_BASE_URL = "https://api.trucore.xyz";
18
+ const BUILD_COMMIT = "809c30e";
19
+ const BUILD_DATE = "2026-02-27T22:14:44.003Z";
20
+ const SIMULATE_PATHS = ["/api/simulate", "/v1/simulate"];
21
+
22
+ // ---- src/redact.mjs ----
23
+ /**
24
+ * redact.mjs — token redaction utilities
25
+ *
26
+ * Ensures secrets (Bearer tokens, API keys) never appear in CLI output.
27
+ * Used by approve, error handling, and the global catch handler.
28
+ */
29
+
30
+ const BEARER_PATTERN = /Bearer\s+[A-Za-z0-9\-\._~\+\/]+=*/g;
31
+
32
+ /**
33
+ * Replace any occurrence of `token` with ***REDACTED***.
34
+ * Also scrub Bearer patterns for defense-in-depth.
35
+ */
36
+ function redactToken(str, token) {
37
+ if (!str || typeof str !== "string") return str || "";
38
+ let result = str;
39
+ if (token && typeof token === "string" && token.length > 0) {
40
+ // split+join: safe literal replacement (no regex special-char issues)
41
+ result = result.split(token).join("***REDACTED***");
42
+ }
43
+ result = result.replace(BEARER_PATTERN, "Bearer ***REDACTED***");
44
+ return result;
45
+ }
46
+
47
+ // ---- src/presets.mjs ----
48
+ /**
49
+ * presets.mjs — deterministic simulation presets
50
+ */
51
+
52
+ const PRESETS = {
53
+ swap_small: {
54
+ description: "Small token swap (should be ALLOWED)",
55
+ transaction: {
56
+ chain_id: 1,
57
+ from_address: "0x1234567890abcdef1234567890abcdef12345678",
58
+ to_address: "0xdef1c0ded9bec7f1a1670819833240f027b25eff",
59
+ value_eth: "0.01",
60
+ function_name: "swap",
61
+ protocol: "0x",
62
+ calldata_summary: "swap exactInputSingle 0.01 ETH -> USDC",
63
+ estimated_gas: 150000,
64
+ },
65
+ },
66
+ swap_too_large: {
67
+ description: "Oversized swap (should be DENIED by value limit)",
68
+ transaction: {
69
+ chain_id: 1,
70
+ from_address: "0x1234567890abcdef1234567890abcdef12345678",
71
+ to_address: "0xdef1c0ded9bec7f1a1670819833240f027b25eff",
72
+ value_eth: "999999.0",
73
+ function_name: "swap",
74
+ protocol: "0x",
75
+ calldata_summary: "swap exactInputSingle 999999 ETH -> USDC",
76
+ estimated_gas: 150000,
77
+ },
78
+ },
79
+ ttl_too_high: {
80
+ description: "Transaction with excessive TTL (should be DENIED by TTL policy)",
81
+ transaction: {
82
+ chain_id: 1,
83
+ from_address: "0x1234567890abcdef1234567890abcdef12345678",
84
+ to_address: "0xdef1c0ded9bec7f1a1670819833240f027b25eff",
85
+ value_eth: "0.01",
86
+ function_name: "swap",
87
+ protocol: "0x",
88
+ calldata_summary: "swap with TTL=86400",
89
+ estimated_gas: 150000,
90
+ ttl_seconds: 86400,
91
+ },
92
+ },
93
+ };
94
+
95
+ const PRESET_NAMES = Object.keys(PRESETS);
96
+
97
+ // ---- src/validate.mjs ----
98
+ /**
99
+ * validate.mjs — receipt hash and input validation
100
+ */
101
+
102
+ const RECEIPT_HASH_RE = /^[0-9a-f]{64}$/;
103
+
104
+ function isValidReceiptHash(hash) {
105
+ return typeof hash === "string" && RECEIPT_HASH_RE.test(hash);
106
+ }
107
+
108
+ function validatePreset(name) {
109
+ if (!PRESETS[name]) {
110
+ return {
111
+ ok: false,
112
+ message: `Unknown preset "${name}". Available: ${PRESET_NAMES.join(", ")}`,
113
+ };
114
+ }
115
+ return { ok: true };
116
+ }
117
+
118
+ /**
119
+ * Recursively sort object keys for canonical JSON serialization.
120
+ * Arrays are left in-order; non-objects pass through.
121
+ */
122
+ function sortKeysDeep(obj) {
123
+ if (obj === null || typeof obj !== "object") return obj;
124
+ if (Array.isArray(obj)) return obj.map(sortKeysDeep);
125
+ const sorted = {};
126
+ for (const k of Object.keys(obj).sort()) sorted[k] = sortKeysDeep(obj[k]);
127
+ return sorted;
128
+ }
129
+
130
+ /**
131
+ * Compute a deterministic content hash from the canonical payload.
132
+ * SHA-256 over canonicalized JSON (sorted keys recursively, compact separators, UTF-8).
133
+ *
134
+ * Canonical payload fields (MUST match Python exactly):
135
+ * - decision (lowercase string)
136
+ * - reasons (array of strings — order preserved)
137
+ * - policy_hash (string) — omit if null/empty
138
+ * - params (object) — omit if null/empty
139
+ *
140
+ * Used by --verify to detect response tampering.
141
+ */
142
+ function computeContentHash(decision, reasons, policyHash, params) {
143
+ // require used inline — safe in the CJS bundle (dist/index.js)
144
+ const { createHash } = require("node:crypto");
145
+ const payload = {
146
+ decision: (decision || "").toLowerCase(),
147
+ reasons: Array.isArray(reasons) ? reasons : [],
148
+ };
149
+ if (policyHash !== undefined && policyHash !== null && policyHash !== "") {
150
+ payload.policy_hash = policyHash;
151
+ }
152
+ if (params !== undefined && params !== null && typeof params === "object" && Object.keys(params).length > 0) {
153
+ payload.params = params;
154
+ }
155
+ const canonical = JSON.stringify(sortKeysDeep(payload));
156
+ return createHash("sha256").update(canonical, "utf8").digest("hex");
157
+ }
158
+
159
+ /**
160
+ * Legacy content hash — decision + reason only (for backward compat).
161
+ */
162
+ function computeLegacyContentHash(decision, reason) {
163
+ const { createHash } = require("node:crypto");
164
+ const payload = { decision: (decision || "").toLowerCase(), reason: reason || "" };
165
+ const keys = Object.keys(payload).sort();
166
+ const sorted = {};
167
+ for (const k of keys) sorted[k] = payload[k];
168
+ const canonical = JSON.stringify(sorted);
169
+ return createHash("sha256").update(canonical, "utf8").digest("hex");
170
+ }
171
+
172
+ function parseJsonBody(raw) {
173
+ try {
174
+ const parsed = JSON.parse(raw);
175
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
176
+ return { ok: false, message: "JSON body must be an object" };
177
+ }
178
+ return { ok: true, body: parsed };
179
+ } catch (e) {
180
+ return { ok: false, message: `Invalid JSON: ${e.message}` };
181
+ }
182
+ }
183
+
184
+ // ---- src/http.mjs ----
185
+ /**
186
+ * http.mjs — minimal HTTP helpers using Node 18+ built-in fetch.
187
+ * Zero runtime dependencies.
188
+ */
189
+
190
+ const DEFAULT_TIMEOUT_MS = 20_000;
191
+ const MAX_TEXT_CAPTURE = 4096;
192
+
193
+ function resolveTimeout(overrideMs) {
194
+ if (overrideMs && overrideMs > 0) return overrideMs;
195
+ const envMs = process.env.ATF_TIMEOUT_MS;
196
+ if (envMs) {
197
+ const parsed = parseInt(envMs, 10);
198
+ if (parsed > 0) return parsed;
199
+ }
200
+ return DEFAULT_TIMEOUT_MS;
201
+ }
202
+
203
+ function generateRequestId() {
204
+ const { randomBytes } = require("node:crypto");
205
+ return randomBytes(8).toString("hex");
206
+ }
207
+
208
+ function stripTrailingSlash(url) {
209
+ return url.replace(/\/+$/, "");
210
+ }
211
+
212
+ async function getHealth(baseUrl, timeoutMs) {
213
+ const timeout = resolveTimeout(timeoutMs);
214
+ const requestId = generateRequestId();
215
+ const url = `${stripTrailingSlash(baseUrl)}/health`;
216
+ const res = await fetch(url, {
217
+ headers: { "X-Request-ID": requestId },
218
+ signal: AbortSignal.timeout(timeout),
219
+ });
220
+ const parsed = await parseResponse(res);
221
+ parsed.requestId = requestId;
222
+ return parsed;
223
+ }
224
+
225
+ async function postSimulate(baseUrl, body, apiKey, timeoutMs, path) {
226
+ const timeout = resolveTimeout(timeoutMs);
227
+ const requestId = generateRequestId();
228
+ const url = `${stripTrailingSlash(baseUrl)}${path || "/api/simulate"}`;
229
+ const headers = {
230
+ "Content-Type": "application/json",
231
+ "X-Request-ID": requestId,
232
+ };
233
+ if (apiKey) headers["x-api-key"] = apiKey;
234
+
235
+ const res = await fetch(url, {
236
+ method: "POST",
237
+ headers,
238
+ body: JSON.stringify(body),
239
+ signal: AbortSignal.timeout(timeout),
240
+ });
241
+ const parsed = await parseResponse(res);
242
+ parsed.requestId = requestId;
243
+ return parsed;
244
+ }
245
+
246
+ async function postApprove(baseUrl, intentId, token, timeoutMs) {
247
+ const timeout = resolveTimeout(timeoutMs);
248
+ const requestId = generateRequestId();
249
+ const url = `${stripTrailingSlash(baseUrl)}/v1/intents/approve`;
250
+ const headers = {
251
+ "Content-Type": "application/json",
252
+ Authorization: `Bearer ${token}`,
253
+ "X-Request-ID": requestId,
254
+ };
255
+ const res = await fetch(url, {
256
+ method: "POST",
257
+ headers,
258
+ body: JSON.stringify({ intent_id: intentId }),
259
+ signal: AbortSignal.timeout(timeout),
260
+ });
261
+ const parsed = await parseResponse(res);
262
+ parsed.requestId = requestId;
263
+ return parsed;
264
+ }
265
+
266
+ async function getReceiptSigningKey(baseUrl) {
267
+ const url = `${stripTrailingSlash(baseUrl)}/api/receipt-signing-key`;
268
+ try {
269
+ const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
270
+ if (!res.ok) return { available: false };
271
+ const data = await res.json();
272
+ return data;
273
+ } catch {
274
+ return { available: false };
275
+ }
276
+ }
277
+
278
+ async function parseResponse(res) {
279
+ const rateLimitHeaders = {};
280
+ for (const h of ["x-ratelimit-limit", "x-ratelimit-remaining", "x-ratelimit-reset"]) {
281
+ const v = res.headers.get(h);
282
+ if (v !== null) rateLimitHeaders[h] = v;
283
+ }
284
+
285
+ let data;
286
+ const rawText = await res.text();
287
+ const text = rawText.slice(0, MAX_TEXT_CAPTURE);
288
+ try {
289
+ data = JSON.parse(text);
290
+ } catch {
291
+ data = null;
292
+ }
293
+
294
+ return {
295
+ ok: res.ok,
296
+ status: res.status,
297
+ data,
298
+ text,
299
+ rateLimitHeaders,
300
+ retryAfter: res.headers.get("retry-after"),
301
+ };
302
+ }
303
+
304
+ // ---- src/format.mjs ----
305
+ /**
306
+ * format.mjs — shared ANSI color constants
307
+ *
308
+ * Each command handles its own output formatting.
309
+ * COLORS is shared across errors.mjs, health.mjs, approve.mjs,
310
+ * version_cmd.mjs, simulate.mjs, and cli.mjs.
311
+ */
312
+
313
+ const COLORS = {
314
+ reset: "\x1b[0m",
315
+ bold: "\x1b[1m",
316
+ dim: "\x1b[2m",
317
+ green: "\x1b[32m",
318
+ red: "\x1b[31m",
319
+ yellow: "\x1b[33m",
320
+ cyan: "\x1b[36m",
321
+ gray: "\x1b[90m",
322
+ };
323
+
324
+ // ---- src/errors.mjs ----
325
+ /**
326
+ * errors.mjs — structured error model for the ATF CLI
327
+ *
328
+ * Error envelope: { ok: false, error: { code, message, details? } }
329
+ * Success envelope: { ok: true, ... }
330
+ *
331
+ * Exit codes:
332
+ * 0 — success (allowed, healthy, version)
333
+ * 1 — user error / denied / verify-failed / 401 / 403 / 404 / 409
334
+ * 2 — network / server error (fetch throw, timeout, 5xx, rate-limited)
335
+ */
336
+
337
+ const ERROR_CODES = {
338
+ USER_ERROR: "USER_ERROR",
339
+ DENIED: "DENIED",
340
+ NETWORK_ERROR: "NETWORK_ERROR",
341
+ SERVER_ERROR: "SERVER_ERROR",
342
+ VALIDATION_ERROR: "VALIDATION_ERROR",
343
+ AUTH_ERROR: "AUTH_ERROR",
344
+ VERIFY_FAILED: "VERIFY_FAILED",
345
+ };
346
+
347
+ function makeError(code, message, details) {
348
+ const err = { code, message };
349
+ if (details && typeof details === "object" && Object.keys(details).length > 0) {
350
+ err.details = details;
351
+ }
352
+ return { ok: false, error: err };
353
+ }
354
+
355
+ function exitCodeForError(code) {
356
+ switch (code) {
357
+ case ERROR_CODES.NETWORK_ERROR:
358
+ case ERROR_CODES.SERVER_ERROR:
359
+ return 2;
360
+ default:
361
+ return 1;
362
+ }
363
+ }
364
+
365
+ function formatErrorPretty(errEnvelope) {
366
+ const e = errEnvelope.error;
367
+ const noColor = process.env.NO_COLOR;
368
+ const c = noColor ? { reset: "", bold: "", red: "", dim: "", yellow: "", cyan: "" } : COLORS;
369
+ const lines = [];
370
+ lines.push("");
371
+ lines.push(`${c.bold}${c.red} \u2717 ERROR [${e.code}]${c.reset}`);
372
+ lines.push(` ${e.message}`);
373
+ if (e.details) {
374
+ if (e.details.hint) lines.push(`${c.dim} Hint: ${e.details.hint}${c.reset}`);
375
+ if (e.details.status) lines.push(`${c.dim} HTTP status: ${e.details.status}${c.reset}`);
376
+ if (e.details.requestId) lines.push(`${c.dim} Request ID: ${e.details.requestId}${c.reset}`);
377
+ }
378
+ lines.push("");
379
+ return lines.join("\n");
380
+ }
381
+
382
+ /**
383
+ * Returns true if `obj` looks like a structured ATF error envelope.
384
+ * Shape: { ok: false, error: { code: <string>, ... } }
385
+ * Used to distinguish app-level 404s from endpoint-miss 404s.
386
+ */
387
+ function isAtfErrorEnvelope(obj) {
388
+ return (
389
+ obj !== null &&
390
+ typeof obj === "object" &&
391
+ obj.ok === false &&
392
+ obj.error !== null &&
393
+ typeof obj.error === "object" &&
394
+ typeof obj.error.code === "string"
395
+ );
396
+ }
397
+
398
+ function exitWithError(code, message, hint, format, extra) {
399
+ const details = {};
400
+ if (hint) details.hint = hint;
401
+ if (extra && extra.status) details.status = extra.status;
402
+ if (extra && extra.requestId) details.requestId = extra.requestId;
403
+ if (extra && extra.path) details.path = extra.path;
404
+ if (extra && extra.attempted_paths) details.attempted_paths = extra.attempted_paths;
405
+ if (extra && extra.baseUrl) details.baseUrl = extra.baseUrl;
406
+ const err = makeError(code, message, Object.keys(details).length > 0 ? details : undefined);
407
+ if (format === "pretty") {
408
+ process.stderr.write(formatErrorPretty(err));
409
+ } else {
410
+ process.stdout.write(JSON.stringify(err, null, 2) + "\n");
411
+ }
412
+ process.exit(exitCodeForError(code));
413
+ }
414
+
415
+ // ---- src/health.mjs ----
416
+ /**
417
+ * health.mjs — health command implementation
418
+ *
419
+ * GET {base}/health — check API availability and measure latency.
420
+ * Output: { ok:true, baseUrl, latencyMs, response:<healthJson> }
421
+ */
422
+
423
+ async function runHealth(args) {
424
+ const baseUrl = args.baseUrl;
425
+ const format = args.format;
426
+ const timeoutMs = args.timeoutMs;
427
+
428
+ const start = Date.now();
429
+ let response;
430
+ try {
431
+ response = await getHealth(baseUrl, timeoutMs);
432
+ } catch (err) {
433
+ exitWithError(
434
+ ERROR_CODES.NETWORK_ERROR,
435
+ `Cannot reach ${baseUrl}/health — ${err.message}`,
436
+ "Check that the API is running and the base URL is correct.",
437
+ format
438
+ );
439
+ }
440
+ const latencyMs = Date.now() - start;
441
+
442
+ if (!response.ok) {
443
+ exitWithError(
444
+ ERROR_CODES.SERVER_ERROR,
445
+ `Health check failed with HTTP ${response.status}`,
446
+ "The API may be down or misconfigured.",
447
+ format,
448
+ { status: response.status, requestId: response.requestId }
449
+ );
450
+ }
451
+
452
+ const result = {
453
+ ok: true,
454
+ baseUrl,
455
+ latency_ms: latencyMs,
456
+ response: response.data || {},
457
+ };
458
+
459
+ if (format === "pretty") {
460
+ const noColor = process.env.NO_COLOR;
461
+ const c = noColor ? { reset: "", bold: "", green: "", dim: "" } : COLORS;
462
+ process.stdout.write(
463
+ `\n${c.bold}${c.green} \u2713 HEALTHY${c.reset}\n` +
464
+ `${c.dim} Base URL:${c.reset} ${baseUrl}\n` +
465
+ `${c.dim} Latency:${c.reset} ${latencyMs}ms\n` +
466
+ `${c.dim} HTTP:${c.reset} ${response.status}\n\n`
467
+ );
468
+ } else {
469
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
470
+ }
471
+ process.exit(0);
472
+ }
473
+
474
+ // ---- src/approve.mjs ----
475
+ /**
476
+ * approve.mjs — approve command implementation
477
+ *
478
+ * POST {base}/v1/intents/approve with Bearer token.
479
+ * Approves a pending intent returned from a previous simulation.
480
+ * Output: { ok:true, baseUrl, intent, response:<approveJson> }
481
+ */
482
+
483
+ async function runApprove(args) {
484
+ const baseUrl = args.baseUrl;
485
+ const format = args.format;
486
+ const token = args.token || args.apiKey;
487
+ const intent = args.intent;
488
+ const timeoutMs = args.timeoutMs;
489
+
490
+ if (!intent) {
491
+ exitWithError(
492
+ ERROR_CODES.USER_ERROR,
493
+ "Missing --intent <id>",
494
+ "Provide the intent ID returned from a simulation.",
495
+ format
496
+ );
497
+ }
498
+
499
+ if (!token) {
500
+ exitWithError(
501
+ ERROR_CODES.AUTH_ERROR,
502
+ "Missing authentication token",
503
+ "Provide --token <bearer> or set ATF_API_KEY env var.",
504
+ format
505
+ );
506
+ }
507
+
508
+ let response;
509
+ try {
510
+ response = await postApprove(baseUrl, intent, token, timeoutMs);
511
+ } catch (err) {
512
+ exitWithError(
513
+ ERROR_CODES.NETWORK_ERROR,
514
+ redactToken(`Cannot reach ${baseUrl} — ${err.message}`, token),
515
+ "Check your network connection and base URL.",
516
+ format
517
+ );
518
+ }
519
+
520
+ if (!response.ok) {
521
+ const data = response.data || {};
522
+ const errCode =
523
+ response.status >= 500
524
+ ? ERROR_CODES.SERVER_ERROR
525
+ : response.status === 401 || response.status === 403
526
+ ? ERROR_CODES.AUTH_ERROR
527
+ : ERROR_CODES.USER_ERROR;
528
+ const hint =
529
+ response.status === 401
530
+ ? "Check your Bearer token."
531
+ : response.status === 404
532
+ ? "Intent not found — check the intent ID."
533
+ : response.status === 409
534
+ ? "Intent already approved or expired."
535
+ : null;
536
+ const msg = redactToken(data.message || data.detail || `HTTP ${response.status}`, token);
537
+ exitWithError(
538
+ errCode,
539
+ msg,
540
+ hint,
541
+ format,
542
+ { status: response.status, requestId: response.requestId || data.request_id }
543
+ );
544
+ }
545
+
546
+ const data = response.data || {};
547
+ const result = { ok: true, baseUrl, intent, response: data };
548
+ if (args.verbose && args._deprecatedIntentId) {
549
+ result.warnings = ["--intent-id is deprecated; use --intent"];
550
+ }
551
+
552
+ if (format === "pretty") {
553
+ const noColor = process.env.NO_COLOR;
554
+ const c = noColor ? { reset: "", bold: "", green: "", dim: "" } : COLORS;
555
+ process.stdout.write(
556
+ `\n${c.bold}${c.green} \u2713 APPROVED${c.reset}\n` +
557
+ `${c.dim} Intent:${c.reset} ${intent}\n` +
558
+ (data.tx_hash
559
+ ? `${c.dim} TX Hash:${c.reset} ${data.tx_hash}\n`
560
+ : "") +
561
+ `\n`
562
+ );
563
+ } else {
564
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
565
+ }
566
+ process.exit(0);
567
+ }
568
+
569
+ // ---- src/version_cmd.mjs ----
570
+ /**
571
+ * version_cmd.mjs — rich version command
572
+ *
573
+ * Output: { ok:true, cli_version, node_version, platform, arch, default_base_url, build_commit, build_date }
574
+ */
575
+
576
+ async function runVersion(args) {
577
+ const format = args.format;
578
+ const commit = BUILD_COMMIT === "__BUILD_COMMIT__" ? null : BUILD_COMMIT;
579
+ const buildTime = BUILD_DATE === "__BUILD_DATE__" ? null : BUILD_DATE;
580
+ const result = {
581
+ ok: true,
582
+ cli_version: VERSION,
583
+ node_version: process.version,
584
+ platform: process.platform,
585
+ arch: process.arch,
586
+ default_base_url: DEFAULT_BASE_URL,
587
+ build_commit: commit,
588
+ build_date: buildTime,
589
+ };
590
+
591
+ if (format === "pretty") {
592
+ const noColor = process.env.NO_COLOR;
593
+ const c = noColor ? { reset: "", bold: "", dim: "" } : COLORS;
594
+ process.stdout.write(
595
+ `\n${c.bold} @trucore/atf v${VERSION}${c.reset}\n` +
596
+ `${c.dim} Node:${c.reset} ${process.version}\n` +
597
+ `${c.dim} Platform:${c.reset} ${process.platform}/${process.arch}\n` +
598
+ `${c.dim} API:${c.reset} ${DEFAULT_BASE_URL}\n` +
599
+ `${c.dim} Commit:${c.reset} ${commit || "dev"}\n` +
600
+ `${c.dim} Built:${c.reset} ${buildTime || "dev"}\n\n`
601
+ );
602
+ } else {
603
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
604
+ }
605
+ process.exit(0);
606
+ }
607
+
608
+ // ---- src/simulate.mjs ----
609
+ /**
610
+ * simulate.mjs — simulate command implementation
611
+ *
612
+ * Output: { ok:true, baseUrl, preset?, verified?:boolean, response:<simulateJson> }
613
+ */
614
+
615
+ async function runSimulate(args) {
616
+ const baseUrl = args.baseUrl;
617
+ const verify = args.verify;
618
+ const format = args.format;
619
+ const quiet = args.quiet;
620
+ const apiKey = args.apiKey;
621
+ const timeoutMs = args.timeoutMs;
622
+
623
+ // Resolve body
624
+ let body;
625
+ let presetName = null;
626
+ if (args.json) {
627
+ const parsed = parseJsonBody(args.json);
628
+ if (!parsed.ok) {
629
+ exitWithError(
630
+ ERROR_CODES.USER_ERROR,
631
+ parsed.message,
632
+ "Provide valid JSON via --json '{...}'",
633
+ format
634
+ );
635
+ }
636
+ body = parsed.body;
637
+ } else if (args.preset) {
638
+ presetName = args.preset;
639
+ const check = validatePreset(presetName);
640
+ if (!check.ok) {
641
+ exitWithError(
642
+ ERROR_CODES.USER_ERROR,
643
+ check.message,
644
+ `Available presets: ${PRESET_NAMES.join(", ")}`,
645
+ format
646
+ );
647
+ }
648
+ body = PRESETS[presetName].transaction;
649
+ if (!quiet && format === "pretty") {
650
+ process.stderr.write(
651
+ `${COLORS.dim}Preset: ${presetName} — ${PRESETS[presetName].description}${COLORS.reset}\n`
652
+ );
653
+ }
654
+ } else {
655
+ exitWithError(
656
+ ERROR_CODES.USER_ERROR,
657
+ "Either --preset <name> or --json '<json>' is required.",
658
+ `Available presets: ${PRESET_NAMES.join(", ")}`,
659
+ format
660
+ );
661
+ }
662
+
663
+ // Make request — try SIMULATE_PATHS in order; stop on first non-404.
664
+ // If a 404 carries a structured ATF error envelope ({ok:false, error:{code:...}})
665
+ // we treat it as an authoritative app-level response and do NOT fallback.
666
+ let response;
667
+ const attemptedPaths = [];
668
+ try {
669
+ for (const path of SIMULATE_PATHS) {
670
+ attemptedPaths.push(path);
671
+ response = await postSimulate(baseUrl, body, apiKey, timeoutMs, path);
672
+ if (response.status !== 404) break;
673
+ // 404 with a structured ATF error envelope → authoritative, stop fallback
674
+ if (isAtfErrorEnvelope(response.data)) break;
675
+ }
676
+ } catch (err) {
677
+ exitWithError(
678
+ ERROR_CODES.NETWORK_ERROR,
679
+ `Network failure — ${err.message}`,
680
+ "Check your network connection and base URL.",
681
+ format
682
+ );
683
+ }
684
+
685
+ // Handle non-200
686
+ if (!response.ok) {
687
+ let errCode;
688
+ let msg;
689
+ let hint = null;
690
+
691
+ if (response.status === 401 || response.status === 403) {
692
+ errCode = ERROR_CODES.AUTH_ERROR;
693
+ const data = response.data || {};
694
+ msg = data.message || data.detail || `HTTP ${response.status}`;
695
+ hint = response.status === 401
696
+ ? "Check your API key or Bearer token."
697
+ : "Access denied. Check your permissions.";
698
+ } else if (response.status === 404) {
699
+ if (isAtfErrorEnvelope(response.data)) {
700
+ // Authoritative ATF 404 — surface the server's error as-is
701
+ errCode = response.data.error.code || ERROR_CODES.SERVER_ERROR;
702
+ msg = response.data.error.message || `HTTP 404`;
703
+ hint = (response.data.error.details && response.data.error.details.hint) || null;
704
+ exitWithError(errCode, msg, hint, format, {
705
+ status: 404,
706
+ attempted_paths: attemptedPaths,
707
+ baseUrl,
708
+ requestId: response.requestId,
709
+ });
710
+ }
711
+ // Endpoint miss — none of the paths exist on this server
712
+ errCode = ERROR_CODES.SERVER_ERROR;
713
+ msg = "SIMULATE_NOT_AVAILABLE";
714
+ hint = "The simulate endpoint is not available on this server.";
715
+ exitWithError(errCode, msg, hint, format, {
716
+ status: 404,
717
+ attempted_paths: attemptedPaths,
718
+ baseUrl,
719
+ requestId: response.requestId,
720
+ });
721
+ } else if (response.status === 422 || response.status === 400) {
722
+ errCode = ERROR_CODES.VALIDATION_ERROR;
723
+ const data = response.data || {};
724
+ msg = data.message || data.detail || data.error || `HTTP ${response.status}`;
725
+ hint = "Check the request body format and required fields.";
726
+ } else if (response.status === 429) {
727
+ errCode = ERROR_CODES.SERVER_ERROR;
728
+ const data = response.data || {};
729
+ msg = data.message || data.detail || data.error || `HTTP ${response.status}`;
730
+ hint = response.retryAfter
731
+ ? `Rate limited. Retry after ${response.retryAfter}s.`
732
+ : "Rate limited. Try again later.";
733
+ } else if (response.status >= 500) {
734
+ errCode = ERROR_CODES.SERVER_ERROR;
735
+ const data = response.data || {};
736
+ msg = data.message || data.detail || data.error || `HTTP ${response.status}`;
737
+ } else {
738
+ errCode = ERROR_CODES.USER_ERROR;
739
+ const data = response.data || {};
740
+ msg = data.message || data.detail || data.error || `HTTP ${response.status}`;
741
+ }
742
+ exitWithError(errCode, msg, hint, format, { status: response.status, requestId: response.requestId });
743
+ }
744
+
745
+ const data = response.data;
746
+ if (!data || typeof data !== "object") {
747
+ exitWithError(
748
+ ERROR_CODES.SERVER_ERROR,
749
+ "API returned non-JSON response.",
750
+ null,
751
+ format
752
+ );
753
+ }
754
+
755
+ // Extract fields — firewall-api returns decision/reasons (array);
756
+ // legacy servers may return status/reason (string).
757
+ const decision = (data.decision || data.status || "").toLowerCase();
758
+ const reasons = Array.isArray(data.reasons) ? data.reasons : [];
759
+ const reason = data.reason || data.deny_reason || data.message || ""; // legacy display
760
+ const receipt_hash = data.receipt_hash || data.hash || "";
761
+ const content_hash = data.content_hash || "";
762
+ const policy_hash = data.policy_hash !== undefined ? data.policy_hash : null;
763
+ const params = data.params || null;
764
+ const displayReason = reasons.length > 0 ? reasons.join(", ") : reason;
765
+
766
+ // Validate receipt_hash
767
+ if (receipt_hash && !isValidReceiptHash(receipt_hash)) {
768
+ exitWithError(
769
+ ERROR_CODES.VALIDATION_ERROR,
770
+ `API returned invalid receipt_hash: "${receipt_hash}"`,
771
+ "Expected: 64 lowercase hex characters.",
772
+ format
773
+ );
774
+ }
775
+
776
+ // Validate content_hash if present
777
+ if (content_hash && !isValidReceiptHash(content_hash)) {
778
+ exitWithError(
779
+ ERROR_CODES.VALIDATION_ERROR,
780
+ `API returned invalid content_hash: "${content_hash}"`,
781
+ "Expected: 64 lowercase hex characters.",
782
+ format
783
+ );
784
+ }
785
+
786
+ // Verification (--verify only)
787
+ let verified = undefined;
788
+ if (verify) {
789
+ if (content_hash) {
790
+ const expectedHash = computeContentHash(decision, reasons, policy_hash, params);
791
+ if (content_hash !== expectedHash) {
792
+ exitWithError(
793
+ ERROR_CODES.VERIFY_FAILED,
794
+ `content_hash does not match local computation.`,
795
+ `Expected: ${expectedHash.slice(0, 16)}... Got: ${content_hash.slice(0, 16)}... Open ${baseUrl}/verify?hash=${content_hash}`,
796
+ format
797
+ );
798
+ }
799
+ verified = true;
800
+ } else if (receipt_hash) {
801
+ // DEPRECATED legacy path — content_hash not returned by server.
802
+ if (!quiet) {
803
+ process.stderr.write(
804
+ `${COLORS.yellow}[DEPRECATED] server did not return content_hash; falling back to legacy receipt_hash verification.${COLORS.reset}\n`
805
+ );
806
+ }
807
+ const expectedHash = computeLegacyContentHash(decision, reason);
808
+ if (receipt_hash !== expectedHash) {
809
+ exitWithError(
810
+ ERROR_CODES.VERIFY_FAILED,
811
+ `receipt_hash does not match local computation (legacy).`,
812
+ `Expected: ${expectedHash.slice(0, 16)}... Got: ${receipt_hash.slice(0, 16)}... Open ${baseUrl}/verify?hash=${receipt_hash}`,
813
+ format
814
+ );
815
+ }
816
+ verified = true;
817
+ } else {
818
+ verified = false;
819
+ }
820
+ }
821
+
822
+ // Build result envelope
823
+ const result = { ok: true, baseUrl };
824
+ result.preset = presetName || null;
825
+ result.verified = verify ? (verified === true) : null;
826
+ result.response = data;
827
+
828
+ // Format output (only reached if verify passed or not requested)
829
+ if (format === "pretty") {
830
+ const isAllowed = decision === "allowed" || decision === "approved" || decision === "approve";
831
+ const noColor = process.env.NO_COLOR;
832
+ const c = noColor
833
+ ? { reset: "", bold: "", dim: "", green: "", red: "", yellow: "", cyan: "", gray: "" }
834
+ : COLORS;
835
+ const statusColor = isAllowed ? c.green : c.red;
836
+ const statusIcon = isAllowed ? "\u2713" : "\u2717";
837
+ const lines = [];
838
+ lines.push("");
839
+ lines.push(`${c.bold}${statusColor} ${statusIcon} ${decision.toUpperCase()}${c.reset}`);
840
+ if (displayReason) lines.push(`${c.dim} Reason:${c.reset} ${displayReason}`);
841
+ if (content_hash) lines.push(`${c.dim} Content hash:${c.reset} ${content_hash}`);
842
+ if (receipt_hash) lines.push(`${c.dim} Receipt:${c.reset} ${receipt_hash}`);
843
+ const verifyHash = content_hash || receipt_hash;
844
+ if (verify && verifyHash) {
845
+ lines.push("");
846
+ lines.push(`${c.cyan}${c.bold} Trustless Verification${c.reset}`);
847
+ lines.push(`${c.cyan} Verify: ${baseUrl}/verify?hash=${verifyHash}${c.reset}`);
848
+ lines.push(`${c.dim} Don't trust TruCore \u2014 recompute/verify deterministically.${c.reset}`);
849
+ }
850
+ lines.push("");
851
+ process.stdout.write(lines.join("\n") + "\n");
852
+ } else {
853
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
854
+ }
855
+
856
+ // If verify + receipt signing key available, print extra info
857
+ if (verify && receipt_hash) {
858
+ try {
859
+ const keyInfo = await getReceiptSigningKey(baseUrl);
860
+ if (keyInfo && keyInfo.available === true) {
861
+ if (!quiet) {
862
+ process.stderr.write(
863
+ `${COLORS.dim} Receipt signing is available. ` +
864
+ `Public key: ${keyInfo.public_key || "(see /api/receipt-signing-key)"}${COLORS.reset}\n`
865
+ );
866
+ process.stderr.write(
867
+ `${COLORS.dim} You can verify the receipt signature via POST /api/receipt-signature${COLORS.reset}\n`
868
+ );
869
+ }
870
+ }
871
+ } catch {
872
+ // Non-critical — ignore
873
+ }
874
+ }
875
+
876
+ // Exit code: allowed=0, denied=1
877
+ if (decision === "allowed" || decision === "approve" || decision === "approved") {
878
+ process.exit(0);
879
+ } else {
880
+ process.exit(1);
881
+ }
882
+ }
883
+
884
+ // ---- src/cli.mjs ----
885
+ /**
886
+ * cli.mjs — argument parsing and entry point (zero deps)
887
+ *
888
+ * VERSION, DEFAULT_BASE_URL, BUILD_COMMIT, BUILD_DATE are defined
889
+ * in constants.mjs (concatenated above by build.mjs).
890
+ */
891
+
892
+ const HELP_TEXT = `
893
+ @trucore/atf v${VERSION} — Agent Transaction Firewall CLI
894
+
895
+ USAGE
896
+ npx @trucore/atf@${VERSION} <command> [options]
897
+
898
+ COMMANDS
899
+ version Show CLI version and build metadata
900
+ health Check API health and latency
901
+ simulate Run a transaction simulation
902
+ approve Approve a pending intent
903
+
904
+ GLOBAL OPTIONS
905
+ --base-url <url> API base URL (default: ${DEFAULT_BASE_URL})
906
+ --timeout-ms <ms> Request timeout in ms (default: 20000)
907
+ --format <fmt> Output format: json | pretty (default: json)
908
+ --pretty Shorthand for --format pretty
909
+ --no-color Disable ANSI colors
910
+ --api-key <key> API key (also: ATF_API_KEY env var)
911
+
912
+ SIMULATE OPTIONS
913
+ --preset <name> Use a built-in preset: ${PRESET_NAMES.join(", ")}
914
+ --json '<json>' Send raw JSON transaction body
915
+ --verify Verify content_hash integrity
916
+ --quiet Suppress non-essential output
917
+
918
+ APPROVE OPTIONS
919
+ --intent <id> Intent ID to approve (required)
920
+ --token <bearer> Bearer token for auth (also: ATF_API_KEY env var)
921
+
922
+ ENVIRONMENT
923
+ ATF_API_KEY API key (sent as x-api-key header or Bearer token)
924
+ ATF_BASE_URL Override default base URL
925
+ ATF_TIMEOUT_MS Override default timeout (ms)
926
+ NO_COLOR Disable ANSI colors when set
927
+
928
+ EXIT CODES
929
+ 0 Success (allowed, healthy, etc.)
930
+ 1 User error or denied
931
+ 2 Network / server error
932
+
933
+ EXAMPLES
934
+ npx @trucore/atf@${VERSION} version
935
+ npx @trucore/atf@${VERSION} health
936
+ npx @trucore/atf@${VERSION} simulate --preset swap_small --verify
937
+ npx @trucore/atf@${VERSION} approve --intent abc123 --token mytoken
938
+ `;
939
+
940
+ function parseArgs(argv) {
941
+ const defaultTimeout = (() => {
942
+ const env = process.env.ATF_TIMEOUT_MS;
943
+ if (env) { const n = parseInt(env, 10); if (n > 0) return n; }
944
+ return 20000;
945
+ })();
946
+
947
+ const args = {
948
+ command: null,
949
+ preset: null,
950
+ json: null,
951
+ baseUrl: process.env.ATF_BASE_URL || DEFAULT_BASE_URL,
952
+ verify: false,
953
+ format: "json",
954
+ quiet: false,
955
+ apiKey: process.env.ATF_API_KEY || null,
956
+ help: false,
957
+ showVersion: false,
958
+ timeoutMs: defaultTimeout,
959
+ noColor: !!process.env.NO_COLOR,
960
+ intent: null,
961
+ token: null,
962
+ verbose: false,
963
+ _deprecatedIntentId: false,
964
+ };
965
+
966
+ const raw = argv.slice(2);
967
+ let i = 0;
968
+
969
+ while (i < raw.length) {
970
+ const arg = raw[i];
971
+
972
+ if (arg === "--help" || arg === "-h") {
973
+ args.help = true;
974
+ i++;
975
+ } else if (arg === "--version" || arg === "-V") {
976
+ args.showVersion = true;
977
+ i++;
978
+ } else if (arg === "--verify") {
979
+ args.verify = true;
980
+ i++;
981
+ } else if (arg === "--quiet" || arg === "-q") {
982
+ args.quiet = true;
983
+ i++;
984
+ } else if (arg === "--pretty") {
985
+ args.format = "pretty";
986
+ i++;
987
+ } else if (arg === "--no-color") {
988
+ args.noColor = true;
989
+ i++;
990
+ } else if (arg === "--verbose") {
991
+ args.verbose = true;
992
+ i++;
993
+ } else if (arg === "--preset") {
994
+ args.preset = raw[++i] || null;
995
+ i++;
996
+ } else if (arg === "--json") {
997
+ args.json = raw[++i] || null;
998
+ i++;
999
+ } else if (arg === "--base-url") {
1000
+ args.baseUrl = raw[++i] || args.baseUrl;
1001
+ i++;
1002
+ } else if (arg === "--format") {
1003
+ args.format = raw[++i] || args.format;
1004
+ i++;
1005
+ } else if (arg === "--api-key") {
1006
+ args.apiKey = raw[++i] || args.apiKey;
1007
+ i++;
1008
+ } else if (arg === "--timeout-ms") {
1009
+ args.timeoutMs = parseInt(raw[++i], 10) || defaultTimeout;
1010
+ i++;
1011
+ } else if (arg === "--intent" || arg === "--intent-id") {
1012
+ if (arg === "--intent-id") args._deprecatedIntentId = true;
1013
+ args.intent = raw[++i] || null;
1014
+ i++;
1015
+ } else if (arg === "--token") {
1016
+ args.token = raw[++i] || null;
1017
+ i++;
1018
+ } else if (!arg.startsWith("-") && !args.command) {
1019
+ args.command = arg;
1020
+ i++;
1021
+ } else {
1022
+ // Unknown argument — structured JSON error, exit 1
1023
+ const err = makeError("USER_ERROR", `Unknown argument: ${arg}`, { hint: "Run with --help for usage." });
1024
+ process.stdout.write(JSON.stringify(err, null, 2) + "\n");
1025
+ process.exit(1);
1026
+ }
1027
+ }
1028
+
1029
+ return args;
1030
+ }
1031
+
1032
+ async function main() {
1033
+ const args = parseArgs(process.argv);
1034
+
1035
+ if (args.showVersion) {
1036
+ process.stdout.write(`${VERSION}\n`);
1037
+ process.exit(0);
1038
+ }
1039
+
1040
+ if (args.help || !args.command) {
1041
+ process.stdout.write(HELP_TEXT);
1042
+ process.exit(0);
1043
+ }
1044
+
1045
+ switch (args.command) {
1046
+ case "version":
1047
+ await runVersion(args);
1048
+ break;
1049
+ case "health":
1050
+ await runHealth(args);
1051
+ break;
1052
+ case "simulate":
1053
+ await runSimulate(args);
1054
+ break;
1055
+ case "approve":
1056
+ await runApprove(args);
1057
+ break;
1058
+ default:
1059
+ exitWithError(ERROR_CODES.USER_ERROR, `Unknown command: ${args.command}`, "Run with --help for usage.", args.format);
1060
+ }
1061
+
1062
+ // Ensure clean exit — fetch() can leave open handles that prevent
1063
+ // the event loop from draining naturally.
1064
+ process.exit(0);
1065
+ }
1066
+
1067
+ main().catch((err) => {
1068
+ const msg = err && err.message ? err.message : String(err);
1069
+ const safeMsg = typeof redactToken === "function" ? redactToken(msg) : msg;
1070
+ process.stderr.write(`Fatal: ${safeMsg}\n`);
1071
+ process.exit(2);
1072
+ });
1073
+
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@trucore/atf",
3
+ "version": "1.0.0",
4
+ "description": "Agent Transaction Firewall CLI — simulate, verify, and audit on-chain transactions trustlessly.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/trucore-ai/agent-transaction-firewall.git",
9
+ "directory": "packages/atf-cli"
10
+ },
11
+ "homepage": "https://github.com/trucore-ai/agent-transaction-firewall/tree/main/packages/atf-cli#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/trucore-ai/agent-transaction-firewall/issues"
14
+ },
15
+ "author": "TruCore AI",
16
+ "funding": "https://trucore.xyz",
17
+ "keywords": [
18
+ "blockchain",
19
+ "firewall",
20
+ "transaction",
21
+ "defi",
22
+ "security",
23
+ "simulation",
24
+ "trustless"
25
+ ],
26
+ "bin": {
27
+ "atf": "./dist/index.js"
28
+ },
29
+ "main": "./dist/index.js",
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "scripts": {
42
+ "build": "node build.mjs",
43
+ "test": "node --test tests/",
44
+ "prepublishOnly": "npm run build && npm run test"
45
+ },
46
+ "devDependencies": {}
47
+ }