@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.
- package/README.md +41 -3
- package/dist/anchor-witness.d.ts +155 -0
- package/dist/anchor-witness.d.ts.map +1 -0
- package/dist/anchor-witness.js +431 -0
- package/dist/anchor-witness.js.map +1 -0
- package/dist/canonical-preimage.d.ts +9 -0
- package/dist/canonical-preimage.d.ts.map +1 -0
- package/dist/canonical-preimage.js +25 -0
- package/dist/canonical-preimage.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +527 -15
- package/dist/cli.js.map +1 -1
- package/dist/epoch.d.ts +109 -0
- package/dist/epoch.d.ts.map +1 -0
- package/dist/epoch.js +209 -0
- package/dist/epoch.js.map +1 -0
- package/dist/head-binding.d.ts +24 -0
- package/dist/head-binding.d.ts.map +1 -0
- package/dist/head-binding.js +61 -0
- package/dist/head-binding.js.map +1 -0
- package/dist/index.d.ts +146 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +305 -24
- package/dist/index.js.map +1 -1
- package/dist/issuer-history.d.ts +91 -0
- package/dist/issuer-history.d.ts.map +1 -0
- package/dist/issuer-history.js +292 -0
- package/dist/issuer-history.js.map +1 -0
- package/dist/jcs.d.ts +8 -0
- package/dist/jcs.d.ts.map +1 -0
- package/dist/jcs.js +64 -0
- package/dist/jcs.js.map +1 -0
- package/dist/preimage.d.ts +49 -0
- package/dist/preimage.d.ts.map +1 -0
- package/dist/preimage.js +203 -0
- package/dist/preimage.js.map +1 -0
- package/dist/receipt-lane.d.ts +234 -0
- package/dist/receipt-lane.d.ts.map +1 -0
- package/dist/receipt-lane.js +443 -0
- package/dist/receipt-lane.js.map +1 -0
- package/dist/refusal.d.ts +90 -0
- package/dist/refusal.d.ts.map +1 -0
- package/dist/refusal.js +226 -0
- package/dist/refusal.js.map +1 -0
- 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>
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
- --
|
|
69
|
-
|
|
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
|
-
#
|
|
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'
|
|
130
|
-
result.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 ??
|
|
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) =>
|
|
825
|
+
.then((code) => finish(code))
|
|
314
826
|
.catch((err) => {
|
|
315
827
|
console.error('Unexpected error:', err);
|
|
316
|
-
|
|
828
|
+
return finish(3);
|
|
317
829
|
});
|
|
318
830
|
//# sourceMappingURL=cli.js.map
|