twzrd-receipt-verifier 1.0.9 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,20 +1,23 @@
1
1
  # TWZRD Receipt Verifier (standalone)
2
2
 
3
- Verify a TWZRD **AO-Receipt (V5 or V6)** offline, trusting **nothing from TWZRD's
4
- servers or codebase** - only the receipt, TWZRD's published public key, and two
5
- widely-audited crypto libraries. The verifier reads the domain the receipt
6
- carries and applies the matching V5 or V6 leaf rules (V6 binds the
7
- `reputation_*` provenance fields into the signed leaf; V5 left them unsigned).
3
+ Verify a TWZRD receipt offline, trusting **nothing from TWZRD's servers or
4
+ codebase** - only the receipt, TWZRD's published public key, and two
5
+ widely-audited crypto libraries. The verifier **auto-detects** two receipt
6
+ families:
8
7
 
9
- A TWZRD trust receipt has two layers:
8
+ | Family | What it is | Scheme | Signing key |
9
+ |--------|-----------|--------|-------------|
10
+ | **AO-Receipt V5/V6** | trust-API receipts from `intel.twzrd.xyz` | `keccak256` leaf over a packed preimage, Ed25519 over the leaf bytes | `9V6Pn19...` (fetched/pinned) |
11
+ | **cNFT Receipt** | the 95k genesis compressed-NFT receipts | Ed25519 **directly** over a compact-JSON payload (no leaf), hex sig | `2ELSDx...` (built-in) |
10
12
 
11
- 1. **Tamper-evidence** - a `keccak256` leaf over the receipt's preimage fields.
12
- 2. **Authenticity** - an **Ed25519 signature** over the leaf bytes, made with
13
- TWZRD's dedicated receipt-signing key.
13
+ For V5/V6 the verifier reads the domain the receipt carries and applies the
14
+ matching leaf rules (V6 binds the `reputation_*` provenance fields into the signed
15
+ leaf; V5 left them unsigned). For cNFT receipts there is no leaf - tamper-evidence
16
+ **is** the signature: any change to a signed field (including the wallet)
17
+ invalidates it.
14
18
 
15
- This tool recomputes the leaf **and** checks the signature against the published
16
- key. If it says `VALID`, the receipt was authored by TWZRD and was not altered.
17
- Unsigned, wrong-key, or tampered receipts fail.
19
+ If it says `VALID`, the receipt was authored by TWZRD and was not altered.
20
+ Unsigned, wrong-key, wrong-wallet, or tampered receipts fail.
18
21
 
19
22
  ## Where this fits: the agent trust loop
20
23
 
@@ -50,10 +53,79 @@ Also published, machine-readable, at:
50
53
  > **Most paranoid mode:** pin the key out-of-band with `--pubkey` instead of
51
54
  > fetching it, so you never trust the live endpoint to tell you which key to trust.
52
55
 
56
+ ## cNFT Receipts (the 95k genesis receipts)
57
+
58
+ Every genesis receipt is a compressed NFT on Solana mainnet (tree
59
+ `8QFdTqBkSeyuvp47dXdpwfWzXTuYSbAC64oT4soPGnXS`, verified creator `2ELSDx...`). Its
60
+ at-mint snapshot is published as a signed `anchor` block in the cNFT metadata,
61
+ served at `https://twzrd.xyz/r/<wallet>.json`:
62
+
63
+ ```json
64
+ {
65
+ "anchor": {
66
+ "tier_at_mint": "Platinum",
67
+ "score_at_mint": 255,
68
+ "verified_tx": "<solana settlement signature>",
69
+ "behavior_proof": "<sha256 hex>",
70
+ "minted_at": 1782415336,
71
+ "signature": "<128-hex Ed25519 sig>",
72
+ "verify_pubkey": "2ELSDxLkb7dYrN6EUG69tNtULAq4Fo7WPvXyrZPmuFif"
73
+ },
74
+ "live": { "...": "current decayed reputation (NOT signed)" }
75
+ }
76
+ ```
77
+
78
+ The signed payload is the compact JSON `{wallet, tier_at_mint, score_at_mint,
79
+ verified_tx, behavior_proof, minted_at}` (exact key order). The `wallet` is the
80
+ first signed field but is **not** stored in the anchor - it is the `<wallet>.json`
81
+ filename / the cNFT leaf owner - so pass `--wallet` or keep the filename. The
82
+ signing key (`2ELSDx...`) is **built in** to the verifier (pinned in the audited
83
+ package); override with `--pubkey`, or fetch the published copy with `--fetch-key`.
84
+
85
+ ```bash
86
+ # fetch a receipt and verify it (wallet inferred from the filename, key built-in)
87
+ W=zoz7neLHXoaLwNBuckSqNqaMsacpqJsphtFuNNpQyt3
88
+ curl -s https://twzrd.xyz/r/$W.json -o $W.json
89
+ npx twzrd-receipt-verifier $W.json --self-test
90
+
91
+ # or pass the wallet explicitly (e.g. when piping from stdin)
92
+ npx twzrd-receipt-verifier anchor.json --wallet $W
93
+
94
+ # fetch the key from the published descriptor instead of the built-in copy
95
+ # (cross-check, or pin to whatever the live domain publishes)
96
+ npx twzrd-receipt-verifier $W.json --fetch-key
97
+ ```
98
+
99
+ The key is published, machine-readable, at `https://api.twzrd.xyz/v1/receipts/pubkey`
100
+ (and `https://twzrd.xyz/.well-known/twzrd-receipt-pubkey`) with the full signing spec
101
+ (`public_key`, `signed_fields`, `scheme`, `tree`). It must equal the built-in key **and**
102
+ the on-chain verified creator of every cNFT in the tree - three independent sources.
103
+
104
+ ```
105
+ mode : cNFT (Bubblegum anchor)
106
+ trusted pubkey : 2ELSDxLkb7dYrN6EUG69tNtULAq4Fo7WPvXyrZPmuFif [source: built-in genesis authority]
107
+ wallet : zoz7neLHXoaLwNBuckSqNqaMsacpqJsphtFuNNpQyt3 [source: filename]
108
+ signature_valid : true
109
+ RESULT : VALID (TWZRD-authored, untampered)
110
+ ```
111
+
112
+ Only the `anchor` block is signed. The `live` block (current decayed reputation)
113
+ is informational and intentionally NOT covered by the signature. For full on-chain
114
+ binding, confirm the cNFT exists in the genesis tree with verified creator
115
+ `2ELSDx` via any DAS provider (`getAsset` / `getAssetProof`); the signature alone
116
+ already proves `2ELSDx` authorship of the at-mint snapshot.
117
+
53
118
  ## Get a receipt to verify
54
119
 
55
- Any TWZRD V5 receipt works. To mint a fresh one, pay the trust endpoint (x402,
56
- 0.05 USDC on Solana mainnet) - e.g. via AgentCash:
120
+ Any TWZRD V5/v6 receipt works. To mint a fresh one, pay the trust endpoint
121
+ (x402, 0.05 USDC on Solana mainnet) with an x402 client that preserves TWZRD's
122
+ sponsored fee-payer slot semantics.
123
+
124
+ Current caveat (2026-06-23): `npx agentcash@latest fetch ...` is not a green
125
+ TWZRD paid-trust repro. It failed closed with `payment_invalid` /
126
+ `fee_payer_slot_already_signed`, and AgentCash balance stayed unchanged.
127
+
128
+ Known-bad compatibility command:
57
129
 
58
130
  ```bash
59
131
  npx agentcash@latest fetch https://intel.twzrd.xyz/v1/intel/trust/<PUBKEY> > resp.json
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "twzrd-receipt-verifier",
3
- "version": "1.0.9",
4
- "description": "Standalone offline verifier for TWZRD AO-Receipt V5 and V6 (Ed25519-signed keccak256 leaf). No trust in TWZRD servers or code.",
3
+ "version": "1.2.0",
4
+ "description": "Standalone offline verifier for TWZRD receipts: AO-Receipt V5/V6 (keccak256 leaf) and genesis cNFT receipts (Ed25519 over compact JSON). No trust in TWZRD servers or code.",
5
5
  "keywords": [
6
6
  "twzrd",
7
7
  "x402",
@@ -11,7 +11,10 @@
11
11
  "receipt",
12
12
  "verifier",
13
13
  "agent",
14
- "attestation"
14
+ "attestation",
15
+ "cnft",
16
+ "bubblegum",
17
+ "compressed-nft"
15
18
  ],
16
19
  "homepage": "https://intel.twzrd.xyz",
17
20
  "bugs": {
@@ -1,21 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
  /*
3
- * Standalone offline verifier for TWZRD AO-Receipt V5 (Node).
3
+ * Standalone offline verifier for TWZRD receipts (Node). Two receipt families,
4
+ * auto-detected:
4
5
  *
5
- * Verifies, with NO trust in TWZRD's servers or codebase, that a receipt was
6
- * authored by TWZRD's published Ed25519 key and was not tampered with:
7
- * 1. TAMPER-EVIDENCE - recompute the keccak256 leaf from the preimage,
8
- * confirm it equals receipt.leaf.
9
- * 2. AUTHENTICITY - verify the Ed25519 signature over the leaf bytes
10
- * against TWZRD's PUBLISHED public key (you supply / fetch it).
6
+ * A. AO-Receipt V5/V6 (trust-API) - keccak256 leaf over a packed preimage,
7
+ * Ed25519-signed over the leaf bytes by the receipt key (9V6Pn19...).
8
+ * Shape: { preimage, leaf, signature, signing_pubkey }.
9
+ * B. cNFT Receipt (Bubblegum anchor) - the genesis compressed-NFT receipts.
10
+ * Ed25519 signed DIRECTLY over a compact-JSON payload (no keccak leaf) by
11
+ * the airship genesis authority (2ELSDx...), signature hex-encoded.
12
+ * Shape: { anchor: { tier_at_mint, score_at_mint, verified_tx,
13
+ * behavior_proof, minted_at, signature, verify_pubkey }, ... }.
11
14
  *
12
- * Crypto comes from audited libs (tweetnacl = ref Ed25519, js-sha3 = Keccak),
13
- * not from this script. base58 + the TWZRD byte layout are the only logic here.
15
+ * Verifies, with NO trust in TWZRD's servers or codebase, that a receipt was
16
+ * authored by TWZRD's published Ed25519 key and was not tampered with. Crypto
17
+ * comes from audited libs (tweetnacl = ref Ed25519, js-sha3 = Keccak), not from
18
+ * this script. base58 + the TWZRD byte layout are the only logic here.
14
19
  *
15
20
  * npm install tweetnacl js-sha3 bs58
16
21
  *
17
- * node verify_twzrd_receipt.js receipt.json
22
+ * # trust-API receipt (A)
18
23
  * node verify_twzrd_receipt.js receipt.json --pubkey 9V6Pn19kiUA5Rn6JpQfNduanvGt2aXGwsarosNfa2Ldf
24
+ * # cNFT receipt (B) - wallet is part of the signed payload but not in the
25
+ * # anchor block, so pass it or name the file <wallet>.json
26
+ * node verify_twzrd_receipt.js zoz7...json # wallet inferred from filename
27
+ * node verify_twzrd_receipt.js anchor.json --wallet zoz7neLHXoaLwNBuckSqNqaMsacpqJsphtFuNNpQyt3
19
28
  * cat receipt.json | node verify_twzrd_receipt.js - # stdin
20
29
  * node verify_twzrd_receipt.js receipt.json --self-test # tamper must fail
21
30
  *
@@ -24,6 +33,7 @@
24
33
  'use strict';
25
34
 
26
35
  const fs = require('fs');
36
+ const path = require('path');
27
37
  const crypto = require('crypto');
28
38
  const https = require('https');
29
39
  const nacl = require('tweetnacl');
@@ -32,6 +42,14 @@ const bs58 = require('bs58');
32
42
 
33
43
  const DEFAULT_BASE_URL = 'https://intel.twzrd.xyz';
34
44
  const KECCAK_EMPTY = 'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470';
45
+ // Genesis cNFT receipt authority (airship). Baked in as the most paranoid form of
46
+ // out-of-band pinning: the key ships in this audited package, never fetched live.
47
+ // Override with --pubkey, or fetch the published copy with --fetch-key (cross-check).
48
+ // Matches `verify_pubkey` in every genesis anchor and the verified creator on every
49
+ // cNFT in tree 8QFdTqBkSeyuvp47dXdpwfWzXTuYSbAC64oT4soPGnXS.
50
+ const DEFAULT_CNFT_PUBKEY = '2ELSDxLkb7dYrN6EUG69tNtULAq4Fo7WPvXyrZPmuFif';
51
+ // Where --fetch-key looks for the published cNFT key descriptor.
52
+ const DEFAULT_CNFT_BASE_URL = 'https://api.twzrd.xyz';
35
53
 
36
54
  function b58decode(s) { return Buffer.from(bs58.decode(s)); }
37
55
 
@@ -131,6 +149,32 @@ function fetchPublishedPubkey(baseUrl) {
131
149
  return fetchPath(0);
132
150
  }
133
151
 
152
+ // Fetch the published cNFT signing key descriptor (for --fetch-key). Returns the
153
+ // base58 pubkey. This trades package-trust for domain/TLS-trust; the built-in key is
154
+ // the default precisely because it needs no network. Use this to CROSS-CHECK the
155
+ // built-in, or to pin to whatever the live domain currently publishes.
156
+ function fetchCnftPubkey(baseUrl) {
157
+ const base = baseUrl.replace(/\/+$/, '');
158
+ const paths = ['/v1/receipts/pubkey', '/.well-known/twzrd-receipt-pubkey'];
159
+ const headers = { 'User-Agent': 'twzrd-receipt-verifier/cnft' };
160
+ function fetchPath(i) {
161
+ if (i >= paths.length) return Promise.reject(new Error('no cNFT pubkey endpoint responded'));
162
+ return new Promise((resolve, reject) => {
163
+ https.get(base + paths[i], { headers }, (res) => {
164
+ let body = '';
165
+ res.on('data', (c) => (body += c));
166
+ res.on('end', () => {
167
+ try {
168
+ const pk = JSON.parse(body).public_key;
169
+ if (pk) resolve(pk); else throw new Error('no public_key field');
170
+ } catch (e) { fetchPath(i + 1).then(resolve, reject); }
171
+ });
172
+ }).on('error', () => fetchPath(i + 1).then(resolve, reject));
173
+ });
174
+ }
175
+ return fetchPath(0);
176
+ }
177
+
134
178
  function verify(receipt, trustedPubkey) {
135
179
  const out = { leaf_valid: false, signature_valid: false, errors: [] };
136
180
  const pre = receipt.preimage || {};
@@ -166,31 +210,124 @@ function verify(receipt, trustedPubkey) {
166
210
  return out;
167
211
  }
168
212
 
213
+ // ── cNFT (Bubblegum anchor) receipt ───────────────────────────────────────
214
+ // The genesis compressed-NFT receipts are NOT keccak-leaf receipts. Each is an
215
+ // Ed25519 signature made DIRECTLY over the UTF-8 bytes of a compact JSON object,
216
+ // in this EXACT key order (JSON.stringify defaults: no spaces). Do not reorder.
217
+ const CNFT_SIGNED_FIELDS = ['wallet', 'tier_at_mint', 'score_at_mint', 'verified_tx', 'behavior_proof', 'minted_at'];
218
+
219
+ // A cNFT receipt is the metadata JSON served at /r/<wallet>.json: it carries an
220
+ // `anchor` block (at-mint snapshot + signature) instead of a keccak `leaf`.
221
+ function isCnftReceipt(receipt) {
222
+ const a = receipt && receipt.anchor;
223
+ return !!(a && typeof a === 'object' && a.signature &&
224
+ (a.tier_at_mint !== undefined || a.score_at_mint !== undefined));
225
+ }
226
+
227
+ // Reconstruct the exact bytes the issuer signed (airship.ts). `wallet` is the
228
+ // first signed field but is NOT stored in the anchor block (it is the leaf owner /
229
+ // the <wallet>.json filename), so it must be supplied by the caller.
230
+ function cnftSignedPayload(anchor, wallet) {
231
+ return Buffer.from(JSON.stringify({
232
+ wallet,
233
+ tier_at_mint: anchor.tier_at_mint,
234
+ score_at_mint: anchor.score_at_mint,
235
+ verified_tx: anchor.verified_tx,
236
+ behavior_proof: anchor.behavior_proof,
237
+ minted_at: anchor.minted_at,
238
+ }), 'utf8');
239
+ }
240
+
241
+ // Resolve the wallet from (in priority): explicit --wallet, the receipt body (if a
242
+ // future format embeds it), or the <wallet>.json filename. Only accepts a filename
243
+ // stem that base58-decodes to a 32-byte pubkey, so a stray filename can't be passed
244
+ // off as the signed wallet.
245
+ function resolveWallet({ explicitWallet, receipt, receiptPath } = {}) {
246
+ if (explicitWallet) return { wallet: explicitWallet, src: '--wallet' };
247
+ if (receipt && typeof receipt.wallet === 'string') return { wallet: receipt.wallet, src: 'receipt.wallet' };
248
+ if (receipt && receipt.anchor && typeof receipt.anchor.wallet === 'string') return { wallet: receipt.anchor.wallet, src: 'anchor.wallet' };
249
+ if (receiptPath && receiptPath !== '-') {
250
+ const stem = path.basename(receiptPath).replace(/\.json$/i, '');
251
+ try { if (Buffer.from(bs58.decode(stem)).length === 32) return { wallet: stem, src: 'filename' }; } catch (_) {}
252
+ }
253
+ return { wallet: undefined, src: 'none' };
254
+ }
255
+
256
+ // Authenticity for a cNFT receipt: Ed25519-verify the hex signature over the
257
+ // reconstructed compact-JSON payload against the trusted key. There is no keccak
258
+ // leaf to recompute - tamper-evidence is the signature itself: any change to a
259
+ // signed field (incl. wallet) invalidates it.
260
+ function verifyCnft(receipt, trustedPubkey, wallet) {
261
+ const out = { mode: 'cnft', signature_valid: false, errors: [] };
262
+ const a = (receipt && receipt.anchor) || {};
263
+ out.wallet = wallet;
264
+ if (!wallet) {
265
+ out.errors.push('cNFT receipt: wallet unknown - it is part of the signed payload but not in the anchor block. Pass --wallet <addr> or name the file <wallet>.json.');
266
+ return out;
267
+ }
268
+ const embedded = a.verify_pubkey;
269
+ if (embedded && embedded !== trustedPubkey) {
270
+ out.errors.push(`anchor.verify_pubkey ${embedded} != trusted key ${trustedPubkey}`);
271
+ return out;
272
+ }
273
+ const sigHex = String(a.signature || '').toLowerCase().replace(/^0x/, '');
274
+ if (!sigHex) { out.errors.push('missing anchor.signature'); return out; }
275
+ if (!/^[0-9a-f]+$/.test(sigHex) || sigHex.length !== 128) {
276
+ out.errors.push(`anchor.signature must be 64 hex bytes (got ${sigHex.length / 2 | 0})`);
277
+ return out;
278
+ }
279
+ const sig = Buffer.from(sigHex, 'hex');
280
+ const msg = cnftSignedPayload(a, wallet);
281
+ out.signed_payload = msg.toString('utf8');
282
+ let pk;
283
+ try { pk = b58decode(trustedPubkey); }
284
+ catch (e) { out.errors.push('trusted pubkey not base58: ' + e.message); return out; }
285
+ try {
286
+ out.signature_valid = nacl.sign.detached.verify(
287
+ new Uint8Array(msg), new Uint8Array(sig), new Uint8Array(pk));
288
+ } catch (e) { out.errors.push('signature check error: ' + e.message); return out; }
289
+ if (!out.signature_valid) {
290
+ out.errors.push('signature not valid for the trusted key (payload tampered, or wrong --wallet / --pubkey)');
291
+ }
292
+ out.valid = out.signature_valid && out.errors.length === 0;
293
+ out.trusted_pubkey = trustedPubkey;
294
+ return out;
295
+ }
296
+
169
297
  async function main() {
170
298
  const args = process.argv.slice(2);
171
- const HELP = `twzrd-receipt-verifier -- offline verifier for TWZRD AO-Receipt V5 (Ed25519-signed keccak256 leaf)
299
+ const HELP = `twzrd-receipt-verifier -- offline verifier for TWZRD receipts (Ed25519)
172
300
 
173
301
  Verifies, with NO trust in TWZRD's servers or code, that a receipt was authored by
174
- TWZRD's published Ed25519 key and was not tampered with.
302
+ TWZRD's published Ed25519 key and was not tampered with. Auto-detects two families:
303
+ - AO-Receipt V5/V6 (trust-API): keccak256 leaf, signed by ${'9V6Pn19...'} (default fetch)
304
+ - cNFT Receipt (genesis anchor): compact-JSON payload, signed by ${DEFAULT_CNFT_PUBKEY.slice(0, 7)}... (built-in)
175
305
 
176
306
  usage:
177
- twzrd-receipt-verifier <receipt.json|-> [--pubkey KEY] [--base-url URL] [--max-age SECS] [--self-test]
307
+ twzrd-receipt-verifier <receipt.json|-> [--pubkey KEY] [--fetch-key] [--wallet ADDR] [--base-url URL] [--max-age SECS] [--self-test]
178
308
 
179
309
  arguments:
180
310
  <receipt.json> path to the receipt JSON, or "-" to read from stdin
181
- --pubkey KEY trust this base58 Ed25519 pubkey (out-of-band) instead of fetching it
182
- --base-url URL where to fetch the published key (default: ${DEFAULT_BASE_URL})
183
- --max-age SECS replay-resistance policy: reject if preimage.timestamp_unix is older than
184
- SECS, OR if the receipt carries no valid timestamp. Crypto (leaf+sig) is
185
- time-independent; this is opt-in relying-party policy.
311
+ --pubkey KEY trust this base58 Ed25519 pubkey (out-of-band) instead of fetching/built-in
312
+ --fetch-key (cNFT only) fetch the signing key from the published well-known descriptor
313
+ (--base-url or ${DEFAULT_CNFT_BASE_URL}) instead of the built-in copy. Trades
314
+ package-trust for domain/TLS-trust; default stays built-in (no network).
315
+ --wallet ADDR (cNFT only) the leaf-owner wallet, which is part of the signed payload but
316
+ not stored in the anchor block. Inferred from a <wallet>.json filename if omitted.
317
+ --base-url URL where to fetch the key: trust-API default ${DEFAULT_BASE_URL}; cNFT (--fetch-key)
318
+ default ${DEFAULT_CNFT_BASE_URL}
319
+ --max-age SECS replay-resistance policy: reject if the receipt's timestamp (preimage.timestamp_unix
320
+ for trust-API, anchor.minted_at for cNFT) is older than SECS, OR is missing.
321
+ Crypto is time-independent; this is opt-in relying-party policy.
186
322
  --self-test additionally confirm a tampered copy FAILS (proves the check works)
187
323
  -h, --help show this help
188
324
 
189
325
  exit code: 0 = VALID, 1 = INVALID / error
190
- key source: ${DEFAULT_BASE_URL}/.well-known/x402`;
326
+ trust-API key source: ${DEFAULT_BASE_URL}/.well-known/x402
327
+ cNFT key source: ${DEFAULT_CNFT_BASE_URL}/v1/receipts/pubkey`;
191
328
  if (args.includes('-h') || args.includes('--help')) { console.log(HELP); process.exit(0); }
192
329
  if (args.length === 0) {
193
- console.error('usage: twzrd-receipt-verifier <receipt.json|-> [--pubkey KEY] [--base-url URL] [--max-age SECS] [--self-test]');
330
+ console.error('usage: twzrd-receipt-verifier <receipt.json|-> [--pubkey KEY] [--fetch-key] [--wallet ADDR] [--base-url URL] [--max-age SECS] [--self-test]');
194
331
  console.error(' twzrd-receipt-verifier --help');
195
332
  process.exit(1);
196
333
  }
@@ -201,16 +338,71 @@ key source: ${DEFAULT_BASE_URL}/.well-known/x402`;
201
338
  const maxAgeArg = getOpt('--max-age');
202
339
  const maxAge = maxAgeArg ? parseInt(maxAgeArg, 10) : 0; // 0 = freshness check off (opt-in policy)
203
340
 
204
- // keccak self-test: refuse to run with a broken hash backend
205
- if (keccak256('') !== KECCAK_EMPTY) { console.error('FATAL: keccak256 backend is wrong'); process.exit(1); }
206
-
207
341
  const raw = receiptArg === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(receiptArg, 'utf8');
208
342
  const receipt = JSON.parse(raw);
209
343
 
344
+ // ── cNFT (Bubblegum anchor) receipt: Ed25519 over compact JSON, no keccak leaf ──
345
+ if (isCnftReceipt(receipt)) {
346
+ let trusted = getOpt('--pubkey'), keySrc;
347
+ if (trusted) {
348
+ keySrc = '--pubkey (out-of-band)';
349
+ } else if (args.includes('--fetch-key')) {
350
+ const fetchBase = getOpt('--base-url') || DEFAULT_CNFT_BASE_URL;
351
+ trusted = await fetchCnftPubkey(fetchBase);
352
+ keySrc = `fetched from ${fetchBase}`;
353
+ } else {
354
+ trusted = DEFAULT_CNFT_PUBKEY;
355
+ keySrc = 'built-in genesis authority';
356
+ }
357
+ const { wallet, src: walletSrc } = resolveWallet({
358
+ explicitWallet: getOpt('--wallet'), receipt, receiptPath: receiptArg,
359
+ });
360
+ console.log(`mode : cNFT (Bubblegum anchor)`);
361
+ console.log(`trusted pubkey : ${trusted} [source: ${keySrc}]`);
362
+ console.log(`wallet : ${wallet || '(unknown)'} [source: ${walletSrc}]`);
363
+
364
+ const res = verifyCnft(receipt, trusted, wallet);
365
+ res.errors = res.errors || [];
366
+
367
+ // Opt-in freshness gate (anchor.minted_at). cNFT receipts are long-lived by
368
+ // design, so this is rarely useful, but kept for parity with the trust-API path.
369
+ if (maxAge > 0) {
370
+ const ts = receipt.anchor ? Number(receipt.anchor.minted_at) : NaN;
371
+ if (!Number.isFinite(ts) || ts <= 0) {
372
+ res.errors.push(`--max-age ${maxAge}s set but anchor has no valid minted_at`);
373
+ res.valid = false;
374
+ } else {
375
+ const age = Math.abs(Math.floor(Date.now() / 1000) - ts);
376
+ if (age > maxAge) { res.errors.push(`receipt too old (age ${age}s > --max-age ${maxAge}s)`); res.valid = false; }
377
+ }
378
+ }
379
+
380
+ console.log(`signature_valid : ${res.signature_valid}`);
381
+ res.errors.forEach((e) => console.log(' - ' + e));
382
+ let ok = !!res.valid;
383
+ console.log(`RESULT : ${ok ? 'VALID (TWZRD-authored, untampered)' : 'INVALID'}`);
384
+
385
+ if (selfTest) {
386
+ const tampered = JSON.parse(raw);
387
+ tampered.anchor = tampered.anchor || {};
388
+ tampered.anchor.score_at_mint = (Number(tampered.anchor.score_at_mint) || 0) + 1;
389
+ const t = verifyCnft(tampered, trusted, wallet);
390
+ const passed = !t.valid;
391
+ console.log(`self-test (tampered score must FAIL): ${passed ? 'PASS' : 'BROKEN'}`);
392
+ ok = ok && passed;
393
+ }
394
+ process.exit(ok ? 0 : 1);
395
+ }
396
+
397
+ // ── trust-API receipt (V5/V6): keccak256 leaf, signed over the leaf bytes ──
398
+ // keccak self-test: refuse to run with a broken hash backend
399
+ if (keccak256('') !== KECCAK_EMPTY) { console.error('FATAL: keccak256 backend is wrong'); process.exit(1); }
400
+
210
401
  let trusted = getOpt('--pubkey'), src;
211
402
  if (trusted) { src = '--pubkey (out-of-band)'; }
212
403
  else { trusted = await fetchPublishedPubkey(baseUrl); src = baseUrl + '/.well-known/x402'; }
213
- console.log(`trusted pubkey: ${trusted} [source: ${src}]`);
404
+ console.log(`mode : AO-Receipt (trust-API)`);
405
+ console.log(`trusted pubkey : ${trusted} [source: ${src}]`);
214
406
 
215
407
  const res = verify(receipt, trusted);
216
408
 
@@ -251,4 +443,15 @@ key source: ${DEFAULT_BASE_URL}/.well-known/x402`;
251
443
  process.exit(ok ? 0 : 1);
252
444
  }
253
445
 
254
- main().catch((e) => { console.error('error:', e.message); process.exit(1); });
446
+ // Export the pure verifiers for tests / programmatic use. Only run the CLI when
447
+ // invoked directly (so `require()` from the test suite does not trigger main()).
448
+ module.exports = {
449
+ verify, recomputeLeaf,
450
+ verifyCnft, cnftSignedPayload, isCnftReceipt, resolveWallet,
451
+ fetchCnftPubkey,
452
+ DEFAULT_CNFT_PUBKEY, DEFAULT_CNFT_BASE_URL, CNFT_SIGNED_FIELDS,
453
+ };
454
+
455
+ if (require.main === module) {
456
+ main().catch((e) => { console.error('error:', e.message); process.exit(1); });
457
+ }