@xfuel/verify 0.3.1 → 0.3.4
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 +16 -6
- package/dist/anchor-trust.d.ts +129 -0
- package/dist/anchor-trust.d.ts.map +1 -0
- package/dist/anchor-trust.js +395 -0
- package/dist/anchor-trust.js.map +1 -0
- package/dist/anchor-witness.d.ts +114 -4
- package/dist/anchor-witness.d.ts.map +1 -1
- package/dist/anchor-witness.js +419 -32
- package/dist/anchor-witness.js.map +1 -1
- package/dist/carry-forward.d.ts +79 -0
- package/dist/carry-forward.d.ts.map +1 -0
- package/dist/carry-forward.js +184 -0
- package/dist/carry-forward.js.map +1 -0
- package/dist/cli.js +691 -42
- package/dist/cli.js.map +1 -1
- package/dist/epoch.d.ts +41 -7
- package/dist/epoch.d.ts.map +1 -1
- package/dist/epoch.js +143 -0
- package/dist/epoch.js.map +1 -1
- package/dist/index.d.ts +65 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +348 -96
- package/dist/index.js.map +1 -1
- package/dist/issuer-history.d.ts +19 -0
- package/dist/issuer-history.d.ts.map +1 -1
- package/dist/issuer-history.js +131 -75
- package/dist/issuer-history.js.map +1 -1
- package/dist/issuer-pin.d.ts +126 -0
- package/dist/issuer-pin.d.ts.map +1 -0
- package/dist/issuer-pin.js +567 -0
- package/dist/issuer-pin.js.map +1 -0
- package/dist/preimage.d.ts +29 -0
- package/dist/preimage.d.ts.map +1 -1
- package/dist/preimage.js +162 -2
- package/dist/preimage.js.map +1 -1
- package/dist/receipt-lane.d.ts +1 -0
- package/dist/receipt-lane.d.ts.map +1 -1
- package/dist/receipt-lane.js +5 -2
- package/dist/receipt-lane.js.map +1 -1
- package/dist/refusal.d.ts +5 -2
- package/dist/refusal.d.ts.map +1 -1
- package/dist/refusal.js +28 -6
- package/dist/refusal.js.map +1 -1
- package/dist/request-binding.d.ts +7 -0
- package/dist/request-binding.d.ts.map +1 -0
- package/dist/request-binding.js +26 -0
- package/dist/request-binding.js.map +1 -0
- package/dist/row-hash.d.ts +22 -0
- package/dist/row-hash.d.ts.map +1 -0
- package/dist/row-hash.js +26 -0
- package/dist/row-hash.js.map +1 -0
- package/dist/shell.d.ts +65 -0
- package/dist/shell.d.ts.map +1 -0
- package/dist/shell.js +268 -0
- package/dist/shell.js.map +1 -0
- package/dist/v11-receipt.d.ts +21 -0
- package/dist/v11-receipt.d.ts.map +1 -0
- package/dist/v11-receipt.js +351 -0
- package/dist/v11-receipt.js.map +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -17,13 +17,18 @@
|
|
|
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");
|
|
23
24
|
const jws_js_1 = require("./jws.js");
|
|
25
|
+
const shell_js_1 = require("./shell.js");
|
|
24
26
|
const preimage_js_1 = require("./preimage.js");
|
|
25
27
|
const issuer_history_js_1 = require("./issuer-history.js");
|
|
26
28
|
const canonical_preimage_js_1 = require("./canonical-preimage.js");
|
|
29
|
+
const v11_receipt_js_1 = require("./v11-receipt.js");
|
|
30
|
+
const issuer_pin_js_1 = require("./issuer-pin.js");
|
|
31
|
+
const anchor_trust_js_1 = require("./anchor-trust.js");
|
|
27
32
|
const HELP = `
|
|
28
33
|
xfuel-verify — Offline verification for Chit402 receipts
|
|
29
34
|
|
|
@@ -52,14 +57,35 @@ Options:
|
|
|
52
57
|
--check-payer Query Base or Solana RPC. Base confirms payer, payee, asset, amount
|
|
53
58
|
--solana-rpc <url> Solana RPC URL (default: https://api.mainnet-beta.solana.com or SOLANA_RPC_URL)
|
|
54
59
|
--rpc <url> Base RPC URL (default: https://mainnet.base.org)
|
|
60
|
+
--version Print @xfuel/verify version and exit
|
|
55
61
|
--rpc With a receipt, an inclusion proof, and a tree head: check the
|
|
56
62
|
leaf, then the Solana memo and the Base calldata for that root.
|
|
63
|
+
The tree head must carry a valid ES256 issuer signature, and
|
|
64
|
+
the anchor wallets must match the signed head.
|
|
57
65
|
A v2 head also needs the signed epoch record.
|
|
66
|
+
A tree-head file with --rpc enters this mode.
|
|
67
|
+
The same files without --rpc check the head signature and the
|
|
68
|
+
index/size-bound inclusion offline. They do not call chain RPC.
|
|
58
69
|
--epoch-record <f> Signed epoch record JSON. Skips the network fetch.
|
|
59
70
|
--epoch-url <url> GET this epoch record. Default: the receipt verify_url
|
|
60
71
|
origin plus /v1/receipts/tree/epoch
|
|
61
72
|
--inclusion <file> Inclusion proof JSON (chit402.inclusion.v1)
|
|
62
73
|
--head <file> Signed tree head JSON (chit402.tree_head.v1 or v2)
|
|
74
|
+
--anchor-wallets-file <path>
|
|
75
|
+
Signed chit402.anchor_wallets.v1 document. Wallets in a
|
|
76
|
+
verified document are added to the package pin. A head
|
|
77
|
+
that names any other wallet fails.
|
|
78
|
+
--carry-old-head <file>
|
|
79
|
+
--carry-old-inclusion <file>
|
|
80
|
+
--carry-head <file>
|
|
81
|
+
--carry-inclusion <file>
|
|
82
|
+
Earlier signed head, its inclusion proof, the current
|
|
83
|
+
signed head, and the current inclusion proof. Repeat
|
|
84
|
+
--carry-inclusion for each current index of the leaf.
|
|
85
|
+
The issued time and epoch are the earlier head's signed
|
|
86
|
+
claims. The logged time, epoch, and leaf index are the
|
|
87
|
+
current head's signed claims. A receipt in the current
|
|
88
|
+
epoch does not use these flags.
|
|
63
89
|
--json Output JSON instead of human-readable
|
|
64
90
|
--quiet Only output errors
|
|
65
91
|
--strict-issuer-history
|
|
@@ -83,8 +109,11 @@ Trust:
|
|
|
83
109
|
issuer_jwk whose RFC 7638 thumbprint equals a pinned trusted kid. The
|
|
84
110
|
embedded key alone is not a trust root. Untrusted keys report "key untrusted".
|
|
85
111
|
|
|
86
|
-
Amount, payer, payee, and tx are read from verified signed claims.
|
|
87
|
-
with the
|
|
112
|
+
Amount, payer, payee, and tx are read from verified signed claims. Every
|
|
113
|
+
signed field is compared with the outer receipt at the same path. A mismatch
|
|
114
|
+
fails. Outer fields the signature does not cover are printed as UNVERIFIED.
|
|
115
|
+
The outer document schema is not a copy of the JWS schema. Log mode (a
|
|
116
|
+
receipt, an inclusion proof, and a tree head) runs these receipt checks too.
|
|
88
117
|
|
|
89
118
|
Network behavior:
|
|
90
119
|
JWKS, nullifiers, and anchors are not fetched unless you pass a flag.
|
|
@@ -105,11 +134,34 @@ Anchored root:
|
|
|
105
134
|
xfuel-verify receipt.json inclusion.json head.json --rpc
|
|
106
135
|
xfuel-verify receipt.json --inclusion inclusion.json --head head.json --rpc https://mainnet.base.org
|
|
107
136
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
137
|
+
Also runs the single-receipt checks (issuer signature and signed-versus-outer
|
|
138
|
+
comparison). Those must pass before Overall can be VERIFIED. Then it checks
|
|
139
|
+
the tree head's ES256 issuer signature (production pin, issuer
|
|
140
|
+
history, or JWKS, same rules as a receipt). The signed root, size, epoch,
|
|
141
|
+
and anchors must match the head. Then it checks Merkle inclusion, requires
|
|
142
|
+
the Solana memo and fee payer, and requires the Base calldata and sender.
|
|
143
|
+
The sender and fee payer must be on the issuer anchor-wallet list (the
|
|
144
|
+
package pin, plus a verified /.well-known/anchor-wallets.json or
|
|
145
|
+
issuer-history anchor_wallets list). The on-chain issuer-root registry and
|
|
146
|
+
the DNS anchor are not read. Prints what this proves and what it does not
|
|
147
|
+
prove. Exit 0 when the receipt checks pass and both chains match, 2 when the
|
|
148
|
+
leaf is included but an anchor is still pending, or when the leaf is past
|
|
149
|
+
the anchored tree (PENDING, not a tamper failure), and the receipt checks
|
|
150
|
+
did not fail, 1 when a check fails. A pending leaf must sit at an index
|
|
151
|
+
at or past the anchored size, and PENDING applies only when the receipt
|
|
152
|
+
itself verifies. A failed signature, field, or preimage is a failure.
|
|
153
|
+
With --rpc, a claimed anchor must name a Solana fee payer that is on the
|
|
154
|
+
anchor-wallet list, and the head root must equal the newest memo that
|
|
155
|
+
payer actually paid. A memo from any other payer is ignored.
|
|
156
|
+
|
|
157
|
+
A receipt or head with issuer_key_pin is a pinned era. Pass the pin file
|
|
158
|
+
and its commit SHA plus SHA-256. The verifier does not fetch a branch.
|
|
159
|
+
--issuer-pin <file> --issuer-pin-commit <40-hex> --issuer-pin-sha256 <64-hex>
|
|
160
|
+
Optional with that pin: --issuer-pin-sig, --issuer-witnesses,
|
|
161
|
+
--issuer-prior-pin, --issuer-prior-commit, --issuer-prior-sha256, --issuer-control.
|
|
162
|
+
Those flags do not start the check on their own. A prior pin does not
|
|
163
|
+
replace the published specimen key. Sepolia only (eip155:84532).
|
|
164
|
+
See docs/product/issuer-key-pin.md.
|
|
113
165
|
|
|
114
166
|
Receipt lane (unsigned, beside book_seq):
|
|
115
167
|
settled_by is observed_transfer when the USDC transfer was checked on Base
|
|
@@ -138,6 +190,13 @@ Examples:
|
|
|
138
190
|
|
|
139
191
|
# Pipe from curl
|
|
140
192
|
curl -s https://api.chit402.com/receipt/task-123?format=json | xfuel-verify -
|
|
193
|
+
|
|
194
|
+
# Public shell. INCLUDED_SHELL is not VERIFIED.
|
|
195
|
+
xfuel-verify shell.json
|
|
196
|
+
|
|
197
|
+
# Owner view. VERIFIED needs a trusted issuer key, an inclusion proof,
|
|
198
|
+
# and a signed payment. The key inside the holder is not enough.
|
|
199
|
+
xfuel-verify shell.json --jws owner-view.json --inclusion inclusion.json --head head.json
|
|
141
200
|
`;
|
|
142
201
|
function parseArgs(args) {
|
|
143
202
|
const result = {
|
|
@@ -157,6 +216,10 @@ function parseArgs(args) {
|
|
|
157
216
|
headFile: null,
|
|
158
217
|
epochRecordFile: null,
|
|
159
218
|
epochUrl: null,
|
|
219
|
+
carryOldHeadFile: null,
|
|
220
|
+
carryOldInclusionFile: null,
|
|
221
|
+
carryHeadFile: null,
|
|
222
|
+
carryInclusionFiles: [],
|
|
160
223
|
positionals: [],
|
|
161
224
|
json: false,
|
|
162
225
|
quiet: false,
|
|
@@ -166,10 +229,29 @@ function parseArgs(args) {
|
|
|
166
229
|
canonicalPreimageFile: null,
|
|
167
230
|
noIssuerHistory: false,
|
|
168
231
|
noPreimage: false,
|
|
232
|
+
issuerPinFile: null,
|
|
233
|
+
issuerPinCommit: null,
|
|
234
|
+
issuerPinSha256: null,
|
|
235
|
+
issuerPinSigFile: null,
|
|
236
|
+
issuerWitnessesFile: null,
|
|
237
|
+
issuerPriorPinFile: null,
|
|
238
|
+
issuerPriorCommit: null,
|
|
239
|
+
issuerPriorSha256: null,
|
|
240
|
+
issuerControlFile: null,
|
|
241
|
+
version: false,
|
|
242
|
+
anchorWalletsFile: null,
|
|
243
|
+
acceptShell: false,
|
|
244
|
+
jwsFile: null,
|
|
245
|
+
salt: null,
|
|
246
|
+
open: [],
|
|
247
|
+
url: null,
|
|
169
248
|
};
|
|
170
249
|
for (let i = 0; i < args.length; i++) {
|
|
171
250
|
const arg = args[i];
|
|
172
|
-
if (arg === '--
|
|
251
|
+
if (arg === '--version' || arg === '-V') {
|
|
252
|
+
result.version = true;
|
|
253
|
+
}
|
|
254
|
+
else if (arg === '--help' || arg === '-h') {
|
|
173
255
|
result.help = true;
|
|
174
256
|
}
|
|
175
257
|
else if (arg === '--check-nullifier') {
|
|
@@ -214,6 +296,18 @@ function parseArgs(args) {
|
|
|
214
296
|
else if (arg === '--epoch-url' && args[i + 1]) {
|
|
215
297
|
result.epochUrl = args[++i];
|
|
216
298
|
}
|
|
299
|
+
else if (arg === '--carry-old-head' && args[i + 1]) {
|
|
300
|
+
result.carryOldHeadFile = args[++i];
|
|
301
|
+
}
|
|
302
|
+
else if (arg === '--carry-old-inclusion' && args[i + 1]) {
|
|
303
|
+
result.carryOldInclusionFile = args[++i];
|
|
304
|
+
}
|
|
305
|
+
else if (arg === '--carry-head' && args[i + 1]) {
|
|
306
|
+
result.carryHeadFile = args[++i];
|
|
307
|
+
}
|
|
308
|
+
else if (arg === '--carry-inclusion' && args[i + 1]) {
|
|
309
|
+
result.carryInclusionFiles.push(args[++i]);
|
|
310
|
+
}
|
|
217
311
|
else if (arg === '--solana-rpc' && args[i + 1]) {
|
|
218
312
|
result.solanaRpcUrl = args[++i];
|
|
219
313
|
}
|
|
@@ -223,15 +317,60 @@ function parseArgs(args) {
|
|
|
223
317
|
else if (arg === '--issuer-history-file' && args[i + 1]) {
|
|
224
318
|
result.issuerHistoryFile = args[++i];
|
|
225
319
|
}
|
|
320
|
+
else if (arg === '--salt' && args[i + 1]) {
|
|
321
|
+
result.salt = args[++i];
|
|
322
|
+
}
|
|
323
|
+
else if (arg === '--open' && args[i + 1]) {
|
|
324
|
+
result.open.push(args[++i]);
|
|
325
|
+
}
|
|
226
326
|
else if (arg === '--canonical-preimage' && args[i + 1]) {
|
|
227
327
|
result.canonicalPreimageFile = args[++i];
|
|
228
328
|
}
|
|
229
329
|
else if (arg === '--no-issuer-history') {
|
|
230
330
|
result.noIssuerHistory = true;
|
|
231
331
|
}
|
|
332
|
+
else if (arg === '--anchor-wallets-file' && args[i + 1]) {
|
|
333
|
+
result.anchorWalletsFile = args[++i];
|
|
334
|
+
}
|
|
232
335
|
else if (arg === '--no-preimage') {
|
|
233
336
|
result.noPreimage = true;
|
|
234
337
|
}
|
|
338
|
+
else if (arg === '--accept-shell') {
|
|
339
|
+
result.acceptShell = true;
|
|
340
|
+
}
|
|
341
|
+
else if (arg === '--jws' && args[i + 1]) {
|
|
342
|
+
result.jwsFile = args[++i];
|
|
343
|
+
}
|
|
344
|
+
else if (arg === '--url' && args[i + 1]) {
|
|
345
|
+
result.url = args[++i];
|
|
346
|
+
}
|
|
347
|
+
else if (arg === '--issuer-pin' && args[i + 1]) {
|
|
348
|
+
result.issuerPinFile = args[++i];
|
|
349
|
+
}
|
|
350
|
+
else if (arg === '--issuer-pin-commit' && args[i + 1]) {
|
|
351
|
+
result.issuerPinCommit = args[++i];
|
|
352
|
+
}
|
|
353
|
+
else if (arg === '--issuer-pin-sha256' && args[i + 1]) {
|
|
354
|
+
result.issuerPinSha256 = args[++i];
|
|
355
|
+
}
|
|
356
|
+
else if (arg === '--issuer-pin-sig' && args[i + 1]) {
|
|
357
|
+
result.issuerPinSigFile = args[++i];
|
|
358
|
+
}
|
|
359
|
+
else if (arg === '--issuer-witnesses' && args[i + 1]) {
|
|
360
|
+
result.issuerWitnessesFile = args[++i];
|
|
361
|
+
}
|
|
362
|
+
else if (arg === '--issuer-prior-pin' && args[i + 1]) {
|
|
363
|
+
result.issuerPriorPinFile = args[++i];
|
|
364
|
+
}
|
|
365
|
+
else if (arg === '--issuer-prior-commit' && args[i + 1]) {
|
|
366
|
+
result.issuerPriorCommit = args[++i];
|
|
367
|
+
}
|
|
368
|
+
else if (arg === '--issuer-prior-sha256' && args[i + 1]) {
|
|
369
|
+
result.issuerPriorSha256 = args[++i];
|
|
370
|
+
}
|
|
371
|
+
else if (arg === '--issuer-control' && args[i + 1]) {
|
|
372
|
+
result.issuerControlFile = args[++i];
|
|
373
|
+
}
|
|
235
374
|
else if (arg === '--json') {
|
|
236
375
|
result.json = true;
|
|
237
376
|
}
|
|
@@ -310,7 +449,18 @@ function printAnchor(result, json, quiet) {
|
|
|
310
449
|
console.log(' Anchored receipt root');
|
|
311
450
|
console.log(' ─────────────────────────────────────────────────');
|
|
312
451
|
console.log(` Root: ${result.root || '—'}`);
|
|
313
|
-
console.log(`
|
|
452
|
+
console.log(` Head signature:${result.head_signature.valid ? ' ✓ YES' : ' ✗ NO'}${result.head_signature.trust ? ` (${result.head_signature.trust})` : ''}`);
|
|
453
|
+
if (result.head_signature.kid)
|
|
454
|
+
console.log(` Head kid: ${result.head_signature.kid}`);
|
|
455
|
+
if (result.head_signature.message)
|
|
456
|
+
console.log(` Head reason: ${result.head_signature.message}`);
|
|
457
|
+
const unlogged = result.inclusion.unlogged_reason ? ` unlogged:${result.inclusion.unlogged_reason}` : '';
|
|
458
|
+
if (result.overall === 'pending') {
|
|
459
|
+
console.log(` Inclusion: PENDING${result.inclusion.reason ? ` (${result.inclusion.reason})` : ''}`);
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
console.log(` Inclusion: ${mark(result.inclusion.valid)}${result.inclusion.reason ? ` (${result.inclusion.reason})` : ''}${unlogged}`);
|
|
463
|
+
}
|
|
314
464
|
console.log(` Leaf source: ${result.inclusion.leaf_source}`);
|
|
315
465
|
console.log(` Solana: ${result.solana.checked ? mark(result.solana.valid) : (result.solana.reason || 'not checked')}`);
|
|
316
466
|
if (result.solana.signature)
|
|
@@ -319,11 +469,18 @@ function printAnchor(result, json, quiet) {
|
|
|
319
469
|
console.log(` Slot: ${result.solana.slot}`);
|
|
320
470
|
if (result.solana.cluster)
|
|
321
471
|
console.log(` Cluster: ${result.solana.cluster}`);
|
|
472
|
+
if (result.solana.fee_payer)
|
|
473
|
+
console.log(` Fee payer: ${result.solana.fee_payer}`);
|
|
322
474
|
if (result.solana.memo)
|
|
323
475
|
console.log(` Memo: ${result.solana.memo}`);
|
|
324
476
|
if (result.solana.reason && !result.solana.valid)
|
|
325
477
|
console.log(` Solana reason: ${result.solana.reason}`);
|
|
478
|
+
if (result.issuer_pin?.checked) {
|
|
479
|
+
console.log(` Issuer pin: ${result.issuer_pin.ok ? '✓ YES' : '✗ NO'}${result.issuer_pin.code ? ` (${result.issuer_pin.code})` : ''}`);
|
|
480
|
+
}
|
|
326
481
|
console.log(` Base: ${result.base.checked ? mark(result.base.valid) : (result.base.reason || 'not checked')}`);
|
|
482
|
+
if (result.base.from)
|
|
483
|
+
console.log(` Base from: ${result.base.from}`);
|
|
327
484
|
if (result.base.tx)
|
|
328
485
|
console.log(` Base tx: ${result.base.tx}`);
|
|
329
486
|
if (result.base.chain_id != null)
|
|
@@ -341,9 +498,16 @@ function printAnchor(result, json, quiet) {
|
|
|
341
498
|
for (const line of result.does_not_prove)
|
|
342
499
|
console.log(` ${line}`);
|
|
343
500
|
console.log('');
|
|
501
|
+
if (result.overall === 'pending') {
|
|
502
|
+
console.log(` Anchored size: ${result.inclusion.anchored_tree_size ?? '—'}`);
|
|
503
|
+
console.log(` Live size: ${result.inclusion.live_tree_size ?? '—'}`);
|
|
504
|
+
}
|
|
344
505
|
console.log(` Overall: ${result.overall.toUpperCase()}`);
|
|
345
506
|
if (result.errors.length > 0)
|
|
346
507
|
console.log(` Errors: ${result.errors.join(', ')}`);
|
|
508
|
+
if (result.errors.includes(anchor_trust_js_1.LEGACY_HEAD_UNPINNED_SIGNER)) {
|
|
509
|
+
console.log(` ${anchor_trust_js_1.HEAD_TRUST_MESSAGES[anchor_trust_js_1.LEGACY_HEAD_UNPINNED_SIGNER]}`);
|
|
510
|
+
}
|
|
347
511
|
console.log('');
|
|
348
512
|
}
|
|
349
513
|
function headNeedsEpochRecord(head) {
|
|
@@ -403,20 +567,139 @@ function epochSignatureOk(record, jws, jwks, trustedKids) {
|
|
|
403
567
|
return false;
|
|
404
568
|
if (JSON.stringify(payload.orphans ?? []) !== JSON.stringify(record.orphans ?? []))
|
|
405
569
|
return false;
|
|
570
|
+
const payloadUnlogged = payload.unlogged === undefined ? null : payload.unlogged;
|
|
571
|
+
const recordUnlogged = record.unlogged === undefined ? null : record.unlogged;
|
|
572
|
+
if (JSON.stringify(payloadUnlogged) !== JSON.stringify(recordUnlogged))
|
|
573
|
+
return false;
|
|
406
574
|
return true;
|
|
407
575
|
}
|
|
408
576
|
return false;
|
|
409
577
|
}
|
|
578
|
+
function packageVersion() {
|
|
579
|
+
const require = (0, node_module_1.createRequire)(__filename);
|
|
580
|
+
const pkg = require('../package.json');
|
|
581
|
+
return pkg.version || '0.0.0';
|
|
582
|
+
}
|
|
583
|
+
function looksLikeTreeHead(value) {
|
|
584
|
+
if (!value || typeof value !== 'object')
|
|
585
|
+
return false;
|
|
586
|
+
const doc = value;
|
|
587
|
+
if (doc.status === 'not_yet_published')
|
|
588
|
+
return true;
|
|
589
|
+
if (typeof doc.schema === 'string' && doc.schema.startsWith('chit402.tree_head.'))
|
|
590
|
+
return true;
|
|
591
|
+
return Boolean(doc.anchors && doc.root && !doc.task_id);
|
|
592
|
+
}
|
|
593
|
+
function readOfflineWitness(args) {
|
|
594
|
+
let head = null;
|
|
595
|
+
let inclusion = null;
|
|
596
|
+
for (const file of args.positionals.slice(1)) {
|
|
597
|
+
let doc;
|
|
598
|
+
try {
|
|
599
|
+
doc = readJson(file);
|
|
600
|
+
}
|
|
601
|
+
catch (err) {
|
|
602
|
+
throw new Error(`Error reading ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
603
|
+
}
|
|
604
|
+
const row = doc;
|
|
605
|
+
if (looksLikeTreeHead(doc))
|
|
606
|
+
head = doc;
|
|
607
|
+
else if (doc && typeof doc === 'object' && (Array.isArray(row.proof) || row.leaf_index != null)) {
|
|
608
|
+
inclusion = doc;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return { head, inclusion };
|
|
612
|
+
}
|
|
613
|
+
function fileLooksLikeHead(file) {
|
|
614
|
+
if (!file)
|
|
615
|
+
return false;
|
|
616
|
+
try {
|
|
617
|
+
return looksLikeTreeHead(readJson(file));
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function historyHostAllowed(url) {
|
|
624
|
+
try {
|
|
625
|
+
const parsed = new URL(url);
|
|
626
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
|
|
627
|
+
return false;
|
|
628
|
+
if (parsed.protocol === 'http:' && parsed.hostname !== '127.0.0.1' && parsed.hostname !== 'localhost')
|
|
629
|
+
return false;
|
|
630
|
+
if (parsed.protocol === 'https:') {
|
|
631
|
+
return index_js_1.DEFAULT_TRUSTED_JWKS_HOSTS.some((host) => host.toLowerCase() === parsed.hostname.toLowerCase());
|
|
632
|
+
}
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async function loadAnchorIssuerHistory(args, receipt) {
|
|
640
|
+
if (args.issuerHistoryFile) {
|
|
641
|
+
return JSON.parse((0, node_fs_1.readFileSync)(args.issuerHistoryFile, 'utf8'));
|
|
642
|
+
}
|
|
643
|
+
if (args.noIssuerHistory)
|
|
644
|
+
return null;
|
|
645
|
+
const url = (0, issuer_history_js_1.historyUrlFromReceipt)(receipt);
|
|
646
|
+
if (!url || !historyHostAllowed(url))
|
|
647
|
+
return null;
|
|
648
|
+
try {
|
|
649
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(4000) });
|
|
650
|
+
if (!res.ok)
|
|
651
|
+
return null;
|
|
652
|
+
return await res.json();
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function loadAnchorWalletDocument(args, receipt) {
|
|
659
|
+
if (args.anchorWalletsFile) {
|
|
660
|
+
return JSON.parse((0, node_fs_1.readFileSync)(args.anchorWalletsFile, 'utf8'));
|
|
661
|
+
}
|
|
662
|
+
if (args.noIssuerHistory)
|
|
663
|
+
return null;
|
|
664
|
+
const historyUrl = (0, issuer_history_js_1.historyUrlFromReceipt)(receipt);
|
|
665
|
+
const epochOrigin = (() => {
|
|
666
|
+
if (!args.epochUrl)
|
|
667
|
+
return null;
|
|
668
|
+
try {
|
|
669
|
+
return new URL(args.epochUrl).origin;
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
})();
|
|
675
|
+
const origin = epochOrigin || (historyUrl ? new URL(historyUrl).origin : null);
|
|
676
|
+
if (!origin)
|
|
677
|
+
return null;
|
|
678
|
+
const url = `${origin}/.well-known/anchor-wallets.json`;
|
|
679
|
+
if (!historyHostAllowed(url))
|
|
680
|
+
return null;
|
|
681
|
+
try {
|
|
682
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(4000) });
|
|
683
|
+
if (res.status === 404)
|
|
684
|
+
return null;
|
|
685
|
+
if (!res.ok)
|
|
686
|
+
return null;
|
|
687
|
+
return await res.json();
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
410
693
|
async function runAnchor(args) {
|
|
411
694
|
const receiptPath = args.file;
|
|
412
695
|
const inclusionPath = args.inclusionFile || args.positionals[1] || null;
|
|
413
696
|
const headPath = args.headFile || (args.inclusionFile ? null : args.positionals[2]) || null;
|
|
697
|
+
const headCandidates = [args.headFile, headPath, ...args.positionals];
|
|
698
|
+
if (headCandidates.some((file) => file && headStatus(file) === 'not_yet_published')) {
|
|
699
|
+
console.error('Tree head is not_yet_published.');
|
|
700
|
+
return 3;
|
|
701
|
+
}
|
|
414
702
|
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
703
|
console.error('Anchor check needs a receipt, an inclusion proof, and a tree head.');
|
|
421
704
|
console.error(' xfuel-verify receipt.json inclusion.json head.json --rpc');
|
|
422
705
|
return 3;
|
|
@@ -433,6 +716,34 @@ async function runAnchor(args) {
|
|
|
433
716
|
console.error(`Error reading anchor inputs: ${err instanceof Error ? err.message : String(err)}`);
|
|
434
717
|
return 3;
|
|
435
718
|
}
|
|
719
|
+
let fileJwks;
|
|
720
|
+
if (args.jwksFile) {
|
|
721
|
+
try {
|
|
722
|
+
fileJwks = JSON.parse((0, node_fs_1.readFileSync)(args.jwksFile, 'utf8'));
|
|
723
|
+
}
|
|
724
|
+
catch (err) {
|
|
725
|
+
console.error(`Error reading JWKS file: ${err instanceof Error ? err.message : String(err)}`);
|
|
726
|
+
return 3;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const loaded = await (0, index_js_1.loadIssuerJwks)(receipt, {
|
|
730
|
+
jwks: fileJwks,
|
|
731
|
+
jwksUri: args.jwksUrl || undefined,
|
|
732
|
+
fetchJwks: args.fetchJwks,
|
|
733
|
+
});
|
|
734
|
+
const trustedKids = args.noTrustedKid
|
|
735
|
+
? []
|
|
736
|
+
: (args.trustedKids ?? [...index_js_1.DEFAULT_TRUSTED_ISSUER_KIDS]);
|
|
737
|
+
let issuerHistory = null;
|
|
738
|
+
let anchorWalletDocument = null;
|
|
739
|
+
try {
|
|
740
|
+
issuerHistory = await loadAnchorIssuerHistory(args, receipt);
|
|
741
|
+
anchorWalletDocument = await loadAnchorWalletDocument(args, receipt);
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
console.error(`Error reading issuer trust files: ${err instanceof Error ? err.message : String(err)}`);
|
|
745
|
+
return 3;
|
|
746
|
+
}
|
|
436
747
|
let epochRecord = null;
|
|
437
748
|
let verifyEpochSignature;
|
|
438
749
|
if (headNeedsEpochRecord(head)) {
|
|
@@ -443,25 +754,46 @@ async function runAnchor(args) {
|
|
|
443
754
|
epochRecord = null;
|
|
444
755
|
}
|
|
445
756
|
if (epochRecord?.issuer_signature?.jws) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
757
|
+
verifyEpochSignature = (jws) => epochSignatureOk(epochRecord, jws, loaded.jwks, trustedKids);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
let issuerPin;
|
|
761
|
+
const pinClaim = (0, issuer_pin_js_1.readIssuerPinClaim)(receipt).claimed || (0, issuer_pin_js_1.readIssuerPinClaim)(head).claimed;
|
|
762
|
+
const pinRequested = Boolean(args.issuerPinFile) || pinClaim;
|
|
763
|
+
if (pinRequested) {
|
|
764
|
+
try {
|
|
765
|
+
const claimRef = (0, issuer_pin_js_1.readIssuerPinClaim)(receipt).ref || (0, issuer_pin_js_1.readIssuerPinClaim)(head).ref;
|
|
766
|
+
const ref = (args.issuerPinCommit || args.issuerPinSha256 || claimRef)
|
|
767
|
+
? {
|
|
768
|
+
commit: args.issuerPinCommit || claimRef?.commit || '',
|
|
769
|
+
path: claimRef?.path || 'docs/well-known/issuer-key.json',
|
|
770
|
+
sha256: (args.issuerPinSha256 || claimRef?.sha256 || '').toLowerCase(),
|
|
450
771
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
772
|
+
: null;
|
|
773
|
+
let witnesses;
|
|
774
|
+
if (args.issuerWitnessesFile)
|
|
775
|
+
witnesses = JSON.parse((0, node_fs_1.readFileSync)(args.issuerWitnessesFile, 'utf8'));
|
|
776
|
+
const priorRef = args.issuerPriorPinFile
|
|
777
|
+
? {
|
|
778
|
+
commit: args.issuerPriorCommit || '',
|
|
779
|
+
path: 'docs/well-known/issuer-key.json',
|
|
780
|
+
sha256: (args.issuerPriorSha256 || '').toLowerCase(),
|
|
454
781
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
:
|
|
464
|
-
|
|
782
|
+
: null;
|
|
783
|
+
issuerPin = {
|
|
784
|
+
pinBytes: args.issuerPinFile ? (0, node_fs_1.readFileSync)(args.issuerPinFile, 'utf8') : null,
|
|
785
|
+
sigBytes: args.issuerPinSigFile ? (0, node_fs_1.readFileSync)(args.issuerPinSigFile, 'utf8') : null,
|
|
786
|
+
ref,
|
|
787
|
+
witnesses,
|
|
788
|
+
priorPinBytes: args.issuerPriorPinFile ? (0, node_fs_1.readFileSync)(args.issuerPriorPinFile, 'utf8') : null,
|
|
789
|
+
priorRef,
|
|
790
|
+
controlJws: args.issuerControlFile ? (0, node_fs_1.readFileSync)(args.issuerControlFile, 'utf8').trim() : null,
|
|
791
|
+
required: true,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
catch (err) {
|
|
795
|
+
console.error(`Error reading issuer pin: ${err instanceof Error ? err.message : String(err)}`);
|
|
796
|
+
return 3;
|
|
465
797
|
}
|
|
466
798
|
}
|
|
467
799
|
const result = await (0, anchor_witness_js_1.verifyAnchoredRoot)({
|
|
@@ -470,25 +802,112 @@ async function runAnchor(args) {
|
|
|
470
802
|
head,
|
|
471
803
|
baseRpcUrl: args.rpcUrl || undefined,
|
|
472
804
|
solanaRpcUrl: args.solanaRpcUrl || undefined,
|
|
805
|
+
checkNewestAnchor: args.sawRpc,
|
|
473
806
|
epochRecord,
|
|
474
807
|
verifyEpochSignature,
|
|
808
|
+
issuerPin,
|
|
809
|
+
jwks: loaded.jwks,
|
|
810
|
+
trustedKids,
|
|
811
|
+
issuerHistory,
|
|
812
|
+
strictIssuerHistory: args.strictIssuerHistory,
|
|
813
|
+
anchorWalletDocument: anchorWalletDocument,
|
|
814
|
+
});
|
|
815
|
+
let canonicalPreimage = null;
|
|
816
|
+
if (args.canonicalPreimageFile) {
|
|
817
|
+
canonicalPreimage = (0, node_fs_1.readFileSync)(args.canonicalPreimageFile, 'utf8');
|
|
818
|
+
}
|
|
819
|
+
const historyBytes = args.issuerHistoryFile ? (0, node_fs_1.readFileSync)(args.issuerHistoryFile, 'utf8') : null;
|
|
820
|
+
const verified = await (0, index_js_1.verifyReceipt)(receipt, {
|
|
821
|
+
jwks: loaded.jwks,
|
|
822
|
+
jwksUri: args.jwksUrl || undefined,
|
|
823
|
+
fetchJwks: args.fetchJwks,
|
|
824
|
+
trustedKids,
|
|
825
|
+
checkNullifier: args.checkNullifier,
|
|
826
|
+
checkPayer: args.checkPayer,
|
|
827
|
+
rpcUrl: args.rpcUrl || undefined,
|
|
828
|
+
solanaRpcUrl: args.solanaRpcUrl || undefined,
|
|
829
|
+
requirePreimages: !args.noPreimage,
|
|
830
|
+
issuerHistory: args.issuerHistoryFile ? issuerHistory : null,
|
|
831
|
+
issuerHistoryBytes: historyBytes,
|
|
832
|
+
fetchIssuerHistory: !args.noIssuerHistory && !args.issuerHistoryFile,
|
|
833
|
+
strictIssuerHistory: args.strictIssuerHistory,
|
|
834
|
+
skipIssuerHistory: args.noIssuerHistory,
|
|
835
|
+
canonicalPreimage,
|
|
836
|
+
head,
|
|
475
837
|
});
|
|
476
|
-
|
|
477
|
-
|
|
838
|
+
// Inclusion was already checked above. Passing it again re-derives the leaf
|
|
839
|
+
// from book_chain.row_hash only, which rejects a receipt that stores row_hash
|
|
840
|
+
// at the top level. Log mode still requires the receipt itself. A missing or
|
|
841
|
+
// stripped issuer signature, a partial check, or a signed-versus-outer
|
|
842
|
+
// mismatch fails the combined result. An inclusion match is not VERIFIED.
|
|
843
|
+
const issuerSig = receipt.issuer_signature;
|
|
844
|
+
const signaturePresent = Boolean(issuerSig?.jws || issuerSig?.value);
|
|
845
|
+
const receiptVerified = signaturePresent
|
|
846
|
+
&& verified.issuer_signature.valid === true
|
|
847
|
+
&& (verified.overall === 'verified' || verified.overall === 'verified_carried_forward')
|
|
848
|
+
&& verified.claim_mismatches.length === 0;
|
|
849
|
+
const receiptFailed = !receiptVerified;
|
|
850
|
+
const receiptErrors = [...verified.errors];
|
|
851
|
+
if (!signaturePresent) {
|
|
852
|
+
receiptErrors.push(verified.issuer_signature.reason || 'receipt_signature_missing');
|
|
853
|
+
}
|
|
854
|
+
else if (verified.overall === 'partial') {
|
|
855
|
+
receiptErrors.push(verified.issuer_signature.reason || 'receipt_check_partial');
|
|
856
|
+
}
|
|
857
|
+
const errors = receiptFailed
|
|
858
|
+
? [...result.errors, ...receiptErrors.filter((err) => !result.errors.includes(err))]
|
|
859
|
+
: result.errors;
|
|
860
|
+
const combined = {
|
|
861
|
+
...result,
|
|
862
|
+
errors,
|
|
863
|
+
overall: receiptFailed ? 'failed' : result.overall,
|
|
864
|
+
receipt_lane: verified.receipt_lane,
|
|
865
|
+
receipt_check: {
|
|
866
|
+
overall: result.overall === 'pending' && !receiptFailed ? 'pending' : verified.overall,
|
|
867
|
+
verified_scope: verified.verified_scope,
|
|
868
|
+
claim_mismatches: verified.claim_mismatches,
|
|
869
|
+
unsigned_fields: verified.unsigned_fields,
|
|
870
|
+
errors: verified.errors,
|
|
871
|
+
},
|
|
872
|
+
};
|
|
478
873
|
if (args.json) {
|
|
479
|
-
console.log(JSON.stringify(
|
|
874
|
+
console.log(JSON.stringify(combined, null, 2));
|
|
480
875
|
}
|
|
481
876
|
else {
|
|
482
|
-
printAnchor(
|
|
877
|
+
printAnchor(combined, false, args.quiet);
|
|
878
|
+
if (!args.quiet || receiptFailed) {
|
|
879
|
+
const anchorPending = result.overall === 'pending';
|
|
880
|
+
const receiptLabel = anchorPending && !receiptFailed
|
|
881
|
+
? 'PENDING'
|
|
882
|
+
: verified.overall.toUpperCase();
|
|
883
|
+
console.log(` Receipt checks: ${receiptLabel} (signed claims only)`);
|
|
884
|
+
if (verified.claim_mismatches.length > 0) {
|
|
885
|
+
for (const mismatch of verified.claim_mismatches) {
|
|
886
|
+
console.log(` ${mismatch.field}: outer ${mismatch.outer} ≠ signed ${mismatch.signed}`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (!(anchorPending && !receiptFailed))
|
|
890
|
+
printUnsigned(verified.unsigned_fields);
|
|
891
|
+
console.log('');
|
|
892
|
+
}
|
|
483
893
|
if (!args.quiet)
|
|
484
|
-
printLane(
|
|
894
|
+
printLane(verified.receipt_lane);
|
|
485
895
|
}
|
|
486
|
-
if (
|
|
896
|
+
if (combined.overall === 'verified')
|
|
487
897
|
return 0;
|
|
488
|
-
if (
|
|
898
|
+
if (combined.overall === 'partial' || combined.overall === 'pending')
|
|
489
899
|
return 2;
|
|
490
900
|
return 1;
|
|
491
901
|
}
|
|
902
|
+
function printUnsigned(fields) {
|
|
903
|
+
console.log(' Scope: signed claims only. Unsigned fields are UNVERIFIED.');
|
|
904
|
+
if (fields.length === 0)
|
|
905
|
+
return;
|
|
906
|
+
console.log(` Unsigned fields (${fields.length})`);
|
|
907
|
+
console.log(' ─────────────────────────────────────────────────');
|
|
908
|
+
for (const field of fields)
|
|
909
|
+
console.log(` UNVERIFIED ${field}`);
|
|
910
|
+
}
|
|
492
911
|
async function runRefusal(doc, args) {
|
|
493
912
|
const trustedKids = args.trustedKids ?? index_js_1.DEFAULT_TRUSTED_ISSUER_KIDS;
|
|
494
913
|
const result = (0, index_js_1.verifyRefusal)(doc, {
|
|
@@ -511,7 +930,7 @@ async function runRefusal(doc, args) {
|
|
|
511
930
|
issuer_history: result.issuer_history,
|
|
512
931
|
}) : null;
|
|
513
932
|
const history = args.skipIssuerHistory
|
|
514
|
-
? { checked: false, ok: true, unreachable: false, warning: null, reason: null, kid: result.kid ?? null }
|
|
933
|
+
? { checked: false, ok: true, unreachable: false, warning: null, reason: null, kid: result.kid ?? null, document: null, loaded: false }
|
|
515
934
|
: await (0, issuer_history_js_1.checkReceiptIssuerHistory)(doc, {
|
|
516
935
|
document: args.issuerHistory,
|
|
517
936
|
documentBytes: args.issuerHistoryBytes,
|
|
@@ -571,14 +990,45 @@ async function runRefusal(doc, args) {
|
|
|
571
990
|
}
|
|
572
991
|
async function main() {
|
|
573
992
|
const args = parseArgs(process.argv.slice(2));
|
|
574
|
-
|
|
993
|
+
if (args.version) {
|
|
994
|
+
console.log(`@xfuel/verify ${packageVersion()}`);
|
|
995
|
+
return 0;
|
|
996
|
+
}
|
|
997
|
+
let anchorMode = args.anchorFlag
|
|
575
998
|
|| Boolean(args.inclusionFile || args.headFile)
|
|
576
999
|
|| (args.positionals.length >= 3 && args.sawRpc);
|
|
1000
|
+
if (!anchorMode && args.sawRpc && args.positionals.some((file) => fileLooksLikeHead(file))) {
|
|
1001
|
+
anchorMode = true;
|
|
1002
|
+
}
|
|
577
1003
|
if (args.help) {
|
|
578
1004
|
console.log(HELP);
|
|
579
1005
|
return 0;
|
|
580
1006
|
}
|
|
581
|
-
if (
|
|
1007
|
+
if (args.url && !args.jwsFile) {
|
|
1008
|
+
try {
|
|
1009
|
+
const response = await fetch(args.url);
|
|
1010
|
+
await response.arrayBuffer();
|
|
1011
|
+
}
|
|
1012
|
+
catch {
|
|
1013
|
+
// A failed fetch is still not a pass.
|
|
1014
|
+
}
|
|
1015
|
+
console.log('owner_proof_required');
|
|
1016
|
+
console.log(' Overall: OWNER_PROOF_REQUIRED');
|
|
1017
|
+
return 1;
|
|
1018
|
+
}
|
|
1019
|
+
let offlineWitness = null;
|
|
1020
|
+
if (!anchorMode) {
|
|
1021
|
+
try {
|
|
1022
|
+
offlineWitness = readOfflineWitness(args);
|
|
1023
|
+
}
|
|
1024
|
+
catch (err) {
|
|
1025
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1026
|
+
return 3;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
// A shell plus a holder is not anchor mode. Inclusion is checked with the
|
|
1030
|
+
// holder. Anchor mode would verify the unsigned shell and skip the key.
|
|
1031
|
+
if (anchorMode && !args.jwsFile && !args.acceptShell)
|
|
582
1032
|
return runAnchor(args);
|
|
583
1033
|
if (!args.file) {
|
|
584
1034
|
console.log(HELP);
|
|
@@ -592,7 +1042,150 @@ async function main() {
|
|
|
592
1042
|
console.error(`Error reading receipt: ${err instanceof Error ? err.message : String(err)}`);
|
|
593
1043
|
return 3;
|
|
594
1044
|
}
|
|
595
|
-
if (
|
|
1045
|
+
if ((0, shell_js_1.isReceiptShell)(receipt) || args.acceptShell || args.jwsFile) {
|
|
1046
|
+
if (!(0, shell_js_1.isReceiptShell)(receipt)) {
|
|
1047
|
+
console.error('Invalid shell: schema is not chit402.receipt_shell.v1');
|
|
1048
|
+
return 3;
|
|
1049
|
+
}
|
|
1050
|
+
if (!args.jwsFile) {
|
|
1051
|
+
console.log(shell_js_1.INCLUDED_SHELL_LINE);
|
|
1052
|
+
console.log(' Overall: INCLUDED_SHELL');
|
|
1053
|
+
return args.acceptShell ? 0 : 1;
|
|
1054
|
+
}
|
|
1055
|
+
let holder;
|
|
1056
|
+
try {
|
|
1057
|
+
const text = (0, node_fs_1.readFileSync)(args.jwsFile, 'utf8');
|
|
1058
|
+
let parsed = text;
|
|
1059
|
+
try {
|
|
1060
|
+
parsed = JSON.parse(text);
|
|
1061
|
+
}
|
|
1062
|
+
catch {
|
|
1063
|
+
parsed = text;
|
|
1064
|
+
}
|
|
1065
|
+
holder = (0, shell_js_1.holderDocumentFromOwnerView)(parsed);
|
|
1066
|
+
}
|
|
1067
|
+
catch (err) {
|
|
1068
|
+
console.error(`Error reading JWS: ${err instanceof Error ? err.message : String(err)}`);
|
|
1069
|
+
return 3;
|
|
1070
|
+
}
|
|
1071
|
+
const shellDoc = receipt;
|
|
1072
|
+
const compared = (0, shell_js_1.compareShellToJws)(shellDoc, holder);
|
|
1073
|
+
if (!compared.ok) {
|
|
1074
|
+
console.log('shell_jws_mismatch');
|
|
1075
|
+
if (compared.field)
|
|
1076
|
+
console.log(` Field: ${compared.field}`);
|
|
1077
|
+
console.log(' Overall: FAILED');
|
|
1078
|
+
return 1;
|
|
1079
|
+
}
|
|
1080
|
+
let jwks;
|
|
1081
|
+
if (args.jwksFile) {
|
|
1082
|
+
try {
|
|
1083
|
+
jwks = JSON.parse((0, node_fs_1.readFileSync)(args.jwksFile, 'utf8'));
|
|
1084
|
+
}
|
|
1085
|
+
catch (err) {
|
|
1086
|
+
console.error(`Error reading JWKS file: ${err instanceof Error ? err.message : String(err)}`);
|
|
1087
|
+
return 3;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
let inclusion = offlineWitness?.inclusion ?? null;
|
|
1091
|
+
let head = offlineWitness?.head ?? null;
|
|
1092
|
+
try {
|
|
1093
|
+
if (args.inclusionFile)
|
|
1094
|
+
inclusion = readJson(args.inclusionFile);
|
|
1095
|
+
if (args.headFile)
|
|
1096
|
+
head = readJson(args.headFile);
|
|
1097
|
+
}
|
|
1098
|
+
catch (err) {
|
|
1099
|
+
console.error(`Error reading inclusion: ${err instanceof Error ? err.message : String(err)}`);
|
|
1100
|
+
return 3;
|
|
1101
|
+
}
|
|
1102
|
+
const trustedKids = args.noTrustedKid
|
|
1103
|
+
? []
|
|
1104
|
+
: (args.trustedKids ?? [...index_js_1.DEFAULT_TRUSTED_ISSUER_KIDS]);
|
|
1105
|
+
const result = await (0, index_js_1.verifyReceipt)(holder, {
|
|
1106
|
+
jwks,
|
|
1107
|
+
jwksUri: args.jwksUrl || undefined,
|
|
1108
|
+
fetchJwks: args.fetchJwks,
|
|
1109
|
+
trustedKids,
|
|
1110
|
+
checkPayer: args.checkPayer,
|
|
1111
|
+
rpcUrl: args.rpcUrl || undefined,
|
|
1112
|
+
solanaRpcUrl: args.solanaRpcUrl || undefined,
|
|
1113
|
+
requirePreimages: false,
|
|
1114
|
+
skipIssuerHistory: args.noIssuerHistory || !args.issuerHistoryFile,
|
|
1115
|
+
issuerHistory: null,
|
|
1116
|
+
fetchIssuerHistory: false,
|
|
1117
|
+
salt: args.salt,
|
|
1118
|
+
head: head ?? undefined,
|
|
1119
|
+
inclusion: inclusion ?? undefined,
|
|
1120
|
+
});
|
|
1121
|
+
const inclusionErrors = result.errors.filter((err) => /inclusion|leaf|row_hash|bad_root|tree_size|tree_head|no_leaf/.test(err));
|
|
1122
|
+
const shellLeaf = (shellDoc.inclusion && typeof shellDoc.inclusion === 'object')
|
|
1123
|
+
? shellDoc.inclusion.leaf_hash
|
|
1124
|
+
: null;
|
|
1125
|
+
const provedLeaf = inclusion && typeof inclusion.leaf === 'string'
|
|
1126
|
+
? inclusion.leaf
|
|
1127
|
+
: null;
|
|
1128
|
+
// A head the user wrote by hand ({root, tree_size}) is not a log. The
|
|
1129
|
+
// inclusion only counts against a head that carries an issuer JWS;
|
|
1130
|
+
// verifyReceipt() checks that JWS against the same trust set.
|
|
1131
|
+
const headSig = head && typeof head === 'object'
|
|
1132
|
+
? head.issuer_signature
|
|
1133
|
+
: null;
|
|
1134
|
+
const headSigned = !!headSig && typeof headSig.jws === 'string' && headSig.jws.split('.').length === 3;
|
|
1135
|
+
const verdict = (0, shell_js_1.shellHolderVerdict)({
|
|
1136
|
+
signatureValid: result.issuer_signature.valid === true,
|
|
1137
|
+
keyTrusted: result.issuer_signature.key_trusted === true,
|
|
1138
|
+
signatureReason: result.issuer_signature.reason || (result.issuer_signature.key_trusted ? null : 'key untrusted'),
|
|
1139
|
+
inclusionSupplied: inclusion != null,
|
|
1140
|
+
inclusionOk: inclusion != null && inclusionErrors.length === 0,
|
|
1141
|
+
inclusionReason: inclusion == null ? 'inclusion_missing' : (inclusionErrors[0] || null),
|
|
1142
|
+
headSigned,
|
|
1143
|
+
paymentRef: result.tx,
|
|
1144
|
+
bindingExpected: !!result.binding.expected,
|
|
1145
|
+
bindingMatches: result.binding.matches === true,
|
|
1146
|
+
shellLeaf: shellLeaf == null ? null : String(shellLeaf),
|
|
1147
|
+
provedLeaf,
|
|
1148
|
+
});
|
|
1149
|
+
if (!verdict.verified || (result.overall !== 'verified' && result.overall !== 'verified_carried_forward')) {
|
|
1150
|
+
const reason = verdict.reason || result.errors[0] || 'failed';
|
|
1151
|
+
console.log(reason);
|
|
1152
|
+
console.log(' Overall: FAILED');
|
|
1153
|
+
return 1;
|
|
1154
|
+
}
|
|
1155
|
+
if (args.checkPayer && !result.payer.valid) {
|
|
1156
|
+
console.log(result.payer.reason || 'payment_unchecked');
|
|
1157
|
+
console.log(' Overall: FAILED');
|
|
1158
|
+
return 1;
|
|
1159
|
+
}
|
|
1160
|
+
const issuer = holder.issuer_signature;
|
|
1161
|
+
if (args.salt) {
|
|
1162
|
+
const opened = (0, shell_js_1.openV11Commitment)({
|
|
1163
|
+
payload: (0, shell_js_1.decodeJwsPayload)(issuer?.jws),
|
|
1164
|
+
saltHex: args.salt || '',
|
|
1165
|
+
body: Buffer.alloc(0),
|
|
1166
|
+
});
|
|
1167
|
+
if (!opened.ok) {
|
|
1168
|
+
console.log(opened.reason || 'commitment_mismatch');
|
|
1169
|
+
console.log(' Overall: FAILED');
|
|
1170
|
+
return 1;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
console.log(' Overall: VERIFIED');
|
|
1174
|
+
return 0;
|
|
1175
|
+
}
|
|
1176
|
+
const v11 = receipt.v === 11 || (receipt.issuer_signature?.jws
|
|
1177
|
+
? (() => {
|
|
1178
|
+
try {
|
|
1179
|
+
const part = String(receipt.issuer_signature.jws).split('.')[1];
|
|
1180
|
+
const payload = JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
|
|
1181
|
+
return payload?.v === 11;
|
|
1182
|
+
}
|
|
1183
|
+
catch {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
})()
|
|
1187
|
+
: false);
|
|
1188
|
+
if (!receipt.task_id && !v11) {
|
|
596
1189
|
console.error('Invalid receipt: missing task_id');
|
|
597
1190
|
return 3;
|
|
598
1191
|
}
|
|
@@ -658,6 +1251,42 @@ async function main() {
|
|
|
658
1251
|
return 3;
|
|
659
1252
|
}
|
|
660
1253
|
}
|
|
1254
|
+
const carryAsked = args.carryOldHeadFile || args.carryOldInclusionFile
|
|
1255
|
+
|| args.carryHeadFile || args.carryInclusionFiles.length > 0;
|
|
1256
|
+
let carry = null;
|
|
1257
|
+
if (carryAsked) {
|
|
1258
|
+
try {
|
|
1259
|
+
carry = {
|
|
1260
|
+
oldHead: args.carryOldHeadFile ? readJson(args.carryOldHeadFile) : {},
|
|
1261
|
+
oldInclusion: args.carryOldInclusionFile ? readJson(args.carryOldInclusionFile) : { error: 'not_in_tree' },
|
|
1262
|
+
currentHead: args.carryHeadFile ? readJson(args.carryHeadFile) : {},
|
|
1263
|
+
currentInclusions: args.carryInclusionFiles.length > 0
|
|
1264
|
+
? args.carryInclusionFiles.map((file) => readJson(file))
|
|
1265
|
+
: [{ error: 'not_in_tree' }],
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
catch (err) {
|
|
1269
|
+
console.error(`Error reading carry-forward files: ${err instanceof Error ? err.message : String(err)}`);
|
|
1270
|
+
return 3;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
let open;
|
|
1274
|
+
if (args.open.length > 0) {
|
|
1275
|
+
open = {};
|
|
1276
|
+
try {
|
|
1277
|
+
for (const spec of args.open) {
|
|
1278
|
+
const opened = (0, v11_receipt_js_1.readOpenFile)(spec);
|
|
1279
|
+
if (opened.kind === 'output')
|
|
1280
|
+
open.output = opened.bytes;
|
|
1281
|
+
else
|
|
1282
|
+
open[opened.kind] = opened.text;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
catch (err) {
|
|
1286
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1287
|
+
return 3;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
661
1290
|
const result = await (0, index_js_1.verifyReceipt)(receipt, {
|
|
662
1291
|
jwks,
|
|
663
1292
|
jwksUri: args.jwksUrl || undefined,
|
|
@@ -674,6 +1303,11 @@ async function main() {
|
|
|
674
1303
|
strictIssuerHistory: args.strictIssuerHistory,
|
|
675
1304
|
skipIssuerHistory: args.noIssuerHistory,
|
|
676
1305
|
canonicalPreimage,
|
|
1306
|
+
salt: args.salt,
|
|
1307
|
+
open,
|
|
1308
|
+
head: offlineWitness?.head ?? undefined,
|
|
1309
|
+
inclusion: offlineWitness?.inclusion ?? undefined,
|
|
1310
|
+
carry,
|
|
677
1311
|
});
|
|
678
1312
|
if (args.json) {
|
|
679
1313
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -746,6 +1380,8 @@ async function main() {
|
|
|
746
1380
|
}
|
|
747
1381
|
}
|
|
748
1382
|
console.log('');
|
|
1383
|
+
printUnsigned(result.unsigned_fields);
|
|
1384
|
+
console.log('');
|
|
749
1385
|
console.log(` Nullifier`);
|
|
750
1386
|
console.log(` ─────────────────────────────────────────────────`);
|
|
751
1387
|
if (result.nullifier.nullifier) {
|
|
@@ -771,6 +1407,18 @@ async function main() {
|
|
|
771
1407
|
else if (result.issuer_history.warning) {
|
|
772
1408
|
console.log(` Issuer history: ${result.issuer_history.warning}`);
|
|
773
1409
|
}
|
|
1410
|
+
if (result.carry_forward?.status === 'VERIFIED_CARRIED_FORWARD') {
|
|
1411
|
+
const carried = result.carry_forward;
|
|
1412
|
+
console.log(` Log`);
|
|
1413
|
+
console.log(` ─────────────────────────────────────────────────`);
|
|
1414
|
+
console.log(` Status: ${carried.status}`);
|
|
1415
|
+
console.log(` Issued at: ${carried.issued_at}`);
|
|
1416
|
+
console.log(` Issued epoch: ${carried.issued_epoch}`);
|
|
1417
|
+
console.log(` Logged at: ${carried.logged_at}`);
|
|
1418
|
+
console.log(` Logged epoch: ${carried.logged_epoch}`);
|
|
1419
|
+
console.log(` Leaf index: ${carried.leaf_index}`);
|
|
1420
|
+
console.log('');
|
|
1421
|
+
}
|
|
774
1422
|
console.log(` Overall: ${result.overall.toUpperCase()}`);
|
|
775
1423
|
if (result.errors.length > 0) {
|
|
776
1424
|
console.log(` Errors: ${result.errors.join(', ')}`);
|
|
@@ -779,6 +1427,7 @@ async function main() {
|
|
|
779
1427
|
}
|
|
780
1428
|
switch (result.overall) {
|
|
781
1429
|
case 'verified':
|
|
1430
|
+
case 'verified_carried_forward':
|
|
782
1431
|
return 0;
|
|
783
1432
|
case 'failed':
|
|
784
1433
|
return 1;
|