@xfuel/verify 0.3.0 → 0.3.3

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 (56) hide show
  1. package/README.md +37 -16
  2. package/dist/anchor-trust.d.ts +129 -0
  3. package/dist/anchor-trust.d.ts.map +1 -0
  4. package/dist/anchor-trust.js +395 -0
  5. package/dist/anchor-trust.js.map +1 -0
  6. package/dist/anchor-witness.d.ts +134 -7
  7. package/dist/anchor-witness.d.ts.map +1 -1
  8. package/dist/anchor-witness.js +497 -44
  9. package/dist/anchor-witness.js.map +1 -1
  10. package/dist/carry-forward.d.ts +79 -0
  11. package/dist/carry-forward.d.ts.map +1 -0
  12. package/dist/carry-forward.js +184 -0
  13. package/dist/carry-forward.js.map +1 -0
  14. package/dist/cli.js +674 -30
  15. package/dist/cli.js.map +1 -1
  16. package/dist/epoch.d.ts +143 -0
  17. package/dist/epoch.d.ts.map +1 -0
  18. package/dist/epoch.js +352 -0
  19. package/dist/epoch.js.map +1 -0
  20. package/dist/index.d.ts +86 -5
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +395 -118
  23. package/dist/index.js.map +1 -1
  24. package/dist/issuer-history.d.ts +19 -0
  25. package/dist/issuer-history.d.ts.map +1 -1
  26. package/dist/issuer-history.js +131 -75
  27. package/dist/issuer-history.js.map +1 -1
  28. package/dist/issuer-pin.d.ts +126 -0
  29. package/dist/issuer-pin.d.ts.map +1 -0
  30. package/dist/issuer-pin.js +567 -0
  31. package/dist/issuer-pin.js.map +1 -0
  32. package/dist/preimage.d.ts +29 -0
  33. package/dist/preimage.d.ts.map +1 -1
  34. package/dist/preimage.js +162 -2
  35. package/dist/preimage.js.map +1 -1
  36. package/dist/receipt-lane.d.ts +1 -0
  37. package/dist/receipt-lane.d.ts.map +1 -1
  38. package/dist/receipt-lane.js +5 -2
  39. package/dist/receipt-lane.js.map +1 -1
  40. package/dist/refusal.d.ts +5 -2
  41. package/dist/refusal.d.ts.map +1 -1
  42. package/dist/refusal.js +28 -6
  43. package/dist/refusal.js.map +1 -1
  44. package/dist/request-binding.d.ts +7 -0
  45. package/dist/request-binding.d.ts.map +1 -0
  46. package/dist/request-binding.js +26 -0
  47. package/dist/request-binding.js.map +1 -0
  48. package/dist/row-hash.d.ts +22 -0
  49. package/dist/row-hash.d.ts.map +1 -0
  50. package/dist/row-hash.js +26 -0
  51. package/dist/row-hash.js.map +1 -0
  52. package/dist/v11-receipt.d.ts +21 -0
  53. package/dist/v11-receipt.d.ts.map +1 -0
  54. package/dist/v11-receipt.js +351 -0
  55. package/dist/v11-receipt.js.map +1 -0
  56. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -17,12 +17,17 @@
17
17
  * 3 = input error
18
18
  */
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
+ const node_module_1 = require("node:module");
20
21
  const node_fs_1 = require("node:fs");
21
22
  const index_js_1 = require("./index.js");
22
23
  const anchor_witness_js_1 = require("./anchor-witness.js");
24
+ const jws_js_1 = require("./jws.js");
23
25
  const preimage_js_1 = require("./preimage.js");
24
26
  const issuer_history_js_1 = require("./issuer-history.js");
25
27
  const canonical_preimage_js_1 = require("./canonical-preimage.js");
28
+ const v11_receipt_js_1 = require("./v11-receipt.js");
29
+ const issuer_pin_js_1 = require("./issuer-pin.js");
30
+ const anchor_trust_js_1 = require("./anchor-trust.js");
26
31
  const HELP = `
27
32
  xfuel-verify — Offline verification for Chit402 receipts
28
33
 
@@ -51,10 +56,35 @@ Options:
51
56
  --check-payer Query Base or Solana RPC. Base confirms payer, payee, asset, amount
52
57
  --solana-rpc <url> Solana RPC URL (default: https://api.mainnet-beta.solana.com or SOLANA_RPC_URL)
53
58
  --rpc <url> Base RPC URL (default: https://mainnet.base.org)
59
+ --version Print @xfuel/verify version and exit
54
60
  --rpc With a receipt, an inclusion proof, and a tree head: check the
55
- leaf, then the Solana memo and the Base calldata for that root
61
+ leaf, then the Solana memo and the Base calldata for that root.
62
+ The tree head must carry a valid ES256 issuer signature, and
63
+ the anchor wallets must match the signed head.
64
+ A v2 head also needs the signed epoch record.
65
+ A tree-head file with --rpc enters this mode.
66
+ The same files without --rpc check the head signature and the
67
+ index/size-bound inclusion offline. They do not call chain RPC.
68
+ --epoch-record <f> Signed epoch record JSON. Skips the network fetch.
69
+ --epoch-url <url> GET this epoch record. Default: the receipt verify_url
70
+ origin plus /v1/receipts/tree/epoch
56
71
  --inclusion <file> Inclusion proof JSON (chit402.inclusion.v1)
57
- --head <file> Signed tree head JSON (chit402.tree_head.v1)
72
+ --head <file> Signed tree head JSON (chit402.tree_head.v1 or v2)
73
+ --anchor-wallets-file <path>
74
+ Signed chit402.anchor_wallets.v1 document. Wallets in a
75
+ verified document are added to the package pin. A head
76
+ that names any other wallet fails.
77
+ --carry-old-head <file>
78
+ --carry-old-inclusion <file>
79
+ --carry-head <file>
80
+ --carry-inclusion <file>
81
+ Earlier signed head, its inclusion proof, the current
82
+ signed head, and the current inclusion proof. Repeat
83
+ --carry-inclusion for each current index of the leaf.
84
+ The issued time and epoch are the earlier head's signed
85
+ claims. The logged time, epoch, and leaf index are the
86
+ current head's signed claims. A receipt in the current
87
+ epoch does not use these flags.
58
88
  --json Output JSON instead of human-readable
59
89
  --quiet Only output errors
60
90
  --strict-issuer-history
@@ -78,15 +108,24 @@ Trust:
78
108
  issuer_jwk whose RFC 7638 thumbprint equals a pinned trusted kid. The
79
109
  embedded key alone is not a trust root. Untrusted keys report "key untrusted".
80
110
 
81
- Amount, payer, payee, and tx are read from verified signed claims. A mismatch
82
- with the unsigned outer payment / caller_binding copy is a failure.
111
+ Amount, payer, payee, and tx are read from verified signed claims. Every
112
+ signed field is compared with the outer receipt at the same path. A mismatch
113
+ fails. Outer fields the signature does not cover are printed as UNVERIFIED.
114
+ The outer document schema is not a copy of the JWS schema. Log mode (a
115
+ receipt, an inclusion proof, and a tree head) runs these receipt checks too.
83
116
 
84
117
  Network behavior:
85
- By default, no network requests are made. Network is only used when:
86
- - --fetch-jwks or --jwks-url is passed
87
- - --check-nullifier is passed (queries Base RPC for on-chain anchor)
88
- - --check-payer is passed (queries Base or Solana RPC for USDC settlement)
89
- - --rpc is passed with a receipt, an inclusion proof, and a tree head
118
+ JWKS, nullifiers, and anchors are not fetched unless you pass a flag.
119
+ Issuer history is fetched by default when the receipt names a history URL
120
+ and neither --issuer-history-file nor --no-issuer-history is set. A payload
121
+ v10 receipt fails (exit 1) when that fetch cannot run. Offline mode is
122
+ --issuer-history-file <snapshot>, or --no-issuer-history to skip the window.
123
+ Other network uses:
124
+ - --fetch-jwks or --jwks-url
125
+ - --check-nullifier (Base RPC)
126
+ - --check-payer (Base or Solana RPC)
127
+ - --rpc with a receipt, an inclusion proof, and a tree head
128
+ - a v2 head, which fetches GET /v1/receipts/tree/epoch unless --epoch-record
90
129
 
91
130
  Solana payer verify uses SOLANA_RPC_URL when set, else the public mainnet RPC.
92
131
 
@@ -94,11 +133,34 @@ Anchored root:
94
133
  xfuel-verify receipt.json inclusion.json head.json --rpc
95
134
  xfuel-verify receipt.json --inclusion inclusion.json --head head.json --rpc https://mainnet.base.org
96
135
 
97
- Checks the Merkle inclusion, fetches the Solana transaction and requires the
98
- SPL Memo to contain the root, and checks the Base calldata the same way.
99
- Prints what this proves and what it does not prove. Exit 0 when both chains
100
- match, 2 when the leaf is included but an anchor is still pending, 1 when a
101
- check fails.
136
+ Also runs the single-receipt checks (issuer signature and signed-versus-outer
137
+ comparison). Those must pass before Overall can be VERIFIED. Then it checks
138
+ the tree head's ES256 issuer signature (production pin, issuer
139
+ history, or JWKS, same rules as a receipt). The signed root, size, epoch,
140
+ and anchors must match the head. Then it checks Merkle inclusion, requires
141
+ the Solana memo and fee payer, and requires the Base calldata and sender.
142
+ The sender and fee payer must be on the issuer anchor-wallet list (the
143
+ package pin, plus a verified /.well-known/anchor-wallets.json or
144
+ issuer-history anchor_wallets list). The on-chain issuer-root registry and
145
+ the DNS anchor are not read. Prints what this proves and what it does not
146
+ prove. Exit 0 when the receipt checks pass and both chains match, 2 when the
147
+ leaf is included but an anchor is still pending, or when the leaf is past
148
+ the anchored tree (PENDING, not a tamper failure), and the receipt checks
149
+ did not fail, 1 when a check fails. A pending leaf must sit at an index
150
+ at or past the anchored size, and PENDING applies only when the receipt
151
+ itself verifies. A failed signature, field, or preimage is a failure.
152
+ With --rpc, a claimed anchor must name a Solana fee payer that is on the
153
+ anchor-wallet list, and the head root must equal the newest memo that
154
+ payer actually paid. A memo from any other payer is ignored.
155
+
156
+ A receipt or head with issuer_key_pin is a pinned era. Pass the pin file
157
+ and its commit SHA plus SHA-256. The verifier does not fetch a branch.
158
+ --issuer-pin <file> --issuer-pin-commit <40-hex> --issuer-pin-sha256 <64-hex>
159
+ Optional with that pin: --issuer-pin-sig, --issuer-witnesses,
160
+ --issuer-prior-pin, --issuer-prior-commit, --issuer-prior-sha256, --issuer-control.
161
+ Those flags do not start the check on their own. A prior pin does not
162
+ replace the published specimen key. Sepolia only (eip155:84532).
163
+ See docs/product/issuer-key-pin.md.
102
164
 
103
165
  Receipt lane (unsigned, beside book_seq):
104
166
  settled_by is observed_transfer when the USDC transfer was checked on Base
@@ -116,8 +178,8 @@ Receipt lane (unsigned, beside book_seq):
116
178
  88201, 88403, and 88596).
117
179
 
118
180
  Examples:
119
- # Local binding verification (no network)
120
- xfuel-verify my-receipt.json
181
+ # Skip the issuer-history fetch. A v10 receipt otherwise needs that network call.
182
+ xfuel-verify my-receipt.json --no-issuer-history
121
183
 
122
184
  # Verify issuer signature with JWKS file
123
185
  xfuel-verify my-receipt.json --jwks-file issuer-jwks.json
@@ -144,6 +206,12 @@ function parseArgs(args) {
144
206
  anchorFlag: false,
145
207
  inclusionFile: null,
146
208
  headFile: null,
209
+ epochRecordFile: null,
210
+ epochUrl: null,
211
+ carryOldHeadFile: null,
212
+ carryOldInclusionFile: null,
213
+ carryHeadFile: null,
214
+ carryInclusionFiles: [],
147
215
  positionals: [],
148
216
  json: false,
149
217
  quiet: false,
@@ -153,10 +221,26 @@ function parseArgs(args) {
153
221
  canonicalPreimageFile: null,
154
222
  noIssuerHistory: false,
155
223
  noPreimage: false,
224
+ issuerPinFile: null,
225
+ issuerPinCommit: null,
226
+ issuerPinSha256: null,
227
+ issuerPinSigFile: null,
228
+ issuerWitnessesFile: null,
229
+ issuerPriorPinFile: null,
230
+ issuerPriorCommit: null,
231
+ issuerPriorSha256: null,
232
+ issuerControlFile: null,
233
+ version: false,
234
+ anchorWalletsFile: null,
235
+ salt: null,
236
+ open: [],
156
237
  };
157
238
  for (let i = 0; i < args.length; i++) {
158
239
  const arg = args[i];
159
- if (arg === '--help' || arg === '-h') {
240
+ if (arg === '--version' || arg === '-V') {
241
+ result.version = true;
242
+ }
243
+ else if (arg === '--help' || arg === '-h') {
160
244
  result.help = true;
161
245
  }
162
246
  else if (arg === '--check-nullifier') {
@@ -195,6 +279,24 @@ function parseArgs(args) {
195
279
  else if (arg === '--head' && args[i + 1]) {
196
280
  result.headFile = args[++i];
197
281
  }
282
+ else if (arg === '--epoch-record' && args[i + 1]) {
283
+ result.epochRecordFile = args[++i];
284
+ }
285
+ else if (arg === '--epoch-url' && args[i + 1]) {
286
+ result.epochUrl = args[++i];
287
+ }
288
+ else if (arg === '--carry-old-head' && args[i + 1]) {
289
+ result.carryOldHeadFile = args[++i];
290
+ }
291
+ else if (arg === '--carry-old-inclusion' && args[i + 1]) {
292
+ result.carryOldInclusionFile = args[++i];
293
+ }
294
+ else if (arg === '--carry-head' && args[i + 1]) {
295
+ result.carryHeadFile = args[++i];
296
+ }
297
+ else if (arg === '--carry-inclusion' && args[i + 1]) {
298
+ result.carryInclusionFiles.push(args[++i]);
299
+ }
198
300
  else if (arg === '--solana-rpc' && args[i + 1]) {
199
301
  result.solanaRpcUrl = args[++i];
200
302
  }
@@ -204,15 +306,51 @@ function parseArgs(args) {
204
306
  else if (arg === '--issuer-history-file' && args[i + 1]) {
205
307
  result.issuerHistoryFile = args[++i];
206
308
  }
309
+ else if (arg === '--salt' && args[i + 1]) {
310
+ result.salt = args[++i];
311
+ }
312
+ else if (arg === '--open' && args[i + 1]) {
313
+ result.open.push(args[++i]);
314
+ }
207
315
  else if (arg === '--canonical-preimage' && args[i + 1]) {
208
316
  result.canonicalPreimageFile = args[++i];
209
317
  }
210
318
  else if (arg === '--no-issuer-history') {
211
319
  result.noIssuerHistory = true;
212
320
  }
321
+ else if (arg === '--anchor-wallets-file' && args[i + 1]) {
322
+ result.anchorWalletsFile = args[++i];
323
+ }
213
324
  else if (arg === '--no-preimage') {
214
325
  result.noPreimage = true;
215
326
  }
327
+ else if (arg === '--issuer-pin' && args[i + 1]) {
328
+ result.issuerPinFile = args[++i];
329
+ }
330
+ else if (arg === '--issuer-pin-commit' && args[i + 1]) {
331
+ result.issuerPinCommit = args[++i];
332
+ }
333
+ else if (arg === '--issuer-pin-sha256' && args[i + 1]) {
334
+ result.issuerPinSha256 = args[++i];
335
+ }
336
+ else if (arg === '--issuer-pin-sig' && args[i + 1]) {
337
+ result.issuerPinSigFile = args[++i];
338
+ }
339
+ else if (arg === '--issuer-witnesses' && args[i + 1]) {
340
+ result.issuerWitnessesFile = args[++i];
341
+ }
342
+ else if (arg === '--issuer-prior-pin' && args[i + 1]) {
343
+ result.issuerPriorPinFile = args[++i];
344
+ }
345
+ else if (arg === '--issuer-prior-commit' && args[i + 1]) {
346
+ result.issuerPriorCommit = args[++i];
347
+ }
348
+ else if (arg === '--issuer-prior-sha256' && args[i + 1]) {
349
+ result.issuerPriorSha256 = args[++i];
350
+ }
351
+ else if (arg === '--issuer-control' && args[i + 1]) {
352
+ result.issuerControlFile = args[++i];
353
+ }
216
354
  else if (arg === '--json') {
217
355
  result.json = true;
218
356
  }
@@ -250,6 +388,15 @@ function readJson(file) {
250
388
  const content = file === '-' ? (0, node_fs_1.readFileSync)(0, 'utf8') : (0, node_fs_1.readFileSync)(file, 'utf8');
251
389
  return JSON.parse(content);
252
390
  }
391
+ function headStatus(file) {
392
+ try {
393
+ const doc = readJson(file);
394
+ return typeof doc?.status === 'string' ? doc.status : null;
395
+ }
396
+ catch {
397
+ return null;
398
+ }
399
+ }
253
400
  function printLane(lane) {
254
401
  const bit = (value) => (value == null ? 'unknown' : (value ? 'true' : 'false'));
255
402
  console.log(` Receipt lane (unsigned)`);
@@ -282,7 +429,18 @@ function printAnchor(result, json, quiet) {
282
429
  console.log(' Anchored receipt root');
283
430
  console.log(' ─────────────────────────────────────────────────');
284
431
  console.log(` Root: ${result.root || '—'}`);
285
- console.log(` Inclusion: ${mark(result.inclusion.valid)}${result.inclusion.reason ? ` (${result.inclusion.reason})` : ''}`);
432
+ console.log(` Head signature:${result.head_signature.valid ? ' ✓ YES' : ' ✗ NO'}${result.head_signature.trust ? ` (${result.head_signature.trust})` : ''}`);
433
+ if (result.head_signature.kid)
434
+ console.log(` Head kid: ${result.head_signature.kid}`);
435
+ if (result.head_signature.message)
436
+ console.log(` Head reason: ${result.head_signature.message}`);
437
+ const unlogged = result.inclusion.unlogged_reason ? ` unlogged:${result.inclusion.unlogged_reason}` : '';
438
+ if (result.overall === 'pending') {
439
+ console.log(` Inclusion: PENDING${result.inclusion.reason ? ` (${result.inclusion.reason})` : ''}`);
440
+ }
441
+ else {
442
+ console.log(` Inclusion: ${mark(result.inclusion.valid)}${result.inclusion.reason ? ` (${result.inclusion.reason})` : ''}${unlogged}`);
443
+ }
286
444
  console.log(` Leaf source: ${result.inclusion.leaf_source}`);
287
445
  console.log(` Solana: ${result.solana.checked ? mark(result.solana.valid) : (result.solana.reason || 'not checked')}`);
288
446
  if (result.solana.signature)
@@ -291,11 +449,18 @@ function printAnchor(result, json, quiet) {
291
449
  console.log(` Slot: ${result.solana.slot}`);
292
450
  if (result.solana.cluster)
293
451
  console.log(` Cluster: ${result.solana.cluster}`);
452
+ if (result.solana.fee_payer)
453
+ console.log(` Fee payer: ${result.solana.fee_payer}`);
294
454
  if (result.solana.memo)
295
455
  console.log(` Memo: ${result.solana.memo}`);
296
456
  if (result.solana.reason && !result.solana.valid)
297
457
  console.log(` Solana reason: ${result.solana.reason}`);
458
+ if (result.issuer_pin?.checked) {
459
+ console.log(` Issuer pin: ${result.issuer_pin.ok ? '✓ YES' : '✗ NO'}${result.issuer_pin.code ? ` (${result.issuer_pin.code})` : ''}`);
460
+ }
298
461
  console.log(` Base: ${result.base.checked ? mark(result.base.valid) : (result.base.reason || 'not checked')}`);
462
+ if (result.base.from)
463
+ console.log(` Base from: ${result.base.from}`);
299
464
  if (result.base.tx)
300
465
  console.log(` Base tx: ${result.base.tx}`);
301
466
  if (result.base.chain_id != null)
@@ -313,15 +478,207 @@ function printAnchor(result, json, quiet) {
313
478
  for (const line of result.does_not_prove)
314
479
  console.log(` ${line}`);
315
480
  console.log('');
481
+ if (result.overall === 'pending') {
482
+ console.log(` Anchored size: ${result.inclusion.anchored_tree_size ?? '—'}`);
483
+ console.log(` Live size: ${result.inclusion.live_tree_size ?? '—'}`);
484
+ }
316
485
  console.log(` Overall: ${result.overall.toUpperCase()}`);
317
486
  if (result.errors.length > 0)
318
487
  console.log(` Errors: ${result.errors.join(', ')}`);
488
+ if (result.errors.includes(anchor_trust_js_1.LEGACY_HEAD_UNPINNED_SIGNER)) {
489
+ console.log(` ${anchor_trust_js_1.HEAD_TRUST_MESSAGES[anchor_trust_js_1.LEGACY_HEAD_UNPINNED_SIGNER]}`);
490
+ }
319
491
  console.log('');
320
492
  }
493
+ function headNeedsEpochRecord(head) {
494
+ const schema = head.schema;
495
+ const version = head.payload_version;
496
+ return head.epoch != null || schema === 'chit402.tree_head.v2' || version === 2;
497
+ }
498
+ function epochUrlFromReceipt(receipt) {
499
+ const raw = receipt?.verify_url;
500
+ if (!raw)
501
+ return null;
502
+ try {
503
+ return `${new URL(raw).origin}/v1/receipts/tree/epoch`;
504
+ }
505
+ catch {
506
+ return null;
507
+ }
508
+ }
509
+ async function loadEpochRecord(args, receipt) {
510
+ if (args.epochRecordFile) {
511
+ return JSON.parse((0, node_fs_1.readFileSync)(args.epochRecordFile, 'utf8'));
512
+ }
513
+ const url = args.epochUrl || epochUrlFromReceipt(receipt);
514
+ if (!url)
515
+ return null;
516
+ const res = await fetch(url);
517
+ if (!res.ok)
518
+ return null;
519
+ const body = await res.json();
520
+ if (!body?.issuer_signature?.jws)
521
+ return null;
522
+ return body;
523
+ }
524
+ function epochSignatureOk(record, jws, jwks, trustedKids) {
525
+ const sig = record.issuer_signature;
526
+ const embedded = sig && 'issuer_jwk' in sig
527
+ ? sig.issuer_jwk
528
+ : undefined;
529
+ const kid = (0, jws_js_1.readJwsHeader)(jws)?.kid;
530
+ const keys = [];
531
+ for (const key of jwks?.keys || []) {
532
+ if (!key || key.kty !== 'EC')
533
+ continue;
534
+ const jwk = key;
535
+ if (kid && jwk.kid !== kid)
536
+ continue;
537
+ keys.push(jwk);
538
+ }
539
+ if (embedded && trustedKids?.includes((0, jws_js_1.jwkThumbprint)(embedded)))
540
+ keys.push(embedded);
541
+ for (const key of keys) {
542
+ const verified = (0, jws_js_1.verifyIssuerJws)(jws, key);
543
+ if (!verified.valid || !verified.payload)
544
+ continue;
545
+ const payload = verified.payload;
546
+ if (JSON.stringify(payload.epochs ?? null) !== JSON.stringify(record.epochs ?? null))
547
+ return false;
548
+ if (JSON.stringify(payload.orphans ?? []) !== JSON.stringify(record.orphans ?? []))
549
+ return false;
550
+ const payloadUnlogged = payload.unlogged === undefined ? null : payload.unlogged;
551
+ const recordUnlogged = record.unlogged === undefined ? null : record.unlogged;
552
+ if (JSON.stringify(payloadUnlogged) !== JSON.stringify(recordUnlogged))
553
+ return false;
554
+ return true;
555
+ }
556
+ return false;
557
+ }
558
+ function packageVersion() {
559
+ const require = (0, node_module_1.createRequire)(__filename);
560
+ const pkg = require('../package.json');
561
+ return pkg.version || '0.0.0';
562
+ }
563
+ function looksLikeTreeHead(value) {
564
+ if (!value || typeof value !== 'object')
565
+ return false;
566
+ const doc = value;
567
+ if (doc.status === 'not_yet_published')
568
+ return true;
569
+ if (typeof doc.schema === 'string' && doc.schema.startsWith('chit402.tree_head.'))
570
+ return true;
571
+ return Boolean(doc.anchors && doc.root && !doc.task_id);
572
+ }
573
+ function readOfflineWitness(args) {
574
+ let head = null;
575
+ let inclusion = null;
576
+ for (const file of args.positionals.slice(1)) {
577
+ let doc;
578
+ try {
579
+ doc = readJson(file);
580
+ }
581
+ catch (err) {
582
+ throw new Error(`Error reading ${file}: ${err instanceof Error ? err.message : String(err)}`);
583
+ }
584
+ const row = doc;
585
+ if (looksLikeTreeHead(doc))
586
+ head = doc;
587
+ else if (doc && typeof doc === 'object' && (Array.isArray(row.proof) || row.leaf_index != null)) {
588
+ inclusion = doc;
589
+ }
590
+ }
591
+ return { head, inclusion };
592
+ }
593
+ function fileLooksLikeHead(file) {
594
+ if (!file)
595
+ return false;
596
+ try {
597
+ return looksLikeTreeHead(readJson(file));
598
+ }
599
+ catch {
600
+ return false;
601
+ }
602
+ }
603
+ function historyHostAllowed(url) {
604
+ try {
605
+ const parsed = new URL(url);
606
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
607
+ return false;
608
+ if (parsed.protocol === 'http:' && parsed.hostname !== '127.0.0.1' && parsed.hostname !== 'localhost')
609
+ return false;
610
+ if (parsed.protocol === 'https:') {
611
+ return index_js_1.DEFAULT_TRUSTED_JWKS_HOSTS.some((host) => host.toLowerCase() === parsed.hostname.toLowerCase());
612
+ }
613
+ return true;
614
+ }
615
+ catch {
616
+ return false;
617
+ }
618
+ }
619
+ async function loadAnchorIssuerHistory(args, receipt) {
620
+ if (args.issuerHistoryFile) {
621
+ return JSON.parse((0, node_fs_1.readFileSync)(args.issuerHistoryFile, 'utf8'));
622
+ }
623
+ if (args.noIssuerHistory)
624
+ return null;
625
+ const url = (0, issuer_history_js_1.historyUrlFromReceipt)(receipt);
626
+ if (!url || !historyHostAllowed(url))
627
+ return null;
628
+ try {
629
+ const res = await fetch(url, { signal: AbortSignal.timeout(4000) });
630
+ if (!res.ok)
631
+ return null;
632
+ return await res.json();
633
+ }
634
+ catch {
635
+ return null;
636
+ }
637
+ }
638
+ async function loadAnchorWalletDocument(args, receipt) {
639
+ if (args.anchorWalletsFile) {
640
+ return JSON.parse((0, node_fs_1.readFileSync)(args.anchorWalletsFile, 'utf8'));
641
+ }
642
+ if (args.noIssuerHistory)
643
+ return null;
644
+ const historyUrl = (0, issuer_history_js_1.historyUrlFromReceipt)(receipt);
645
+ const epochOrigin = (() => {
646
+ if (!args.epochUrl)
647
+ return null;
648
+ try {
649
+ return new URL(args.epochUrl).origin;
650
+ }
651
+ catch {
652
+ return null;
653
+ }
654
+ })();
655
+ const origin = epochOrigin || (historyUrl ? new URL(historyUrl).origin : null);
656
+ if (!origin)
657
+ return null;
658
+ const url = `${origin}/.well-known/anchor-wallets.json`;
659
+ if (!historyHostAllowed(url))
660
+ return null;
661
+ try {
662
+ const res = await fetch(url, { signal: AbortSignal.timeout(4000) });
663
+ if (res.status === 404)
664
+ return null;
665
+ if (!res.ok)
666
+ return null;
667
+ return await res.json();
668
+ }
669
+ catch {
670
+ return null;
671
+ }
672
+ }
321
673
  async function runAnchor(args) {
322
674
  const receiptPath = args.file;
323
675
  const inclusionPath = args.inclusionFile || args.positionals[1] || null;
324
676
  const headPath = args.headFile || (args.inclusionFile ? null : args.positionals[2]) || null;
677
+ const headCandidates = [args.headFile, headPath, ...args.positionals];
678
+ if (headCandidates.some((file) => file && headStatus(file) === 'not_yet_published')) {
679
+ console.error('Tree head is not_yet_published.');
680
+ return 3;
681
+ }
325
682
  if (!receiptPath || !inclusionPath || !headPath) {
326
683
  console.error('Anchor check needs a receipt, an inclusion proof, and a tree head.');
327
684
  console.error(' xfuel-verify receipt.json inclusion.json head.json --rpc');
@@ -339,29 +696,198 @@ async function runAnchor(args) {
339
696
  console.error(`Error reading anchor inputs: ${err instanceof Error ? err.message : String(err)}`);
340
697
  return 3;
341
698
  }
699
+ let fileJwks;
700
+ if (args.jwksFile) {
701
+ try {
702
+ fileJwks = JSON.parse((0, node_fs_1.readFileSync)(args.jwksFile, 'utf8'));
703
+ }
704
+ catch (err) {
705
+ console.error(`Error reading JWKS file: ${err instanceof Error ? err.message : String(err)}`);
706
+ return 3;
707
+ }
708
+ }
709
+ const loaded = await (0, index_js_1.loadIssuerJwks)(receipt, {
710
+ jwks: fileJwks,
711
+ jwksUri: args.jwksUrl || undefined,
712
+ fetchJwks: args.fetchJwks,
713
+ });
714
+ const trustedKids = args.noTrustedKid
715
+ ? []
716
+ : (args.trustedKids ?? [...index_js_1.DEFAULT_TRUSTED_ISSUER_KIDS]);
717
+ let issuerHistory = null;
718
+ let anchorWalletDocument = null;
719
+ try {
720
+ issuerHistory = await loadAnchorIssuerHistory(args, receipt);
721
+ anchorWalletDocument = await loadAnchorWalletDocument(args, receipt);
722
+ }
723
+ catch (err) {
724
+ console.error(`Error reading issuer trust files: ${err instanceof Error ? err.message : String(err)}`);
725
+ return 3;
726
+ }
727
+ let epochRecord = null;
728
+ let verifyEpochSignature;
729
+ if (headNeedsEpochRecord(head)) {
730
+ try {
731
+ epochRecord = await loadEpochRecord(args, receipt);
732
+ }
733
+ catch {
734
+ epochRecord = null;
735
+ }
736
+ if (epochRecord?.issuer_signature?.jws) {
737
+ verifyEpochSignature = (jws) => epochSignatureOk(epochRecord, jws, loaded.jwks, trustedKids);
738
+ }
739
+ }
740
+ let issuerPin;
741
+ const pinClaim = (0, issuer_pin_js_1.readIssuerPinClaim)(receipt).claimed || (0, issuer_pin_js_1.readIssuerPinClaim)(head).claimed;
742
+ const pinRequested = Boolean(args.issuerPinFile) || pinClaim;
743
+ if (pinRequested) {
744
+ try {
745
+ const claimRef = (0, issuer_pin_js_1.readIssuerPinClaim)(receipt).ref || (0, issuer_pin_js_1.readIssuerPinClaim)(head).ref;
746
+ const ref = (args.issuerPinCommit || args.issuerPinSha256 || claimRef)
747
+ ? {
748
+ commit: args.issuerPinCommit || claimRef?.commit || '',
749
+ path: claimRef?.path || 'docs/well-known/issuer-key.json',
750
+ sha256: (args.issuerPinSha256 || claimRef?.sha256 || '').toLowerCase(),
751
+ }
752
+ : null;
753
+ let witnesses;
754
+ if (args.issuerWitnessesFile)
755
+ witnesses = JSON.parse((0, node_fs_1.readFileSync)(args.issuerWitnessesFile, 'utf8'));
756
+ const priorRef = args.issuerPriorPinFile
757
+ ? {
758
+ commit: args.issuerPriorCommit || '',
759
+ path: 'docs/well-known/issuer-key.json',
760
+ sha256: (args.issuerPriorSha256 || '').toLowerCase(),
761
+ }
762
+ : null;
763
+ issuerPin = {
764
+ pinBytes: args.issuerPinFile ? (0, node_fs_1.readFileSync)(args.issuerPinFile, 'utf8') : null,
765
+ sigBytes: args.issuerPinSigFile ? (0, node_fs_1.readFileSync)(args.issuerPinSigFile, 'utf8') : null,
766
+ ref,
767
+ witnesses,
768
+ priorPinBytes: args.issuerPriorPinFile ? (0, node_fs_1.readFileSync)(args.issuerPriorPinFile, 'utf8') : null,
769
+ priorRef,
770
+ controlJws: args.issuerControlFile ? (0, node_fs_1.readFileSync)(args.issuerControlFile, 'utf8').trim() : null,
771
+ required: true,
772
+ };
773
+ }
774
+ catch (err) {
775
+ console.error(`Error reading issuer pin: ${err instanceof Error ? err.message : String(err)}`);
776
+ return 3;
777
+ }
778
+ }
342
779
  const result = await (0, anchor_witness_js_1.verifyAnchoredRoot)({
343
780
  receipt,
344
781
  inclusion,
345
782
  head,
346
783
  baseRpcUrl: args.rpcUrl || undefined,
347
784
  solanaRpcUrl: args.solanaRpcUrl || undefined,
785
+ checkNewestAnchor: args.sawRpc,
786
+ epochRecord,
787
+ verifyEpochSignature,
788
+ issuerPin,
789
+ jwks: loaded.jwks,
790
+ trustedKids,
791
+ issuerHistory,
792
+ strictIssuerHistory: args.strictIssuerHistory,
793
+ anchorWalletDocument: anchorWalletDocument,
348
794
  });
349
- const verified = await (0, index_js_1.verifyReceipt)(receipt, { head });
350
- const lane = verified.receipt_lane;
795
+ let canonicalPreimage = null;
796
+ if (args.canonicalPreimageFile) {
797
+ canonicalPreimage = (0, node_fs_1.readFileSync)(args.canonicalPreimageFile, 'utf8');
798
+ }
799
+ const historyBytes = args.issuerHistoryFile ? (0, node_fs_1.readFileSync)(args.issuerHistoryFile, 'utf8') : null;
800
+ const verified = await (0, index_js_1.verifyReceipt)(receipt, {
801
+ jwks: loaded.jwks,
802
+ jwksUri: args.jwksUrl || undefined,
803
+ fetchJwks: args.fetchJwks,
804
+ trustedKids,
805
+ checkNullifier: args.checkNullifier,
806
+ checkPayer: args.checkPayer,
807
+ rpcUrl: args.rpcUrl || undefined,
808
+ solanaRpcUrl: args.solanaRpcUrl || undefined,
809
+ requirePreimages: !args.noPreimage,
810
+ issuerHistory: args.issuerHistoryFile ? issuerHistory : null,
811
+ issuerHistoryBytes: historyBytes,
812
+ fetchIssuerHistory: !args.noIssuerHistory && !args.issuerHistoryFile,
813
+ strictIssuerHistory: args.strictIssuerHistory,
814
+ skipIssuerHistory: args.noIssuerHistory,
815
+ canonicalPreimage,
816
+ head,
817
+ });
818
+ // Inclusion was already checked above. Passing it again re-derives the leaf
819
+ // from book_chain.row_hash only, which rejects a receipt that stores row_hash
820
+ // at the top level. Log mode still requires the receipt itself. A missing or
821
+ // stripped issuer signature, a partial check, or a signed-versus-outer
822
+ // mismatch fails the combined result. An inclusion match is not VERIFIED.
823
+ const issuerSig = receipt.issuer_signature;
824
+ const signaturePresent = Boolean(issuerSig?.jws || issuerSig?.value);
825
+ const receiptVerified = signaturePresent
826
+ && verified.issuer_signature.valid === true
827
+ && (verified.overall === 'verified' || verified.overall === 'verified_carried_forward')
828
+ && verified.claim_mismatches.length === 0;
829
+ const receiptFailed = !receiptVerified;
830
+ const receiptErrors = [...verified.errors];
831
+ if (!signaturePresent) {
832
+ receiptErrors.push(verified.issuer_signature.reason || 'receipt_signature_missing');
833
+ }
834
+ else if (verified.overall === 'partial') {
835
+ receiptErrors.push(verified.issuer_signature.reason || 'receipt_check_partial');
836
+ }
837
+ const errors = receiptFailed
838
+ ? [...result.errors, ...receiptErrors.filter((err) => !result.errors.includes(err))]
839
+ : result.errors;
840
+ const combined = {
841
+ ...result,
842
+ errors,
843
+ overall: receiptFailed ? 'failed' : result.overall,
844
+ receipt_lane: verified.receipt_lane,
845
+ receipt_check: {
846
+ overall: result.overall === 'pending' && !receiptFailed ? 'pending' : verified.overall,
847
+ verified_scope: verified.verified_scope,
848
+ claim_mismatches: verified.claim_mismatches,
849
+ unsigned_fields: verified.unsigned_fields,
850
+ errors: verified.errors,
851
+ },
852
+ };
351
853
  if (args.json) {
352
- console.log(JSON.stringify({ ...result, receipt_lane: lane }, null, 2));
854
+ console.log(JSON.stringify(combined, null, 2));
353
855
  }
354
856
  else {
355
- printAnchor(result, false, args.quiet);
857
+ printAnchor(combined, false, args.quiet);
858
+ if (!args.quiet || receiptFailed) {
859
+ const anchorPending = result.overall === 'pending';
860
+ const receiptLabel = anchorPending && !receiptFailed
861
+ ? 'PENDING'
862
+ : verified.overall.toUpperCase();
863
+ console.log(` Receipt checks: ${receiptLabel} (signed claims only)`);
864
+ if (verified.claim_mismatches.length > 0) {
865
+ for (const mismatch of verified.claim_mismatches) {
866
+ console.log(` ${mismatch.field}: outer ${mismatch.outer} ≠ signed ${mismatch.signed}`);
867
+ }
868
+ }
869
+ if (!(anchorPending && !receiptFailed))
870
+ printUnsigned(verified.unsigned_fields);
871
+ console.log('');
872
+ }
356
873
  if (!args.quiet)
357
- printLane(lane);
874
+ printLane(verified.receipt_lane);
358
875
  }
359
- if (result.overall === 'verified')
876
+ if (combined.overall === 'verified')
360
877
  return 0;
361
- if (result.overall === 'partial')
878
+ if (combined.overall === 'partial' || combined.overall === 'pending')
362
879
  return 2;
363
880
  return 1;
364
881
  }
882
+ function printUnsigned(fields) {
883
+ console.log(' Scope: signed claims only. Unsigned fields are UNVERIFIED.');
884
+ if (fields.length === 0)
885
+ return;
886
+ console.log(` Unsigned fields (${fields.length})`);
887
+ console.log(' ─────────────────────────────────────────────────');
888
+ for (const field of fields)
889
+ console.log(` UNVERIFIED ${field}`);
890
+ }
365
891
  async function runRefusal(doc, args) {
366
892
  const trustedKids = args.trustedKids ?? index_js_1.DEFAULT_TRUSTED_ISSUER_KIDS;
367
893
  const result = (0, index_js_1.verifyRefusal)(doc, {
@@ -384,7 +910,7 @@ async function runRefusal(doc, args) {
384
910
  issuer_history: result.issuer_history,
385
911
  }) : null;
386
912
  const history = args.skipIssuerHistory
387
- ? { checked: false, ok: true, unreachable: false, warning: null, reason: null, kid: result.kid ?? null }
913
+ ? { checked: false, ok: true, unreachable: false, warning: null, reason: null, kid: result.kid ?? null, document: null, loaded: false }
388
914
  : await (0, issuer_history_js_1.checkReceiptIssuerHistory)(doc, {
389
915
  document: args.issuerHistory,
390
916
  documentBytes: args.issuerHistoryBytes,
@@ -444,13 +970,30 @@ async function runRefusal(doc, args) {
444
970
  }
445
971
  async function main() {
446
972
  const args = parseArgs(process.argv.slice(2));
447
- const anchorMode = args.anchorFlag
973
+ if (args.version) {
974
+ console.log(`@xfuel/verify ${packageVersion()}`);
975
+ return 0;
976
+ }
977
+ let anchorMode = args.anchorFlag
448
978
  || Boolean(args.inclusionFile || args.headFile)
449
979
  || (args.positionals.length >= 3 && args.sawRpc);
980
+ if (!anchorMode && args.sawRpc && args.positionals.some((file) => fileLooksLikeHead(file))) {
981
+ anchorMode = true;
982
+ }
450
983
  if (args.help) {
451
984
  console.log(HELP);
452
985
  return 0;
453
986
  }
987
+ let offlineWitness = null;
988
+ if (!anchorMode) {
989
+ try {
990
+ offlineWitness = readOfflineWitness(args);
991
+ }
992
+ catch (err) {
993
+ console.error(err instanceof Error ? err.message : String(err));
994
+ return 3;
995
+ }
996
+ }
454
997
  if (anchorMode)
455
998
  return runAnchor(args);
456
999
  if (!args.file) {
@@ -465,7 +1008,19 @@ async function main() {
465
1008
  console.error(`Error reading receipt: ${err instanceof Error ? err.message : String(err)}`);
466
1009
  return 3;
467
1010
  }
468
- if (!receipt.task_id) {
1011
+ const v11 = receipt.v === 11 || (receipt.issuer_signature?.jws
1012
+ ? (() => {
1013
+ try {
1014
+ const part = String(receipt.issuer_signature.jws).split('.')[1];
1015
+ const payload = JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
1016
+ return payload?.v === 11;
1017
+ }
1018
+ catch {
1019
+ return false;
1020
+ }
1021
+ })()
1022
+ : false);
1023
+ if (!receipt.task_id && !v11) {
469
1024
  console.error('Invalid receipt: missing task_id');
470
1025
  return 3;
471
1026
  }
@@ -531,6 +1086,42 @@ async function main() {
531
1086
  return 3;
532
1087
  }
533
1088
  }
1089
+ const carryAsked = args.carryOldHeadFile || args.carryOldInclusionFile
1090
+ || args.carryHeadFile || args.carryInclusionFiles.length > 0;
1091
+ let carry = null;
1092
+ if (carryAsked) {
1093
+ try {
1094
+ carry = {
1095
+ oldHead: args.carryOldHeadFile ? readJson(args.carryOldHeadFile) : {},
1096
+ oldInclusion: args.carryOldInclusionFile ? readJson(args.carryOldInclusionFile) : { error: 'not_in_tree' },
1097
+ currentHead: args.carryHeadFile ? readJson(args.carryHeadFile) : {},
1098
+ currentInclusions: args.carryInclusionFiles.length > 0
1099
+ ? args.carryInclusionFiles.map((file) => readJson(file))
1100
+ : [{ error: 'not_in_tree' }],
1101
+ };
1102
+ }
1103
+ catch (err) {
1104
+ console.error(`Error reading carry-forward files: ${err instanceof Error ? err.message : String(err)}`);
1105
+ return 3;
1106
+ }
1107
+ }
1108
+ let open;
1109
+ if (args.open.length > 0) {
1110
+ open = {};
1111
+ try {
1112
+ for (const spec of args.open) {
1113
+ const opened = (0, v11_receipt_js_1.readOpenFile)(spec);
1114
+ if (opened.kind === 'output')
1115
+ open.output = opened.bytes;
1116
+ else
1117
+ open[opened.kind] = opened.text;
1118
+ }
1119
+ }
1120
+ catch (err) {
1121
+ console.error(err instanceof Error ? err.message : String(err));
1122
+ return 3;
1123
+ }
1124
+ }
534
1125
  const result = await (0, index_js_1.verifyReceipt)(receipt, {
535
1126
  jwks,
536
1127
  jwksUri: args.jwksUrl || undefined,
@@ -547,6 +1138,11 @@ async function main() {
547
1138
  strictIssuerHistory: args.strictIssuerHistory,
548
1139
  skipIssuerHistory: args.noIssuerHistory,
549
1140
  canonicalPreimage,
1141
+ salt: args.salt,
1142
+ open,
1143
+ head: offlineWitness?.head ?? undefined,
1144
+ inclusion: offlineWitness?.inclusion ?? undefined,
1145
+ carry,
550
1146
  });
551
1147
  if (args.json) {
552
1148
  console.log(JSON.stringify(result, null, 2));
@@ -619,6 +1215,8 @@ async function main() {
619
1215
  }
620
1216
  }
621
1217
  console.log('');
1218
+ printUnsigned(result.unsigned_fields);
1219
+ console.log('');
622
1220
  console.log(` Nullifier`);
623
1221
  console.log(` ─────────────────────────────────────────────────`);
624
1222
  if (result.nullifier.nullifier) {
@@ -644,6 +1242,18 @@ async function main() {
644
1242
  else if (result.issuer_history.warning) {
645
1243
  console.log(` Issuer history: ${result.issuer_history.warning}`);
646
1244
  }
1245
+ if (result.carry_forward?.status === 'VERIFIED_CARRIED_FORWARD') {
1246
+ const carried = result.carry_forward;
1247
+ console.log(` Log`);
1248
+ console.log(` ─────────────────────────────────────────────────`);
1249
+ console.log(` Status: ${carried.status}`);
1250
+ console.log(` Issued at: ${carried.issued_at}`);
1251
+ console.log(` Issued epoch: ${carried.issued_epoch}`);
1252
+ console.log(` Logged at: ${carried.logged_at}`);
1253
+ console.log(` Logged epoch: ${carried.logged_epoch}`);
1254
+ console.log(` Leaf index: ${carried.leaf_index}`);
1255
+ console.log('');
1256
+ }
647
1257
  console.log(` Overall: ${result.overall.toUpperCase()}`);
648
1258
  if (result.errors.length > 0) {
649
1259
  console.log(` Errors: ${result.errors.join(', ')}`);
@@ -652,6 +1262,7 @@ async function main() {
652
1262
  }
653
1263
  switch (result.overall) {
654
1264
  case 'verified':
1265
+ case 'verified_carried_forward':
655
1266
  return 0;
656
1267
  case 'failed':
657
1268
  return 1;
@@ -661,10 +1272,43 @@ async function main() {
661
1272
  return 1;
662
1273
  }
663
1274
  }
1275
+ /**
1276
+ * Node's fetch keeps its undici agent on this symbol after the first request.
1277
+ * There is no public handle for that pool. Closing it lets the loop drain.
1278
+ */
1279
+ const UNDICI_GLOBAL_DISPATCHER = Symbol.for('undici.globalDispatcher.1');
1280
+ function fetchDispatcher() {
1281
+ const bag = globalThis;
1282
+ return bag[UNDICI_GLOBAL_DISPATCHER];
1283
+ }
1284
+ /**
1285
+ * process.exit() while a fetch keep-alive socket is already CLOSING aborts
1286
+ * on Windows: Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)
1287
+ * (src/win/async.c). Set the code and close the pool so the loop can drain.
1288
+ */
1289
+ async function finish(code) {
1290
+ process.exitCode = code;
1291
+ const dispatcher = fetchDispatcher();
1292
+ if (!dispatcher)
1293
+ return;
1294
+ try {
1295
+ const closing = dispatcher.close?.();
1296
+ if (closing)
1297
+ await closing;
1298
+ }
1299
+ catch {
1300
+ try {
1301
+ await dispatcher.destroy?.();
1302
+ }
1303
+ catch {
1304
+ // The verification result is already the exit code.
1305
+ }
1306
+ }
1307
+ }
664
1308
  main()
665
- .then((code) => process.exit(code))
1309
+ .then((code) => finish(code))
666
1310
  .catch((err) => {
667
1311
  console.error('Unexpected error:', err);
668
- process.exit(3);
1312
+ return finish(3);
669
1313
  });
670
1314
  //# sourceMappingURL=cli.js.map