@xfuel/verify 0.2.1 → 0.3.1

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 (46) hide show
  1. package/README.md +41 -3
  2. package/dist/anchor-witness.d.ts +155 -0
  3. package/dist/anchor-witness.d.ts.map +1 -0
  4. package/dist/anchor-witness.js +431 -0
  5. package/dist/anchor-witness.js.map +1 -0
  6. package/dist/canonical-preimage.d.ts +9 -0
  7. package/dist/canonical-preimage.d.ts.map +1 -0
  8. package/dist/canonical-preimage.js +25 -0
  9. package/dist/canonical-preimage.js.map +1 -0
  10. package/dist/cli.d.ts +1 -0
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +527 -15
  13. package/dist/cli.js.map +1 -1
  14. package/dist/epoch.d.ts +109 -0
  15. package/dist/epoch.d.ts.map +1 -0
  16. package/dist/epoch.js +209 -0
  17. package/dist/epoch.js.map +1 -0
  18. package/dist/head-binding.d.ts +24 -0
  19. package/dist/head-binding.d.ts.map +1 -0
  20. package/dist/head-binding.js +61 -0
  21. package/dist/head-binding.js.map +1 -0
  22. package/dist/index.d.ts +146 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +305 -24
  25. package/dist/index.js.map +1 -1
  26. package/dist/issuer-history.d.ts +91 -0
  27. package/dist/issuer-history.d.ts.map +1 -0
  28. package/dist/issuer-history.js +292 -0
  29. package/dist/issuer-history.js.map +1 -0
  30. package/dist/jcs.d.ts +8 -0
  31. package/dist/jcs.d.ts.map +1 -0
  32. package/dist/jcs.js +64 -0
  33. package/dist/jcs.js.map +1 -0
  34. package/dist/preimage.d.ts +49 -0
  35. package/dist/preimage.d.ts.map +1 -0
  36. package/dist/preimage.js +203 -0
  37. package/dist/preimage.js.map +1 -0
  38. package/dist/receipt-lane.d.ts +234 -0
  39. package/dist/receipt-lane.d.ts.map +1 -0
  40. package/dist/receipt-lane.js +443 -0
  41. package/dist/receipt-lane.js.map +1 -0
  42. package/dist/refusal.d.ts +90 -0
  43. package/dist/refusal.d.ts.map +1 -0
  44. package/dist/refusal.js +226 -0
  45. package/dist/refusal.js.map +1 -0
  46. package/package.json +6 -4
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * xfuel-verify receipt.json
8
8
  * xfuel-verify receipt.json --check-nullifier
9
9
  * xfuel-verify receipt.json --rpc https://mainnet.base.org
10
+ * xfuel-verify receipt.json inclusion.json head.json --rpc
10
11
  * cat receipt.json | xfuel-verify -
11
12
  *
12
13
  * Exit codes:
@@ -18,11 +19,18 @@
18
19
  Object.defineProperty(exports, "__esModule", { value: true });
19
20
  const node_fs_1 = require("node:fs");
20
21
  const index_js_1 = require("./index.js");
22
+ const anchor_witness_js_1 = require("./anchor-witness.js");
23
+ const jws_js_1 = require("./jws.js");
24
+ const preimage_js_1 = require("./preimage.js");
25
+ const issuer_history_js_1 = require("./issuer-history.js");
26
+ const canonical_preimage_js_1 = require("./canonical-preimage.js");
21
27
  const HELP = `
22
28
  xfuel-verify — Offline verification for Chit402 receipts
23
29
 
24
30
  Usage:
25
31
  xfuel-verify <receipt.json> Verify binding locally
32
+ xfuel-verify <refusal.json> Verify a chit402.refusal.v1 document
33
+ (issuer refused; not a payment)
26
34
  xfuel-verify <receipt.json> --jwks-file <jwks.json>
27
35
  Verify issuer signature with a JWKS file
28
36
  xfuel-verify <receipt.json> --fetch-jwks
@@ -43,9 +51,25 @@ Options:
43
51
  --check-nullifier Query Base RPC for nullifier anchor (requires network)
44
52
  --check-payer Query Base or Solana RPC. Base confirms payer, payee, asset, amount
45
53
  --solana-rpc <url> Solana RPC URL (default: https://api.mainnet-beta.solana.com or SOLANA_RPC_URL)
46
- --rpc <url> Custom RPC URL (default: https://mainnet.base.org)
54
+ --rpc <url> Base RPC URL (default: https://mainnet.base.org)
55
+ --rpc With a receipt, an inclusion proof, and a tree head: check the
56
+ leaf, then the Solana memo and the Base calldata for that root.
57
+ A v2 head also needs the signed epoch record.
58
+ --epoch-record <f> Signed epoch record JSON. Skips the network fetch.
59
+ --epoch-url <url> GET this epoch record. Default: the receipt verify_url
60
+ origin plus /v1/receipts/tree/epoch
61
+ --inclusion <file> Inclusion proof JSON (chit402.inclusion.v1)
62
+ --head <file> Signed tree head JSON (chit402.tree_head.v1 or v2)
47
63
  --json Output JSON instead of human-readable
48
64
  --quiet Only output errors
65
+ --strict-issuer-history
66
+ Fail if the issuer key history cannot be fetched
67
+ --issuer-history-file <path>
68
+ Read issuer history JSON instead of fetching it
69
+ --canonical-preimage <path>
70
+ SHA-256 this file and match the signed payload_hash
71
+ --no-issuer-history Do not check the kid's not_before / not_after window
72
+ --no-preimage Do not require published hash preimages
49
73
 
50
74
  Exit codes:
51
75
  0 = verified
@@ -63,16 +87,48 @@ Trust:
63
87
  with the unsigned outer payment / caller_binding copy is a failure.
64
88
 
65
89
  Network behavior:
66
- By default, no network requests are made. Network is only used when:
67
- - --fetch-jwks or --jwks-url is passed
68
- - --check-nullifier is passed (queries Base RPC for on-chain anchor)
69
- - --check-payer is passed (queries Base or Solana RPC for USDC settlement)
90
+ JWKS, nullifiers, and anchors are not fetched unless you pass a flag.
91
+ Issuer history is fetched by default when the receipt names a history URL
92
+ and neither --issuer-history-file nor --no-issuer-history is set. A payload
93
+ v10 receipt fails (exit 1) when that fetch cannot run. Offline mode is
94
+ --issuer-history-file <snapshot>, or --no-issuer-history to skip the window.
95
+ Other network uses:
96
+ - --fetch-jwks or --jwks-url
97
+ - --check-nullifier (Base RPC)
98
+ - --check-payer (Base or Solana RPC)
99
+ - --rpc with a receipt, an inclusion proof, and a tree head
100
+ - a v2 head, which fetches GET /v1/receipts/tree/epoch unless --epoch-record
70
101
 
71
102
  Solana payer verify uses SOLANA_RPC_URL when set, else the public mainnet RPC.
72
103
 
104
+ Anchored root:
105
+ xfuel-verify receipt.json inclusion.json head.json --rpc
106
+ xfuel-verify receipt.json --inclusion inclusion.json --head head.json --rpc https://mainnet.base.org
107
+
108
+ Checks the Merkle inclusion, fetches the Solana transaction and requires the
109
+ SPL Memo to contain the root, and checks the Base calldata the same way.
110
+ Prints what this proves and what it does not prove. Exit 0 when both chains
111
+ match, 2 when the leaf is included but an anchor is still pending, 1 when a
112
+ check fails.
113
+
114
+ Receipt lane (unsigned, beside book_seq):
115
+ settled_by is observed_transfer when the USDC transfer was checked on Base
116
+ or Solana, and receipt when only the issuer asserts settlement. Unknown is
117
+ null. Ordering is seq + settled_by + (anchor_changed AND not settled).
118
+ Boundary: complete over registry marks, blind to payments the registry
119
+ never joined. freeze is true only when book_seq is set, settled_by is
120
+ receipt, the anchor changed after binding, and the row is not settled. An
121
+ anchor change alone does not freeze, and freeze does not change the
122
+ signature exit code. classification unverifiable_from_registry means the
123
+ binding is past expiry and settled_by, receipt_id, observed_tx_hash, and
124
+ observed_transfer_id are all null. That is not unpaid. local_check, when
125
+ set, is a Base USDC payee and amount a stranger can check. It does not
126
+ claim the row was paid. Design by Turbo on 1F916 (post 6579, comments
127
+ 88201, 88403, and 88596).
128
+
73
129
  Examples:
74
- # Local binding verification (no network)
75
- xfuel-verify my-receipt.json
130
+ # Skip the issuer-history fetch. A v10 receipt otherwise needs that network call.
131
+ xfuel-verify my-receipt.json --no-issuer-history
76
132
 
77
133
  # Verify issuer signature with JWKS file
78
134
  xfuel-verify my-receipt.json --jwks-file issuer-jwks.json
@@ -95,9 +151,21 @@ function parseArgs(args) {
95
151
  checkPayer: false,
96
152
  rpcUrl: null,
97
153
  solanaRpcUrl: null,
154
+ sawRpc: false,
155
+ anchorFlag: false,
156
+ inclusionFile: null,
157
+ headFile: null,
158
+ epochRecordFile: null,
159
+ epochUrl: null,
160
+ positionals: [],
98
161
  json: false,
99
162
  quiet: false,
100
163
  help: false,
164
+ strictIssuerHistory: false,
165
+ issuerHistoryFile: null,
166
+ canonicalPreimageFile: null,
167
+ noIssuerHistory: false,
168
+ noPreimage: false,
101
169
  };
102
170
  for (let i = 0; i < args.length; i++) {
103
171
  const arg = args[i];
@@ -126,12 +194,44 @@ function parseArgs(args) {
126
194
  else if (arg === '--no-trusted-kid') {
127
195
  result.noTrustedKid = true;
128
196
  }
129
- else if (arg === '--rpc' && args[i + 1]) {
130
- result.rpcUrl = args[++i];
197
+ else if (arg === '--rpc') {
198
+ result.sawRpc = true;
199
+ const next = args[i + 1];
200
+ if (next && /^https?:\/\//i.test(next))
201
+ result.rpcUrl = args[++i];
202
+ else
203
+ result.anchorFlag = true;
204
+ }
205
+ else if (arg === '--inclusion' && args[i + 1]) {
206
+ result.inclusionFile = args[++i];
207
+ }
208
+ else if (arg === '--head' && args[i + 1]) {
209
+ result.headFile = args[++i];
210
+ }
211
+ else if (arg === '--epoch-record' && args[i + 1]) {
212
+ result.epochRecordFile = args[++i];
213
+ }
214
+ else if (arg === '--epoch-url' && args[i + 1]) {
215
+ result.epochUrl = args[++i];
131
216
  }
132
217
  else if (arg === '--solana-rpc' && args[i + 1]) {
133
218
  result.solanaRpcUrl = args[++i];
134
219
  }
220
+ else if (arg === '--strict-issuer-history') {
221
+ result.strictIssuerHistory = true;
222
+ }
223
+ else if (arg === '--issuer-history-file' && args[i + 1]) {
224
+ result.issuerHistoryFile = args[++i];
225
+ }
226
+ else if (arg === '--canonical-preimage' && args[i + 1]) {
227
+ result.canonicalPreimageFile = args[++i];
228
+ }
229
+ else if (arg === '--no-issuer-history') {
230
+ result.noIssuerHistory = true;
231
+ }
232
+ else if (arg === '--no-preimage') {
233
+ result.noPreimage = true;
234
+ }
135
235
  else if (arg === '--json') {
136
236
  result.json = true;
137
237
  }
@@ -139,7 +239,9 @@ function parseArgs(args) {
139
239
  result.quiet = true;
140
240
  }
141
241
  else if (!arg.startsWith('-')) {
142
- result.file = arg;
242
+ result.positionals.push(arg);
243
+ if (!result.file)
244
+ result.file = arg;
143
245
  }
144
246
  }
145
247
  return result;
@@ -163,11 +265,324 @@ function formatAmount(units) {
163
265
  const usd = n / 1e6;
164
266
  return `$${usd.toFixed(usd >= 0.01 ? 2 : 6)} (${units} units)`;
165
267
  }
268
+ function readJson(file) {
269
+ const content = file === '-' ? (0, node_fs_1.readFileSync)(0, 'utf8') : (0, node_fs_1.readFileSync)(file, 'utf8');
270
+ return JSON.parse(content);
271
+ }
272
+ function headStatus(file) {
273
+ try {
274
+ const doc = readJson(file);
275
+ return typeof doc?.status === 'string' ? doc.status : null;
276
+ }
277
+ catch {
278
+ return null;
279
+ }
280
+ }
281
+ function printLane(lane) {
282
+ const bit = (value) => (value == null ? 'unknown' : (value ? 'true' : 'false'));
283
+ console.log(` Receipt lane (unsigned)`);
284
+ console.log(` ─────────────────────────────────────────────────`);
285
+ console.log(` book_seq: ${lane.book_seq ?? '—'}`);
286
+ console.log(` settled_by: ${lane.settled_by ?? 'null'}`);
287
+ console.log(` settled: ${bit(lane.settled)}`);
288
+ console.log(` anchor_changed_since_binding: ${bit(lane.anchor_changed_since_binding)}`);
289
+ console.log(` freeze: ${lane.freeze ? 'YES' : 'no'}`);
290
+ if (lane.reason)
291
+ console.log(` reason: ${lane.reason}`);
292
+ console.log(` classification:${lane.classification}`);
293
+ console.log(` ordering: ${lane.ordering}`);
294
+ console.log(` boundary: ${lane.boundary}`);
295
+ if (lane.local_check) {
296
+ console.log(` local_check: payee ${lane.local_check.payee} amount ${lane.local_check.amount_atomic} (not a payment)`);
297
+ }
298
+ console.log(` ${lane.rule}`);
299
+ console.log('');
300
+ }
301
+ function printAnchor(result, json, quiet) {
302
+ if (json) {
303
+ console.log(JSON.stringify(result, null, 2));
304
+ return;
305
+ }
306
+ if (quiet && result.overall !== 'failed')
307
+ return;
308
+ const mark = (ok) => (ok ? '✓ YES' : '✗ NO');
309
+ console.log('');
310
+ console.log(' Anchored receipt root');
311
+ console.log(' ─────────────────────────────────────────────────');
312
+ console.log(` Root: ${result.root || '—'}`);
313
+ console.log(` Inclusion: ${mark(result.inclusion.valid)}${result.inclusion.reason ? ` (${result.inclusion.reason})` : ''}`);
314
+ console.log(` Leaf source: ${result.inclusion.leaf_source}`);
315
+ console.log(` Solana: ${result.solana.checked ? mark(result.solana.valid) : (result.solana.reason || 'not checked')}`);
316
+ if (result.solana.signature)
317
+ console.log(` Signature: ${result.solana.signature}`);
318
+ if (result.solana.slot != null)
319
+ console.log(` Slot: ${result.solana.slot}`);
320
+ if (result.solana.cluster)
321
+ console.log(` Cluster: ${result.solana.cluster}`);
322
+ if (result.solana.memo)
323
+ console.log(` Memo: ${result.solana.memo}`);
324
+ if (result.solana.reason && !result.solana.valid)
325
+ console.log(` Solana reason: ${result.solana.reason}`);
326
+ console.log(` Base: ${result.base.checked ? mark(result.base.valid) : (result.base.reason || 'not checked')}`);
327
+ if (result.base.tx)
328
+ console.log(` Base tx: ${result.base.tx}`);
329
+ if (result.base.chain_id != null)
330
+ console.log(` Chain id: ${result.base.chain_id}`);
331
+ if (result.base.reason && !result.base.valid)
332
+ console.log(` Base reason: ${result.base.reason}`);
333
+ console.log('');
334
+ console.log(' What this proves');
335
+ console.log(' ─────────────────────────────────────────────────');
336
+ for (const line of result.proves)
337
+ console.log(` ${line}`);
338
+ console.log('');
339
+ console.log(' What this does not prove');
340
+ console.log(' ─────────────────────────────────────────────────');
341
+ for (const line of result.does_not_prove)
342
+ console.log(` ${line}`);
343
+ console.log('');
344
+ console.log(` Overall: ${result.overall.toUpperCase()}`);
345
+ if (result.errors.length > 0)
346
+ console.log(` Errors: ${result.errors.join(', ')}`);
347
+ console.log('');
348
+ }
349
+ function headNeedsEpochRecord(head) {
350
+ const schema = head.schema;
351
+ const version = head.payload_version;
352
+ return head.epoch != null || schema === 'chit402.tree_head.v2' || version === 2;
353
+ }
354
+ function epochUrlFromReceipt(receipt) {
355
+ const raw = receipt?.verify_url;
356
+ if (!raw)
357
+ return null;
358
+ try {
359
+ return `${new URL(raw).origin}/v1/receipts/tree/epoch`;
360
+ }
361
+ catch {
362
+ return null;
363
+ }
364
+ }
365
+ async function loadEpochRecord(args, receipt) {
366
+ if (args.epochRecordFile) {
367
+ return JSON.parse((0, node_fs_1.readFileSync)(args.epochRecordFile, 'utf8'));
368
+ }
369
+ const url = args.epochUrl || epochUrlFromReceipt(receipt);
370
+ if (!url)
371
+ return null;
372
+ const res = await fetch(url);
373
+ if (!res.ok)
374
+ return null;
375
+ const body = await res.json();
376
+ if (!body?.issuer_signature?.jws)
377
+ return null;
378
+ return body;
379
+ }
380
+ function epochSignatureOk(record, jws, jwks, trustedKids) {
381
+ const sig = record.issuer_signature;
382
+ const embedded = sig && 'issuer_jwk' in sig
383
+ ? sig.issuer_jwk
384
+ : undefined;
385
+ const kid = (0, jws_js_1.readJwsHeader)(jws)?.kid;
386
+ const keys = [];
387
+ for (const key of jwks?.keys || []) {
388
+ if (!key || key.kty !== 'EC')
389
+ continue;
390
+ const jwk = key;
391
+ if (kid && jwk.kid !== kid)
392
+ continue;
393
+ keys.push(jwk);
394
+ }
395
+ if (embedded && trustedKids?.includes((0, jws_js_1.jwkThumbprint)(embedded)))
396
+ keys.push(embedded);
397
+ for (const key of keys) {
398
+ const verified = (0, jws_js_1.verifyIssuerJws)(jws, key);
399
+ if (!verified.valid || !verified.payload)
400
+ continue;
401
+ const payload = verified.payload;
402
+ if (JSON.stringify(payload.epochs ?? null) !== JSON.stringify(record.epochs ?? null))
403
+ return false;
404
+ if (JSON.stringify(payload.orphans ?? []) !== JSON.stringify(record.orphans ?? []))
405
+ return false;
406
+ return true;
407
+ }
408
+ return false;
409
+ }
410
+ async function runAnchor(args) {
411
+ const receiptPath = args.file;
412
+ const inclusionPath = args.inclusionFile || args.positionals[1] || null;
413
+ const headPath = args.headFile || (args.inclusionFile ? null : args.positionals[2]) || null;
414
+ if (!receiptPath || !inclusionPath || !headPath) {
415
+ const candidates = [args.headFile, headPath, ...args.positionals];
416
+ if (candidates.some((file) => file && headStatus(file) === 'not_yet_published')) {
417
+ console.error('Tree head is not_yet_published.');
418
+ return 3;
419
+ }
420
+ console.error('Anchor check needs a receipt, an inclusion proof, and a tree head.');
421
+ console.error(' xfuel-verify receipt.json inclusion.json head.json --rpc');
422
+ return 3;
423
+ }
424
+ let receipt;
425
+ let inclusion;
426
+ let head;
427
+ try {
428
+ receipt = readJson(receiptPath);
429
+ inclusion = readJson(inclusionPath);
430
+ head = readJson(headPath);
431
+ }
432
+ catch (err) {
433
+ console.error(`Error reading anchor inputs: ${err instanceof Error ? err.message : String(err)}`);
434
+ return 3;
435
+ }
436
+ let epochRecord = null;
437
+ let verifyEpochSignature;
438
+ if (headNeedsEpochRecord(head)) {
439
+ try {
440
+ epochRecord = await loadEpochRecord(args, receipt);
441
+ }
442
+ catch {
443
+ epochRecord = null;
444
+ }
445
+ if (epochRecord?.issuer_signature?.jws) {
446
+ let fileJwks;
447
+ if (args.jwksFile) {
448
+ try {
449
+ fileJwks = JSON.parse((0, node_fs_1.readFileSync)(args.jwksFile, 'utf8'));
450
+ }
451
+ catch (err) {
452
+ console.error(`Error reading JWKS file: ${err instanceof Error ? err.message : String(err)}`);
453
+ return 3;
454
+ }
455
+ }
456
+ const loaded = await (0, index_js_1.loadIssuerJwks)(receipt, {
457
+ jwks: fileJwks,
458
+ jwksUri: args.jwksUrl || undefined,
459
+ fetchJwks: args.fetchJwks,
460
+ });
461
+ const trustedKids = args.noTrustedKid
462
+ ? []
463
+ : (args.trustedKids ?? [...index_js_1.DEFAULT_TRUSTED_ISSUER_KIDS]);
464
+ verifyEpochSignature = (jws) => epochSignatureOk(epochRecord, jws, loaded.jwks, trustedKids);
465
+ }
466
+ }
467
+ const result = await (0, anchor_witness_js_1.verifyAnchoredRoot)({
468
+ receipt,
469
+ inclusion,
470
+ head,
471
+ baseRpcUrl: args.rpcUrl || undefined,
472
+ solanaRpcUrl: args.solanaRpcUrl || undefined,
473
+ epochRecord,
474
+ verifyEpochSignature,
475
+ });
476
+ const verified = await (0, index_js_1.verifyReceipt)(receipt, { head });
477
+ const lane = verified.receipt_lane;
478
+ if (args.json) {
479
+ console.log(JSON.stringify({ ...result, receipt_lane: lane }, null, 2));
480
+ }
481
+ else {
482
+ printAnchor(result, false, args.quiet);
483
+ if (!args.quiet)
484
+ printLane(lane);
485
+ }
486
+ if (result.overall === 'verified')
487
+ return 0;
488
+ if (result.overall === 'partial')
489
+ return 2;
490
+ return 1;
491
+ }
492
+ async function runRefusal(doc, args) {
493
+ const trustedKids = args.trustedKids ?? index_js_1.DEFAULT_TRUSTED_ISSUER_KIDS;
494
+ const result = (0, index_js_1.verifyRefusal)(doc, {
495
+ jwks: args.jwks,
496
+ trustedKids,
497
+ });
498
+ const preimages = await (0, preimage_js_1.verifyPublishedPreimages)(doc, {
499
+ requirePreimages: args.requirePreimages,
500
+ });
501
+ let canonicalFailed = false;
502
+ if (args.canonicalPreimage != null) {
503
+ const canonical = (0, canonical_preimage_js_1.verifyCanonicalPreimageBytes)(args.canonicalPreimage, result.payload_hash);
504
+ canonicalFailed = !canonical.ok;
505
+ if (canonicalFailed)
506
+ preimages.errors.push(`canonical preimage: ${canonical.reason}`);
507
+ if (canonicalFailed)
508
+ preimages.ok = false;
509
+ }
510
+ const pin = result.valid ? (0, issuer_history_js_1.readIssuerHistoryPin)({
511
+ issuer_history: result.issuer_history,
512
+ }) : null;
513
+ const history = args.skipIssuerHistory
514
+ ? { checked: false, ok: true, unreachable: false, warning: null, reason: null, kid: result.kid ?? null }
515
+ : await (0, issuer_history_js_1.checkReceiptIssuerHistory)(doc, {
516
+ document: args.issuerHistory,
517
+ documentBytes: args.issuerHistoryBytes,
518
+ fetchHistory: args.fetchIssuerHistory,
519
+ strict: args.strictIssuerHistory,
520
+ jwks: args.jwks,
521
+ trustedKids,
522
+ issuedAt: doc.issued_at ?? null,
523
+ kid: result.kid ?? doc.issuer_signature?.kid ?? null,
524
+ pin,
525
+ requirePin: result.valid && Number(result.payload_version) >= 2,
526
+ });
527
+ const historyFailed = history.checked && !history.ok;
528
+ const failed = !result.valid || !preimages.ok || historyFailed || canonicalFailed;
529
+ if (args.json) {
530
+ console.log(JSON.stringify({ ...result, preimages, issuer_history: history }, null, 2));
531
+ }
532
+ else if (!args.quiet || failed) {
533
+ console.log('');
534
+ console.log(' Chit402 Refusal Verification');
535
+ console.log(' ─────────────────────────────────────────────────');
536
+ console.log(` Schema: ${result.schema}`);
537
+ console.log(` Refusal ID: ${result.refusal_id || doc.refusal_id || '—'}`);
538
+ console.log(` Code: ${result.refusal_code || doc.refusal_code || '—'}`);
539
+ console.log(` Nonce: ${result.nonce || '—'}`);
540
+ console.log(` Chain: ${result.chain_id ?? 'null'}`);
541
+ console.log(` Charged: false`);
542
+ console.log('');
543
+ console.log(' Issuer Signature');
544
+ console.log(' ─────────────────────────────────────────────────');
545
+ console.log(` Valid: ${result.valid ? '✓ YES' : '✗ NO'}`);
546
+ if (result.kid)
547
+ console.log(` Kid: ${result.kid}`);
548
+ if (!result.valid && result.reason)
549
+ console.log(` Reason: ${result.reason}`);
550
+ console.log('');
551
+ console.log(' What this proves');
552
+ console.log(' ─────────────────────────────────────────────────');
553
+ for (const line of result.proves)
554
+ console.log(` ${line}`);
555
+ console.log('');
556
+ console.log(' What this does not prove');
557
+ console.log(' ─────────────────────────────────────────────────');
558
+ for (const line of result.does_not_prove)
559
+ console.log(` ${line}`);
560
+ console.log('');
561
+ console.log(` Overall: ${failed ? 'FAILED' : 'VERIFIED'}`);
562
+ if (!preimages.ok)
563
+ console.log(` Preimages: ${preimages.errors.join('; ')}`);
564
+ if (history.warning)
565
+ console.log(` Issuer history: ${history.warning}`);
566
+ if (historyFailed && history.reason)
567
+ console.log(` Issuer history: ${history.reason}`);
568
+ console.log('');
569
+ }
570
+ return failed ? 1 : 0;
571
+ }
166
572
  async function main() {
167
573
  const args = parseArgs(process.argv.slice(2));
168
- if (args.help || !args.file) {
574
+ const anchorMode = args.anchorFlag
575
+ || Boolean(args.inclusionFile || args.headFile)
576
+ || (args.positionals.length >= 3 && args.sawRpc);
577
+ if (args.help) {
169
578
  console.log(HELP);
170
- return args.help ? 0 : 3;
579
+ return 0;
580
+ }
581
+ if (anchorMode)
582
+ return runAnchor(args);
583
+ if (!args.file) {
584
+ console.log(HELP);
585
+ return 3;
171
586
  }
172
587
  let receipt;
173
588
  try {
@@ -195,7 +610,54 @@ async function main() {
195
610
  }
196
611
  const trustedKids = args.noTrustedKid
197
612
  ? []
198
- : (args.trustedKids ?? undefined);
613
+ : (args.trustedKids ?? index_js_1.DEFAULT_TRUSTED_ISSUER_KIDS);
614
+ if ((0, index_js_1.isRefusalDocument)(receipt)) {
615
+ let issuerHistory = null;
616
+ let issuerHistoryBytes = null;
617
+ if (args.issuerHistoryFile) {
618
+ issuerHistoryBytes = (0, node_fs_1.readFileSync)(args.issuerHistoryFile, 'utf8');
619
+ issuerHistory = JSON.parse(issuerHistoryBytes);
620
+ }
621
+ let canonicalPreimage = null;
622
+ if (args.canonicalPreimageFile) {
623
+ canonicalPreimage = (0, node_fs_1.readFileSync)(args.canonicalPreimageFile, 'utf8');
624
+ }
625
+ return runRefusal(receipt, {
626
+ jwks,
627
+ trustedKids,
628
+ json: args.json,
629
+ quiet: args.quiet,
630
+ requirePreimages: !args.noPreimage,
631
+ issuerHistory,
632
+ issuerHistoryBytes,
633
+ fetchIssuerHistory: !args.noIssuerHistory && !issuerHistory,
634
+ strictIssuerHistory: args.strictIssuerHistory,
635
+ skipIssuerHistory: args.noIssuerHistory,
636
+ canonicalPreimage,
637
+ });
638
+ }
639
+ let issuerHistory = null;
640
+ let issuerHistoryBytes = null;
641
+ if (args.issuerHistoryFile) {
642
+ try {
643
+ issuerHistoryBytes = (0, node_fs_1.readFileSync)(args.issuerHistoryFile, 'utf8');
644
+ issuerHistory = JSON.parse(issuerHistoryBytes);
645
+ }
646
+ catch (err) {
647
+ console.error(`Error reading issuer history: ${err instanceof Error ? err.message : String(err)}`);
648
+ return 3;
649
+ }
650
+ }
651
+ let canonicalPreimage = null;
652
+ if (args.canonicalPreimageFile) {
653
+ try {
654
+ canonicalPreimage = (0, node_fs_1.readFileSync)(args.canonicalPreimageFile, 'utf8');
655
+ }
656
+ catch (err) {
657
+ console.error(`Error reading canonical preimage: ${err instanceof Error ? err.message : String(err)}`);
658
+ return 3;
659
+ }
660
+ }
199
661
  const result = await (0, index_js_1.verifyReceipt)(receipt, {
200
662
  jwks,
201
663
  jwksUri: args.jwksUrl || undefined,
@@ -205,6 +667,13 @@ async function main() {
205
667
  checkPayer: args.checkPayer,
206
668
  rpcUrl: args.rpcUrl || undefined,
207
669
  solanaRpcUrl: args.solanaRpcUrl || undefined,
670
+ requirePreimages: !args.noPreimage,
671
+ issuerHistory,
672
+ issuerHistoryBytes,
673
+ fetchIssuerHistory: !args.noIssuerHistory && !issuerHistory,
674
+ strictIssuerHistory: args.strictIssuerHistory,
675
+ skipIssuerHistory: args.noIssuerHistory,
676
+ canonicalPreimage,
208
677
  });
209
678
  if (args.json) {
210
679
  console.log(JSON.stringify(result, null, 2));
@@ -292,6 +761,16 @@ async function main() {
292
761
  console.log(` Status: No nullifier (Tier-1 receipt)`);
293
762
  }
294
763
  console.log('');
764
+ printLane(result.receipt_lane);
765
+ if (result.preimages.checked) {
766
+ console.log(` Preimages: ${result.preimages.ok ? '✓ MATCH' : '✗ ' + result.preimages.errors.join('; ')}`);
767
+ }
768
+ if (result.issuer_history.checked) {
769
+ console.log(` Issuer history: ${result.issuer_history.ok ? '✓ kid in window' : '✗ ' + (result.issuer_history.reason || 'failed')}`);
770
+ }
771
+ else if (result.issuer_history.warning) {
772
+ console.log(` Issuer history: ${result.issuer_history.warning}`);
773
+ }
295
774
  console.log(` Overall: ${result.overall.toUpperCase()}`);
296
775
  if (result.errors.length > 0) {
297
776
  console.log(` Errors: ${result.errors.join(', ')}`);
@@ -309,10 +788,43 @@ async function main() {
309
788
  return 1;
310
789
  }
311
790
  }
791
+ /**
792
+ * Node's fetch keeps its undici agent on this symbol after the first request.
793
+ * There is no public handle for that pool. Closing it lets the loop drain.
794
+ */
795
+ const UNDICI_GLOBAL_DISPATCHER = Symbol.for('undici.globalDispatcher.1');
796
+ function fetchDispatcher() {
797
+ const bag = globalThis;
798
+ return bag[UNDICI_GLOBAL_DISPATCHER];
799
+ }
800
+ /**
801
+ * process.exit() while a fetch keep-alive socket is already CLOSING aborts
802
+ * on Windows: Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)
803
+ * (src/win/async.c). Set the code and close the pool so the loop can drain.
804
+ */
805
+ async function finish(code) {
806
+ process.exitCode = code;
807
+ const dispatcher = fetchDispatcher();
808
+ if (!dispatcher)
809
+ return;
810
+ try {
811
+ const closing = dispatcher.close?.();
812
+ if (closing)
813
+ await closing;
814
+ }
815
+ catch {
816
+ try {
817
+ await dispatcher.destroy?.();
818
+ }
819
+ catch {
820
+ // The verification result is already the exit code.
821
+ }
822
+ }
823
+ }
312
824
  main()
313
- .then((code) => process.exit(code))
825
+ .then((code) => finish(code))
314
826
  .catch((err) => {
315
827
  console.error('Unexpected error:', err);
316
- process.exit(3);
828
+ return finish(3);
317
829
  });
318
830
  //# sourceMappingURL=cli.js.map