@xfuel/verify 0.3.1 → 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 +16 -6
  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 +114 -4
  7. package/dist/anchor-witness.d.ts.map +1 -1
  8. package/dist/anchor-witness.js +419 -32
  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 +525 -41
  15. package/dist/cli.js.map +1 -1
  16. package/dist/epoch.d.ts +41 -7
  17. package/dist/epoch.d.ts.map +1 -1
  18. package/dist/epoch.js +143 -0
  19. package/dist/epoch.js.map +1 -1
  20. package/dist/index.d.ts +64 -6
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +338 -96
  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,6 +17,7 @@
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,6 +25,9 @@ const jws_js_1 = require("./jws.js");
24
25
  const preimage_js_1 = require("./preimage.js");
25
26
  const issuer_history_js_1 = require("./issuer-history.js");
26
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");
27
31
  const HELP = `
28
32
  xfuel-verify — Offline verification for Chit402 receipts
29
33
 
@@ -52,14 +56,35 @@ Options:
52
56
  --check-payer Query Base or Solana RPC. Base confirms payer, payee, asset, amount
53
57
  --solana-rpc <url> Solana RPC URL (default: https://api.mainnet-beta.solana.com or SOLANA_RPC_URL)
54
58
  --rpc <url> Base RPC URL (default: https://mainnet.base.org)
59
+ --version Print @xfuel/verify version and exit
55
60
  --rpc With a receipt, an inclusion proof, and a tree head: check the
56
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.
57
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.
58
68
  --epoch-record <f> Signed epoch record JSON. Skips the network fetch.
59
69
  --epoch-url <url> GET this epoch record. Default: the receipt verify_url
60
70
  origin plus /v1/receipts/tree/epoch
61
71
  --inclusion <file> Inclusion proof JSON (chit402.inclusion.v1)
62
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.
63
88
  --json Output JSON instead of human-readable
64
89
  --quiet Only output errors
65
90
  --strict-issuer-history
@@ -83,8 +108,11 @@ Trust:
83
108
  issuer_jwk whose RFC 7638 thumbprint equals a pinned trusted kid. The
84
109
  embedded key alone is not a trust root. Untrusted keys report "key untrusted".
85
110
 
86
- Amount, payer, payee, and tx are read from verified signed claims. A mismatch
87
- 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.
88
116
 
89
117
  Network behavior:
90
118
  JWKS, nullifiers, and anchors are not fetched unless you pass a flag.
@@ -105,11 +133,34 @@ Anchored root:
105
133
  xfuel-verify receipt.json inclusion.json head.json --rpc
106
134
  xfuel-verify receipt.json --inclusion inclusion.json --head head.json --rpc https://mainnet.base.org
107
135
 
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.
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.
113
164
 
114
165
  Receipt lane (unsigned, beside book_seq):
115
166
  settled_by is observed_transfer when the USDC transfer was checked on Base
@@ -157,6 +208,10 @@ function parseArgs(args) {
157
208
  headFile: null,
158
209
  epochRecordFile: null,
159
210
  epochUrl: null,
211
+ carryOldHeadFile: null,
212
+ carryOldInclusionFile: null,
213
+ carryHeadFile: null,
214
+ carryInclusionFiles: [],
160
215
  positionals: [],
161
216
  json: false,
162
217
  quiet: false,
@@ -166,10 +221,26 @@ function parseArgs(args) {
166
221
  canonicalPreimageFile: null,
167
222
  noIssuerHistory: false,
168
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: [],
169
237
  };
170
238
  for (let i = 0; i < args.length; i++) {
171
239
  const arg = args[i];
172
- if (arg === '--help' || arg === '-h') {
240
+ if (arg === '--version' || arg === '-V') {
241
+ result.version = true;
242
+ }
243
+ else if (arg === '--help' || arg === '-h') {
173
244
  result.help = true;
174
245
  }
175
246
  else if (arg === '--check-nullifier') {
@@ -214,6 +285,18 @@ function parseArgs(args) {
214
285
  else if (arg === '--epoch-url' && args[i + 1]) {
215
286
  result.epochUrl = args[++i];
216
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
+ }
217
300
  else if (arg === '--solana-rpc' && args[i + 1]) {
218
301
  result.solanaRpcUrl = args[++i];
219
302
  }
@@ -223,15 +306,51 @@ function parseArgs(args) {
223
306
  else if (arg === '--issuer-history-file' && args[i + 1]) {
224
307
  result.issuerHistoryFile = args[++i];
225
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
+ }
226
315
  else if (arg === '--canonical-preimage' && args[i + 1]) {
227
316
  result.canonicalPreimageFile = args[++i];
228
317
  }
229
318
  else if (arg === '--no-issuer-history') {
230
319
  result.noIssuerHistory = true;
231
320
  }
321
+ else if (arg === '--anchor-wallets-file' && args[i + 1]) {
322
+ result.anchorWalletsFile = args[++i];
323
+ }
232
324
  else if (arg === '--no-preimage') {
233
325
  result.noPreimage = true;
234
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
+ }
235
354
  else if (arg === '--json') {
236
355
  result.json = true;
237
356
  }
@@ -310,7 +429,18 @@ function printAnchor(result, json, quiet) {
310
429
  console.log(' Anchored receipt root');
311
430
  console.log(' ─────────────────────────────────────────────────');
312
431
  console.log(` Root: ${result.root || '—'}`);
313
- 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
+ }
314
444
  console.log(` Leaf source: ${result.inclusion.leaf_source}`);
315
445
  console.log(` Solana: ${result.solana.checked ? mark(result.solana.valid) : (result.solana.reason || 'not checked')}`);
316
446
  if (result.solana.signature)
@@ -319,11 +449,18 @@ function printAnchor(result, json, quiet) {
319
449
  console.log(` Slot: ${result.solana.slot}`);
320
450
  if (result.solana.cluster)
321
451
  console.log(` Cluster: ${result.solana.cluster}`);
452
+ if (result.solana.fee_payer)
453
+ console.log(` Fee payer: ${result.solana.fee_payer}`);
322
454
  if (result.solana.memo)
323
455
  console.log(` Memo: ${result.solana.memo}`);
324
456
  if (result.solana.reason && !result.solana.valid)
325
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
+ }
326
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}`);
327
464
  if (result.base.tx)
328
465
  console.log(` Base tx: ${result.base.tx}`);
329
466
  if (result.base.chain_id != null)
@@ -341,9 +478,16 @@ function printAnchor(result, json, quiet) {
341
478
  for (const line of result.does_not_prove)
342
479
  console.log(` ${line}`);
343
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
+ }
344
485
  console.log(` Overall: ${result.overall.toUpperCase()}`);
345
486
  if (result.errors.length > 0)
346
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
+ }
347
491
  console.log('');
348
492
  }
349
493
  function headNeedsEpochRecord(head) {
@@ -403,20 +547,139 @@ function epochSignatureOk(record, jws, jwks, trustedKids) {
403
547
  return false;
404
548
  if (JSON.stringify(payload.orphans ?? []) !== JSON.stringify(record.orphans ?? []))
405
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;
406
554
  return true;
407
555
  }
408
556
  return false;
409
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
+ }
410
673
  async function runAnchor(args) {
411
674
  const receiptPath = args.file;
412
675
  const inclusionPath = args.inclusionFile || args.positionals[1] || null;
413
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
+ }
414
682
  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
683
  console.error('Anchor check needs a receipt, an inclusion proof, and a tree head.');
421
684
  console.error(' xfuel-verify receipt.json inclusion.json head.json --rpc');
422
685
  return 3;
@@ -433,6 +696,34 @@ async function runAnchor(args) {
433
696
  console.error(`Error reading anchor inputs: ${err instanceof Error ? err.message : String(err)}`);
434
697
  return 3;
435
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
+ }
436
727
  let epochRecord = null;
437
728
  let verifyEpochSignature;
438
729
  if (headNeedsEpochRecord(head)) {
@@ -443,25 +734,46 @@ async function runAnchor(args) {
443
734
  epochRecord = null;
444
735
  }
445
736
  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'));
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(),
450
751
  }
451
- catch (err) {
452
- console.error(`Error reading JWKS file: ${err instanceof Error ? err.message : String(err)}`);
453
- return 3;
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(),
454
761
  }
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);
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;
465
777
  }
466
778
  }
467
779
  const result = await (0, anchor_witness_js_1.verifyAnchoredRoot)({
@@ -470,25 +782,112 @@ async function runAnchor(args) {
470
782
  head,
471
783
  baseRpcUrl: args.rpcUrl || undefined,
472
784
  solanaRpcUrl: args.solanaRpcUrl || undefined,
785
+ checkNewestAnchor: args.sawRpc,
473
786
  epochRecord,
474
787
  verifyEpochSignature,
788
+ issuerPin,
789
+ jwks: loaded.jwks,
790
+ trustedKids,
791
+ issuerHistory,
792
+ strictIssuerHistory: args.strictIssuerHistory,
793
+ anchorWalletDocument: anchorWalletDocument,
475
794
  });
476
- const verified = await (0, index_js_1.verifyReceipt)(receipt, { head });
477
- 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
+ };
478
853
  if (args.json) {
479
- console.log(JSON.stringify({ ...result, receipt_lane: lane }, null, 2));
854
+ console.log(JSON.stringify(combined, null, 2));
480
855
  }
481
856
  else {
482
- 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
+ }
483
873
  if (!args.quiet)
484
- printLane(lane);
874
+ printLane(verified.receipt_lane);
485
875
  }
486
- if (result.overall === 'verified')
876
+ if (combined.overall === 'verified')
487
877
  return 0;
488
- if (result.overall === 'partial')
878
+ if (combined.overall === 'partial' || combined.overall === 'pending')
489
879
  return 2;
490
880
  return 1;
491
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
+ }
492
891
  async function runRefusal(doc, args) {
493
892
  const trustedKids = args.trustedKids ?? index_js_1.DEFAULT_TRUSTED_ISSUER_KIDS;
494
893
  const result = (0, index_js_1.verifyRefusal)(doc, {
@@ -511,7 +910,7 @@ async function runRefusal(doc, args) {
511
910
  issuer_history: result.issuer_history,
512
911
  }) : null;
513
912
  const history = args.skipIssuerHistory
514
- ? { 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 }
515
914
  : await (0, issuer_history_js_1.checkReceiptIssuerHistory)(doc, {
516
915
  document: args.issuerHistory,
517
916
  documentBytes: args.issuerHistoryBytes,
@@ -571,13 +970,30 @@ async function runRefusal(doc, args) {
571
970
  }
572
971
  async function main() {
573
972
  const args = parseArgs(process.argv.slice(2));
574
- const anchorMode = args.anchorFlag
973
+ if (args.version) {
974
+ console.log(`@xfuel/verify ${packageVersion()}`);
975
+ return 0;
976
+ }
977
+ let anchorMode = args.anchorFlag
575
978
  || Boolean(args.inclusionFile || args.headFile)
576
979
  || (args.positionals.length >= 3 && args.sawRpc);
980
+ if (!anchorMode && args.sawRpc && args.positionals.some((file) => fileLooksLikeHead(file))) {
981
+ anchorMode = true;
982
+ }
577
983
  if (args.help) {
578
984
  console.log(HELP);
579
985
  return 0;
580
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
+ }
581
997
  if (anchorMode)
582
998
  return runAnchor(args);
583
999
  if (!args.file) {
@@ -592,7 +1008,19 @@ async function main() {
592
1008
  console.error(`Error reading receipt: ${err instanceof Error ? err.message : String(err)}`);
593
1009
  return 3;
594
1010
  }
595
- 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) {
596
1024
  console.error('Invalid receipt: missing task_id');
597
1025
  return 3;
598
1026
  }
@@ -658,6 +1086,42 @@ async function main() {
658
1086
  return 3;
659
1087
  }
660
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
+ }
661
1125
  const result = await (0, index_js_1.verifyReceipt)(receipt, {
662
1126
  jwks,
663
1127
  jwksUri: args.jwksUrl || undefined,
@@ -674,6 +1138,11 @@ async function main() {
674
1138
  strictIssuerHistory: args.strictIssuerHistory,
675
1139
  skipIssuerHistory: args.noIssuerHistory,
676
1140
  canonicalPreimage,
1141
+ salt: args.salt,
1142
+ open,
1143
+ head: offlineWitness?.head ?? undefined,
1144
+ inclusion: offlineWitness?.inclusion ?? undefined,
1145
+ carry,
677
1146
  });
678
1147
  if (args.json) {
679
1148
  console.log(JSON.stringify(result, null, 2));
@@ -746,6 +1215,8 @@ async function main() {
746
1215
  }
747
1216
  }
748
1217
  console.log('');
1218
+ printUnsigned(result.unsigned_fields);
1219
+ console.log('');
749
1220
  console.log(` Nullifier`);
750
1221
  console.log(` ─────────────────────────────────────────────────`);
751
1222
  if (result.nullifier.nullifier) {
@@ -771,6 +1242,18 @@ async function main() {
771
1242
  else if (result.issuer_history.warning) {
772
1243
  console.log(` Issuer history: ${result.issuer_history.warning}`);
773
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
+ }
774
1257
  console.log(` Overall: ${result.overall.toUpperCase()}`);
775
1258
  if (result.errors.length > 0) {
776
1259
  console.log(` Errors: ${result.errors.join(', ')}`);
@@ -779,6 +1262,7 @@ async function main() {
779
1262
  }
780
1263
  switch (result.overall) {
781
1264
  case 'verified':
1265
+ case 'verified_carried_forward':
782
1266
  return 0;
783
1267
  case 'failed':
784
1268
  return 1;