@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.
- package/LICENSE +21 -0
- package/README.md +260 -0
- package/dist/index.js +1073 -0
- 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
|
+
}
|