@xfuel/verify 0.1.1 → 0.3.0
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 +62 -17
- package/dist/anchor-witness.d.ts +138 -0
- package/dist/anchor-witness.d.ts.map +1 -0
- package/dist/anchor-witness.js +365 -0
- package/dist/anchor-witness.js.map +1 -0
- package/dist/base-payer.d.ts +8 -1
- package/dist/base-payer.d.ts.map +1 -1
- package/dist/base-payer.js +64 -20
- package/dist/base-payer.js.map +1 -1
- 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 +424 -25
- package/dist/cli.js.map +1 -1
- 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 +263 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +919 -140
- 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/jws.d.ts +23 -0
- package/dist/jws.d.ts.map +1 -1
- package/dist/jws.js +71 -1
- package/dist/jws.js.map +1 -1
- package/dist/payer.d.ts +16 -5
- package/dist/payer.d.ts.map +1 -1
- package/dist/payer.js +38 -26
- package/dist/payer.js.map +1 -1
- 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 +9 -5
package/dist/index.js
CHANGED
|
@@ -12,16 +12,24 @@
|
|
|
12
12
|
* See docs/specs/RECEIPT_V2_SEMANTICS.md for the verification algorithm.
|
|
13
13
|
*/
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.verifyIssuerJws = exports.resolvePinnedIssuerJwk = exports.canonicalSignedPayload = exports.BASE_SEPOLIA_RPC_URL = exports.ZK_VERIFIER_ADDRESS = exports.decodeJwsPayload = exports.receiptPayerClaimsFromEnvelope = exports.verifyPayerBinding = exports.SOLANA_USDC_MINT_DEVNET = exports.SOLANA_USDC_MINT_MAINNET = exports.SOLANA_RPC_URL = exports.fetchSolanaTransaction = exports.inferUsdcOutflowFromBalances = exports.extractUsdcTransfersFromTx = exports.isSolanaBase58Pubkey = exports.parseSolanaPaymentRef = exports.verifySolanaPayer = exports.BASE_RPC_URL = exports.ERC20_TRANSFER_TOPIC = exports.USDC_ADDRESSES = exports.fetchBaseTransactionReceipt = exports.sumUsdcTransfersFromPayer = exports.isEvmAddress = exports.parseBasePaymentRef = exports.verifyBasePayer = exports.computeInferenceBinding = exports.computePaymentCommitment = void 0;
|
|
15
|
+
exports.REFUSAL_SCHEMA = exports.verifyRefusal = exports.isRefusalDocument = exports.HEAD_BINDING_PAYLOAD_VERSION = exports.outerHeadDisagrees = exports.signedHeadBinding = exports.headBindingVerdict = exports.RECEIPT_LANE_BOUNDARY = exports.RECEIPT_LANE_ORDERING = exports.RECEIPT_LANE_RULE = exports.RECEIPT_LANE_SCHEMA = exports.anchorChangedSinceBinding = exports.deriveSettled = exports.deriveSettledBy = exports.receiptLaneDecision = exports.receiptLaneFromVerification = exports.buildReceiptLane = exports.jwksHostAllowed = exports.isPinnedTrustedJwk = exports.jwkThumbprint = exports.KEY_UNTRUSTED = exports.DEFAULT_TRUSTED_JWKS_HOSTS = exports.DEFAULT_TRUSTED_ISSUER_KIDS = exports.verifyIssuerJws = exports.resolvePinnedIssuerJwk = exports.canonicalSignedPayload = exports.BASE_SEPOLIA_RPC_URL = exports.ZK_VERIFIER_ADDRESS = exports.decodeJwsPayload = exports.receiptPayerClaimsFromEnvelope = exports.verifyPayerBinding = exports.SOLANA_USDC_MINT_DEVNET = exports.SOLANA_USDC_MINT_MAINNET = exports.SOLANA_RPC_URL = exports.fetchSolanaTransaction = exports.inferUsdcOutflowFromBalances = exports.extractUsdcTransfersFromTx = exports.isSolanaBase58Pubkey = exports.parseSolanaPaymentRef = exports.verifySolanaPayer = exports.BASE_RPC_URL = exports.ERC20_TRANSFER_TOPIC = exports.USDC_ADDRESSES = exports.fetchBaseTransactionReceipt = exports.sumUsdcTransfersFromPayer = exports.isEvmAddress = exports.parseBasePaymentRef = exports.verifyBasePayer = exports.computeInferenceBinding = exports.computePaymentCommitment = void 0;
|
|
16
|
+
exports.issuerHistoryDocumentHash = exports.readIssuerHistoryPin = exports.CANONICAL_PAYLOAD_VERSION = exports.verifyCanonicalPreimageBytes = exports.BASE_MAINNET_CHAIN_ID = exports.SOLANA_GENESIS = exports.ANCHOR_DOES_NOT_PROVE = exports.ANCHOR_PROVES = exports.leafHash = exports.parseAnchorMemo = exports.extractMemos = exports.verifyMerkleInclusion = exports.verifyAnchoredRoot = exports.REFUSAL_DOES_NOT_PROVE = exports.REFUSAL_PROVES = exports.REFUSAL_PAYLOAD_VERSION = void 0;
|
|
17
|
+
exports.canonicalPayloadVersion = canonicalPayloadVersion;
|
|
16
18
|
exports.canonicalIssuerPayload = canonicalIssuerPayload;
|
|
19
|
+
exports.sumUsdcTransfersToPayee = sumUsdcTransfersToPayee;
|
|
20
|
+
exports.reconcileSettledTransfer = reconcileSettledTransfer;
|
|
17
21
|
exports.verifyIssuerSignature = verifyIssuerSignature;
|
|
18
22
|
exports.verifyIssuerSignatureWithJwks = verifyIssuerSignatureWithJwks;
|
|
19
23
|
exports.verifyBinding = verifyBinding;
|
|
20
24
|
exports.verifyNullifier = verifyNullifier;
|
|
21
25
|
exports.hashCanonicalPayload = hashCanonicalPayload;
|
|
26
|
+
exports.claimIdVerdict = claimIdVerdict;
|
|
27
|
+
exports.diffOuterClaims = diffOuterClaims;
|
|
28
|
+
exports.fetchIssuerJwks = fetchIssuerJwks;
|
|
22
29
|
exports.receiptPayerClaims = receiptPayerClaims;
|
|
23
30
|
exports.verifyReceipt = verifyReceipt;
|
|
24
31
|
const ethers_1 = require("ethers");
|
|
32
|
+
const anchor_witness_js_1 = require("./anchor-witness.js");
|
|
25
33
|
const node_crypto_1 = require("node:crypto");
|
|
26
34
|
const binding_js_1 = require("./binding.js");
|
|
27
35
|
Object.defineProperty(exports, "computePaymentCommitment", { enumerable: true, get: function () { return binding_js_1.computePaymentCommitment; } });
|
|
@@ -50,6 +58,12 @@ Object.defineProperty(exports, "verifyPayerBinding", { enumerable: true, get: fu
|
|
|
50
58
|
Object.defineProperty(exports, "receiptPayerClaimsFromEnvelope", { enumerable: true, get: function () { return payer_js_1.receiptPayerClaimsFromEnvelope; } });
|
|
51
59
|
Object.defineProperty(exports, "decodeJwsPayload", { enumerable: true, get: function () { return payer_js_1.decodeJwsPayload; } });
|
|
52
60
|
const jws_js_1 = require("./jws.js");
|
|
61
|
+
const head_binding_js_1 = require("./head-binding.js");
|
|
62
|
+
const receipt_lane_js_1 = require("./receipt-lane.js");
|
|
63
|
+
const refusal_js_1 = require("./refusal.js");
|
|
64
|
+
const preimage_js_1 = require("./preimage.js");
|
|
65
|
+
const issuer_history_js_1 = require("./issuer-history.js");
|
|
66
|
+
const canonical_preimage_js_1 = require("./canonical-preimage.js");
|
|
53
67
|
/** ZKVerifierSP1 contract on Base mainnet. */
|
|
54
68
|
exports.ZK_VERIFIER_ADDRESS = '0x9373499645292715a2275A78eD65B14215C41c06';
|
|
55
69
|
/** Base Sepolia testnet RPC. */
|
|
@@ -59,28 +73,28 @@ const ZK_VERIFIER_ABI = [
|
|
|
59
73
|
'function usedNullifiers(bytes32) view returns (bool)',
|
|
60
74
|
];
|
|
61
75
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* 2. payment.rail
|
|
68
|
-
* 3. payment.ref
|
|
69
|
-
* 4. payment.gross_amount
|
|
70
|
-
* 5. payment.net_amount
|
|
71
|
-
* 6. payment.fee_amount
|
|
72
|
-
* 7. payment.protocol_fee_bps ?? payment.fee_bps
|
|
73
|
-
* 8. payment.platform_fee
|
|
74
|
-
* 9. payment.platform_fee_bps
|
|
75
|
-
* 10. provider_cogs.actual
|
|
76
|
-
* 11. route.model
|
|
77
|
-
* 12. route.model_commitment.commitment
|
|
78
|
-
* 13. route.provider
|
|
79
|
-
* 14. output.hash
|
|
80
|
-
* 15. binding.expected_commitment
|
|
76
|
+
* HMAC payload version.
|
|
77
|
+
* Versions <= 7 keep the historical net/fee field list.
|
|
78
|
+
* Version 8 signs settled_amount plus the internal accounting breakdown.
|
|
79
|
+
* Lockstep with `canonicalPayloadVersion` in services/gateway/src/receipt.js
|
|
80
|
+
* for the v8 list (the v7 list here is the historical 15-field public formula).
|
|
81
81
|
*/
|
|
82
|
-
function
|
|
83
|
-
|
|
82
|
+
function canonicalPayloadVersion(receipt) {
|
|
83
|
+
const stamped = receipt.hmac_attestation?.payload_version
|
|
84
|
+
?? receipt.signature?.payload_version;
|
|
85
|
+
if (stamped != null)
|
|
86
|
+
return Number(stamped);
|
|
87
|
+
const issuerV = receipt.issuer_signature?.payload_version;
|
|
88
|
+
if (issuerV != null)
|
|
89
|
+
return Number(issuerV);
|
|
90
|
+
const payment = receipt.payment;
|
|
91
|
+
if (payment?.accounting || (payment && Object.prototype.hasOwnProperty.call(payment, 'settled_amount'))) {
|
|
92
|
+
return 8;
|
|
93
|
+
}
|
|
94
|
+
return 7;
|
|
95
|
+
}
|
|
96
|
+
function canonicalFieldsV7(receipt) {
|
|
97
|
+
return [
|
|
84
98
|
receipt.task_id ?? null,
|
|
85
99
|
receipt.payment?.rail ?? null,
|
|
86
100
|
receipt.payment?.ref ?? null,
|
|
@@ -96,7 +110,204 @@ function canonicalIssuerPayload(receipt) {
|
|
|
96
110
|
receipt.route?.provider ?? null,
|
|
97
111
|
receipt.output?.hash ?? null,
|
|
98
112
|
receipt.binding?.expected_commitment ?? null,
|
|
99
|
-
]
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
function canonicalFieldsV8(receipt) {
|
|
116
|
+
const breakdown = receipt.payment?.accounting?.internal_breakdown;
|
|
117
|
+
const caller = receipt.caller_binding;
|
|
118
|
+
return [
|
|
119
|
+
receipt.task_id ?? null,
|
|
120
|
+
receipt.payment?.rail ?? null,
|
|
121
|
+
receipt.payment?.ref ?? null,
|
|
122
|
+
receipt.payment?.gross_amount ?? null,
|
|
123
|
+
receipt.payment?.settled_amount ?? null,
|
|
124
|
+
breakdown?.route_margin_bps ?? null,
|
|
125
|
+
breakdown?.route_margin_amount ?? null,
|
|
126
|
+
breakdown?.receipt_floor_amount ?? null,
|
|
127
|
+
breakdown?.provider_cogs_amount ?? null,
|
|
128
|
+
breakdown?.tier2_proof_amount ?? null,
|
|
129
|
+
receipt.provider_cogs?.actual ?? null,
|
|
130
|
+
receipt.route?.model ?? null,
|
|
131
|
+
receipt.route?.model_commitment?.commitment ?? null,
|
|
132
|
+
receipt.route?.provider ?? null,
|
|
133
|
+
receipt.output?.hash ?? null,
|
|
134
|
+
receipt.binding?.expected_commitment ?? null,
|
|
135
|
+
caller?.payer_wallet ?? null,
|
|
136
|
+
caller?.agent_pubkey ?? null,
|
|
137
|
+
caller?.api_key_hash ?? null,
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Canonical, order-stable payload an HMAC issuer signature covers.
|
|
142
|
+
* v8 matches `canonicalSignedPayload` in services/gateway/src/receipt.js.
|
|
143
|
+
* v7 and earlier stay on the historical 15-field list so old signatures verify.
|
|
144
|
+
*/
|
|
145
|
+
function canonicalIssuerPayload(receipt) {
|
|
146
|
+
const fields = canonicalPayloadVersion(receipt) >= 8
|
|
147
|
+
? canonicalFieldsV8(receipt)
|
|
148
|
+
: canonicalFieldsV7(receipt);
|
|
149
|
+
return JSON.stringify(fields);
|
|
150
|
+
}
|
|
151
|
+
function topicAddress(topic) {
|
|
152
|
+
const hex = topic.toLowerCase().replace(/^0x/, '');
|
|
153
|
+
return `0x${hex.slice(-40)}`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Sum USDC Transfer values to `payee` in a transaction's logs.
|
|
157
|
+
* Non-Transfer logs are ignored. When `usdcAddress` is set, other contracts are ignored.
|
|
158
|
+
*/
|
|
159
|
+
function sumUsdcTransfersToPayee(logs, payee, usdcAddress) {
|
|
160
|
+
if (!payee || !(0, base_payer_js_1.isEvmAddress)(payee))
|
|
161
|
+
return null;
|
|
162
|
+
const payeeLower = payee.toLowerCase();
|
|
163
|
+
const usdcLower = usdcAddress?.toLowerCase();
|
|
164
|
+
let total = 0n;
|
|
165
|
+
let found = false;
|
|
166
|
+
for (const log of logs || []) {
|
|
167
|
+
if (usdcLower && log.address?.toLowerCase() !== usdcLower)
|
|
168
|
+
continue;
|
|
169
|
+
const topics = log.topics || [];
|
|
170
|
+
if (topics[0]?.toLowerCase() !== base_payer_js_1.ERC20_TRANSFER_TOPIC)
|
|
171
|
+
continue;
|
|
172
|
+
if (topics.length < 3 || !topics[2])
|
|
173
|
+
continue;
|
|
174
|
+
if (topicAddress(topics[2]) !== payeeLower)
|
|
175
|
+
continue;
|
|
176
|
+
found = true;
|
|
177
|
+
total += BigInt(log.data || '0x0');
|
|
178
|
+
}
|
|
179
|
+
return found ? total : null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Payment facts for on-chain reconciliation.
|
|
183
|
+
* A JWS is used only after `verifyIssuerSignatureWithJwks` accepts the key.
|
|
184
|
+
* An untrusted or invalid JWS is not decoded into the comparison, and the
|
|
185
|
+
* unsigned outer `payment` copy is not a fallback for those claims.
|
|
186
|
+
* Receipts with no JWS still use the outer payment (legacy envelopes).
|
|
187
|
+
*/
|
|
188
|
+
function trustedPaymentForReconcile(receipt, options) {
|
|
189
|
+
const outerVersion = Number(receipt.issuer_signature?.payload_version ?? canonicalPayloadVersion(receipt));
|
|
190
|
+
const jws = receipt.issuer_signature?.jws;
|
|
191
|
+
if (!jws) {
|
|
192
|
+
return { ok: true, version: outerVersion, payment: receipt.payment || {} };
|
|
193
|
+
}
|
|
194
|
+
const verification = verifyIssuerSignatureWithJwks(receipt, options.jwks, {
|
|
195
|
+
trustedKids: options.trustedKids,
|
|
196
|
+
});
|
|
197
|
+
if (!verification.valid || verification.key_trusted === false) {
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
version: outerVersion,
|
|
201
|
+
reason: verification.reason || jws_js_1.KEY_UNTRUSTED,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const claims = verification.payload;
|
|
205
|
+
const claimPayment = (claims?.payment && typeof claims.payment === 'object')
|
|
206
|
+
? claims.payment
|
|
207
|
+
: null;
|
|
208
|
+
if (!claimPayment) {
|
|
209
|
+
return { ok: false, version: outerVersion, reason: 'no_signed_amount' };
|
|
210
|
+
}
|
|
211
|
+
const version = typeof claims?.payload_version === 'number'
|
|
212
|
+
? claims.payload_version
|
|
213
|
+
: outerVersion;
|
|
214
|
+
return { ok: true, version: Number(version), payment: claimPayment };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Optional on-chain reconciliation. Given a tx receipt's logs, the signed
|
|
218
|
+
* amount the payee was supposed to receive must equal the USDC Transfer to payee.
|
|
219
|
+
*
|
|
220
|
+
* The amount and payee come from JWS claims only after a key-trust check
|
|
221
|
+
* (JWKS by kid, or an embedded key whose thumbprint is a pinned kid). An
|
|
222
|
+
* untrusted signature does not contribute those facts.
|
|
223
|
+
*
|
|
224
|
+
* v8 compares `settled_amount` (falling back to `gross_amount`): that is the
|
|
225
|
+
* on-chain transfer. v7 and earlier compare `net_amount`, which those receipts
|
|
226
|
+
* defined as "amount after fees". A single direct transfer of gross will not
|
|
227
|
+
* match a net that subtracts a fee that never moved on chain.
|
|
228
|
+
*/
|
|
229
|
+
function reconcileSettledTransfer(receipt, logs, options = {}) {
|
|
230
|
+
const trusted = trustedPaymentForReconcile(receipt, options);
|
|
231
|
+
if (!trusted.ok) {
|
|
232
|
+
return {
|
|
233
|
+
checked: false,
|
|
234
|
+
matches: false,
|
|
235
|
+
payload_version: trusted.version,
|
|
236
|
+
signed_field: null,
|
|
237
|
+
signed_amount: null,
|
|
238
|
+
transfer_amount: null,
|
|
239
|
+
payee: null,
|
|
240
|
+
reason: trusted.reason,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const { version, payment } = trusted;
|
|
244
|
+
const payee = payment.payee ?? null;
|
|
245
|
+
let signed_field = null;
|
|
246
|
+
let signed_amount = null;
|
|
247
|
+
if (version >= 8) {
|
|
248
|
+
if (payment.settled_amount != null && payment.settled_amount !== '') {
|
|
249
|
+
signed_field = 'settled_amount';
|
|
250
|
+
signed_amount = String(payment.settled_amount);
|
|
251
|
+
}
|
|
252
|
+
else if (payment.gross_amount != null && payment.gross_amount !== '') {
|
|
253
|
+
signed_field = 'gross_amount';
|
|
254
|
+
signed_amount = String(payment.gross_amount);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
signed_field = 'net_amount';
|
|
259
|
+
signed_amount = payment.net_amount != null ? String(payment.net_amount) : null;
|
|
260
|
+
}
|
|
261
|
+
if (!payee || !(0, base_payer_js_1.isEvmAddress)(payee)) {
|
|
262
|
+
return {
|
|
263
|
+
checked: false,
|
|
264
|
+
matches: false,
|
|
265
|
+
payload_version: version,
|
|
266
|
+
signed_field,
|
|
267
|
+
signed_amount,
|
|
268
|
+
transfer_amount: null,
|
|
269
|
+
payee,
|
|
270
|
+
reason: 'no_payee',
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (signed_amount == null) {
|
|
274
|
+
return {
|
|
275
|
+
checked: false,
|
|
276
|
+
matches: false,
|
|
277
|
+
payload_version: version,
|
|
278
|
+
signed_field,
|
|
279
|
+
signed_amount,
|
|
280
|
+
transfer_amount: null,
|
|
281
|
+
payee,
|
|
282
|
+
reason: 'no_signed_amount',
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const transfer = sumUsdcTransfersToPayee(logs, payee, options.usdcAddress);
|
|
286
|
+
if (transfer == null) {
|
|
287
|
+
return {
|
|
288
|
+
checked: true,
|
|
289
|
+
matches: false,
|
|
290
|
+
payload_version: version,
|
|
291
|
+
signed_field,
|
|
292
|
+
signed_amount,
|
|
293
|
+
transfer_amount: null,
|
|
294
|
+
payee,
|
|
295
|
+
reason: 'no_transfer_to_payee',
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const matches = BigInt(signed_amount) === transfer;
|
|
299
|
+
return {
|
|
300
|
+
checked: true,
|
|
301
|
+
matches,
|
|
302
|
+
payload_version: version,
|
|
303
|
+
signed_field,
|
|
304
|
+
signed_amount,
|
|
305
|
+
transfer_amount: transfer.toString(),
|
|
306
|
+
payee,
|
|
307
|
+
reason: matches
|
|
308
|
+
? undefined
|
|
309
|
+
: `amount_mismatch: signed ${signed_field} ${signed_amount} !== transfer ${transfer.toString()}`,
|
|
310
|
+
};
|
|
100
311
|
}
|
|
101
312
|
/**
|
|
102
313
|
* Verify a receipt's ES256 issuer signature against a JWK (public key).
|
|
@@ -146,59 +357,122 @@ function verifyIssuerSignature(receipt, jwk) {
|
|
|
146
357
|
* @param receipt - Receipt JSON with issuer_signature
|
|
147
358
|
* @param jwks - JWKS with keys array
|
|
148
359
|
*/
|
|
149
|
-
function
|
|
360
|
+
function jwksCandidates(jwks, kid) {
|
|
361
|
+
const keys = jwks?.keys || [];
|
|
362
|
+
const es256 = keys.filter((k) => (0, jws_js_1.isEs256PublicJwk)(k) && (k.alg == null || k.alg === 'ES256'));
|
|
363
|
+
if (!kid)
|
|
364
|
+
return es256;
|
|
365
|
+
return es256.filter((k) => k.kid === kid);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Verify an issuer signature against trusted keys only.
|
|
369
|
+
*
|
|
370
|
+
* Trust, in order:
|
|
371
|
+
* 1. A JWKS entry matched by `kid` (supplied file or fetched issuer JWKS).
|
|
372
|
+
* 2. An embedded `issuer_jwk` whose RFC 7638 thumbprint equals a pinned trusted kid.
|
|
373
|
+
*
|
|
374
|
+
* An embedded key that merely verifies the bytes is `key untrusted`.
|
|
375
|
+
*/
|
|
376
|
+
function verifyIssuerSignatureWithJwks(receipt, jwks, options = {}) {
|
|
150
377
|
const sig = receipt.issuer_signature;
|
|
151
378
|
if (!sig) {
|
|
152
|
-
return { checked: false, valid: false, reason: 'no_issuer_signature' };
|
|
153
|
-
}
|
|
154
|
-
const pinned = (0, jws_js_1.resolvePinnedIssuerJwk)(receipt);
|
|
155
|
-
if (pinned && sig.jws) {
|
|
156
|
-
const jwsResult = (0, jws_js_1.verifyIssuerJws)(sig.jws, pinned);
|
|
157
|
-
return {
|
|
158
|
-
checked: true,
|
|
159
|
-
valid: jwsResult.valid,
|
|
160
|
-
kid: pinned.kid || sig.kid,
|
|
161
|
-
reason: jwsResult.reason,
|
|
162
|
-
};
|
|
379
|
+
return { checked: false, valid: false, key_trusted: false, reason: 'no_issuer_signature' };
|
|
163
380
|
}
|
|
381
|
+
const trustedKids = options.trustedKids ?? jws_js_1.DEFAULT_TRUSTED_ISSUER_KIDS;
|
|
382
|
+
const header = sig.jws ? (0, jws_js_1.readJwsHeader)(sig.jws) : null;
|
|
383
|
+
const kid = header?.kid || sig.kid;
|
|
384
|
+
const embedded = (0, jws_js_1.resolvePinnedIssuerJwk)(receipt);
|
|
385
|
+
const pinned = !!(embedded && (0, jws_js_1.isPinnedTrustedJwk)(embedded, trustedKids));
|
|
164
386
|
if (sig.jws) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
const candidates = sig.kid
|
|
169
|
-
? jwks.keys.filter(k => k.kid === sig.kid && k.alg === 'ES256')
|
|
170
|
-
: jwks.keys.filter(k => k.alg === 'ES256');
|
|
171
|
-
if (candidates.length === 0) {
|
|
172
|
-
return { checked: false, valid: false, reason: 'no_matching_key' };
|
|
173
|
-
}
|
|
174
|
-
for (const jwk of candidates) {
|
|
387
|
+
const fromJwks = jwksCandidates(jwks, kid);
|
|
388
|
+
for (const jwk of fromJwks) {
|
|
175
389
|
const jwsResult = (0, jws_js_1.verifyIssuerJws)(sig.jws, jwk);
|
|
176
390
|
if (jwsResult.valid) {
|
|
177
|
-
return {
|
|
391
|
+
return {
|
|
392
|
+
checked: true,
|
|
393
|
+
valid: true,
|
|
394
|
+
key_trusted: true,
|
|
395
|
+
trust: 'jwks',
|
|
396
|
+
kid: jwk.kid || kid,
|
|
397
|
+
payload: jwsResult.payload,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (pinned && embedded) {
|
|
402
|
+
const pinResult = (0, jws_js_1.verifyIssuerJws)(sig.jws, embedded);
|
|
403
|
+
if (pinResult.valid) {
|
|
404
|
+
return {
|
|
405
|
+
checked: true,
|
|
406
|
+
valid: true,
|
|
407
|
+
key_trusted: true,
|
|
408
|
+
trust: 'pinned_kid',
|
|
409
|
+
kid: embedded.kid || (0, jws_js_1.jwkThumbprint)(embedded),
|
|
410
|
+
payload: pinResult.payload,
|
|
411
|
+
};
|
|
178
412
|
}
|
|
413
|
+
return {
|
|
414
|
+
checked: true,
|
|
415
|
+
valid: false,
|
|
416
|
+
key_trusted: true,
|
|
417
|
+
kid: kid || embedded.kid,
|
|
418
|
+
reason: pinResult.reason || 'signature_invalid',
|
|
419
|
+
};
|
|
179
420
|
}
|
|
180
|
-
|
|
421
|
+
if (embedded && (0, jws_js_1.verifyIssuerJws)(sig.jws, embedded).valid) {
|
|
422
|
+
return {
|
|
423
|
+
checked: true,
|
|
424
|
+
valid: false,
|
|
425
|
+
key_trusted: false,
|
|
426
|
+
kid: embedded.kid || kid,
|
|
427
|
+
reason: jws_js_1.KEY_UNTRUSTED,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (fromJwks.length === 0 && !pinned) {
|
|
431
|
+
return {
|
|
432
|
+
checked: true,
|
|
433
|
+
valid: false,
|
|
434
|
+
key_trusted: false,
|
|
435
|
+
kid,
|
|
436
|
+
reason: jws_js_1.KEY_UNTRUSTED,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
checked: true,
|
|
441
|
+
valid: false,
|
|
442
|
+
key_trusted: fromJwks.length > 0 || pinned,
|
|
443
|
+
kid,
|
|
444
|
+
reason: 'signature_invalid',
|
|
445
|
+
};
|
|
181
446
|
}
|
|
182
447
|
if (!sig.value) {
|
|
183
|
-
return { checked: false, valid: false, reason: 'no_issuer_signature' };
|
|
448
|
+
return { checked: false, valid: false, key_trusted: false, reason: 'no_issuer_signature' };
|
|
184
449
|
}
|
|
185
|
-
if (!jwks || !Array.isArray(jwks.keys)
|
|
186
|
-
return {
|
|
450
|
+
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
451
|
+
return {
|
|
452
|
+
checked: false,
|
|
453
|
+
valid: false,
|
|
454
|
+
key_trusted: false,
|
|
455
|
+
kid: sig.kid,
|
|
456
|
+
reason: 'JWKS not provided — pass jwks option to verify issuer signature',
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
if (jwks.keys.length === 0) {
|
|
460
|
+
return { checked: false, valid: false, key_trusted: false, kid: sig.kid, reason: 'empty_jwks' };
|
|
187
461
|
}
|
|
188
462
|
// Find matching key by kid, or try all ES256 keys if no kid on signature
|
|
189
463
|
const candidates = sig.kid
|
|
190
464
|
? jwks.keys.filter(k => k.kid === sig.kid && k.alg === 'ES256')
|
|
191
465
|
: jwks.keys.filter(k => k.alg === 'ES256');
|
|
192
466
|
if (candidates.length === 0) {
|
|
193
|
-
return { checked: false, valid: false, reason: 'no_matching_key' };
|
|
467
|
+
return { checked: false, valid: false, key_trusted: false, kid: sig.kid, reason: 'no_matching_key' };
|
|
194
468
|
}
|
|
195
469
|
for (const jwk of candidates) {
|
|
196
470
|
const result = verifyIssuerSignature(receipt, jwk);
|
|
197
471
|
if (result.valid) {
|
|
198
|
-
return result;
|
|
472
|
+
return { ...result, key_trusted: true, trust: 'jwks', kid: jwk.kid || sig.kid };
|
|
199
473
|
}
|
|
200
474
|
}
|
|
201
|
-
return { checked: true, valid: false, reason: 'signature_invalid' };
|
|
475
|
+
return { checked: true, valid: false, key_trusted: true, kid: sig.kid, reason: 'signature_invalid' };
|
|
202
476
|
}
|
|
203
477
|
/**
|
|
204
478
|
* Verify a receipt's payment binding locally (no network required).
|
|
@@ -206,8 +480,18 @@ function verifyIssuerSignatureWithJwks(receipt, jwks) {
|
|
|
206
480
|
* This recomputes the commitment from the receipt's fields and compares
|
|
207
481
|
* it to the stored `binding.expected_commitment`.
|
|
208
482
|
*/
|
|
483
|
+
function noCommitmentReason(rail) {
|
|
484
|
+
if (rail === 'unmetered' || rail === 'tfuel') {
|
|
485
|
+
return 'No binding present on receipt (unmetered or TFUEL rail)';
|
|
486
|
+
}
|
|
487
|
+
if (rail === 'usdc' || rail === 'reported') {
|
|
488
|
+
return 'No payment-binding commitment (expected_commitment is null)';
|
|
489
|
+
}
|
|
490
|
+
return 'No binding present on receipt (may be unmetered or TFUEL rail)';
|
|
491
|
+
}
|
|
209
492
|
function verifyBinding(receipt) {
|
|
210
493
|
const binding = receipt.binding;
|
|
494
|
+
const railHint = binding?.rail || receipt.payment?.rail || null;
|
|
211
495
|
if (!binding) {
|
|
212
496
|
return {
|
|
213
497
|
verified: false,
|
|
@@ -215,7 +499,17 @@ function verifyBinding(receipt) {
|
|
|
215
499
|
recomputed: null,
|
|
216
500
|
matches: false,
|
|
217
501
|
covers: [],
|
|
218
|
-
reason:
|
|
502
|
+
reason: noCommitmentReason(railHint),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
if (binding.expected_commitment == null || binding.expected_commitment === '') {
|
|
506
|
+
return {
|
|
507
|
+
verified: false,
|
|
508
|
+
expected: null,
|
|
509
|
+
recomputed: null,
|
|
510
|
+
matches: false,
|
|
511
|
+
covers: binding.covers || [],
|
|
512
|
+
reason: noCommitmentReason(railHint),
|
|
219
513
|
};
|
|
220
514
|
}
|
|
221
515
|
const paymentRef = receipt.payment?.ref ?? null;
|
|
@@ -329,88 +623,424 @@ function hashCanonicalPayload(receipt) {
|
|
|
329
623
|
return (0, ethers_1.keccak256)((0, ethers_1.toUtf8Bytes)((0, exports.canonicalSignedPayload)(receipt)));
|
|
330
624
|
}
|
|
331
625
|
/**
|
|
332
|
-
*
|
|
626
|
+
* claim_id-era receipts include the key even when the value is null.
|
|
627
|
+
* Older payloads omit the key and stay valid.
|
|
628
|
+
*/
|
|
629
|
+
function claimIdVerdict(claims) {
|
|
630
|
+
if (!claims || !Object.prototype.hasOwnProperty.call(claims, 'claim_id')) {
|
|
631
|
+
return 'not_present_legacy';
|
|
632
|
+
}
|
|
633
|
+
const payment = asRecord(claims.payment);
|
|
634
|
+
const ref = payment?.ref;
|
|
635
|
+
const hasRef = ref != null && String(ref) !== '';
|
|
636
|
+
const id = claims.claim_id;
|
|
637
|
+
const missing = id == null || id === '';
|
|
638
|
+
if (hasRef && missing)
|
|
639
|
+
return 'refused';
|
|
640
|
+
return 'ok';
|
|
641
|
+
}
|
|
642
|
+
function claimString(value) {
|
|
643
|
+
if (value == null || value === '')
|
|
644
|
+
return null;
|
|
645
|
+
return String(value);
|
|
646
|
+
}
|
|
647
|
+
function asRecord(value) {
|
|
648
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
649
|
+
return null;
|
|
650
|
+
return value;
|
|
651
|
+
}
|
|
652
|
+
function normFact(value, address = false) {
|
|
653
|
+
const text = claimString(value);
|
|
654
|
+
if (text == null)
|
|
655
|
+
return null;
|
|
656
|
+
return address ? text.toLowerCase() : text;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Fields a verifier may quote. Compared only when both the unsigned outer
|
|
660
|
+
* copy and the JWS payload carry a value.
|
|
661
|
+
*/
|
|
662
|
+
const CLAIM_COMPARE = [
|
|
663
|
+
{ field: 'task_id', outer: (r) => r.task_id, signed: (c) => c.task_id },
|
|
664
|
+
{ field: 'payment.rail', outer: (r) => r.payment?.rail, signed: (c) => asRecord(c.payment)?.rail },
|
|
665
|
+
{ field: 'payment.ref', outer: (r) => r.payment?.ref, signed: (c) => asRecord(c.payment)?.ref },
|
|
666
|
+
{ field: 'payment.gross_amount', outer: (r) => r.payment?.gross_amount, signed: (c) => asRecord(c.payment)?.gross_amount },
|
|
667
|
+
{ field: 'payment.settled_amount', outer: (r) => r.payment?.settled_amount, signed: (c) => asRecord(c.payment)?.settled_amount },
|
|
668
|
+
{ field: 'payment.net_amount', outer: (r) => r.payment?.net_amount, signed: (c) => asRecord(c.payment)?.net_amount },
|
|
669
|
+
{ field: 'payment.asset', outer: (r) => r.payment?.asset, signed: (c) => asRecord(c.payment)?.asset, address: true },
|
|
670
|
+
{ field: 'payment.payee', outer: (r) => r.payment?.payee, signed: (c) => asRecord(c.payment)?.payee, address: true },
|
|
671
|
+
{ field: 'caller_binding.payer_wallet', outer: (r) => r.caller_binding?.payer_wallet, signed: (c) => asRecord(c.caller_binding)?.payer_wallet, address: true },
|
|
672
|
+
{ field: 'caller_binding.agent_pubkey', outer: (r) => r.caller_binding?.agent_pubkey, signed: (c) => asRecord(c.caller_binding)?.agent_pubkey },
|
|
673
|
+
{ field: 'caller_binding.api_key_hash', outer: (r) => r.caller_binding?.api_key_hash, signed: (c) => asRecord(c.caller_binding)?.api_key_hash },
|
|
674
|
+
{ field: 'claim_id', outer: (r) => r.claim_id, signed: (c) => c.claim_id },
|
|
675
|
+
{ field: 'route.model', outer: (r) => r.route?.model, signed: (c) => asRecord(c.route)?.model },
|
|
676
|
+
{ field: 'route.provider', outer: (r) => r.route?.provider, signed: (c) => asRecord(c.route)?.provider },
|
|
677
|
+
{ field: 'output.hash', outer: (r) => r.output?.hash, signed: (c) => asRecord(c.output)?.hash },
|
|
678
|
+
{ field: 'binding.expected_commitment', outer: (r) => r.binding?.expected_commitment, signed: (c) => asRecord(c.binding)?.expected_commitment },
|
|
679
|
+
{ field: 'provider_cogs.actual', outer: (r) => r.provider_cogs?.actual, signed: (c) => asRecord(c.provider_cogs)?.actual },
|
|
680
|
+
];
|
|
681
|
+
/** Flag unsigned outer copies that disagree with the JWS payload. */
|
|
682
|
+
function diffOuterClaims(receipt, claims) {
|
|
683
|
+
if (!claims)
|
|
684
|
+
return [];
|
|
685
|
+
const mismatches = [];
|
|
686
|
+
for (const spec of CLAIM_COMPARE) {
|
|
687
|
+
const outer = normFact(spec.outer(receipt), spec.address);
|
|
688
|
+
const signed = normFact(spec.signed(claims), spec.address);
|
|
689
|
+
if (outer == null || signed == null || outer === signed)
|
|
690
|
+
continue;
|
|
691
|
+
mismatches.push({
|
|
692
|
+
field: spec.field,
|
|
693
|
+
outer: claimString(spec.outer(receipt)),
|
|
694
|
+
signed: claimString(spec.signed(claims)),
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
// v9 head pair. Compared when the outer key is present, including a null
|
|
698
|
+
// signed hash against a filled-in outer copy. v8 claims skip this.
|
|
699
|
+
if ((0, head_binding_js_1.headBindingVerdict)(claims) === 'ok') {
|
|
700
|
+
const disagree = (0, head_binding_js_1.outerHeadDisagrees)(receipt, claims);
|
|
701
|
+
if (disagree) {
|
|
702
|
+
const outerValue = disagree === 'tree_head_hash' ? receipt.tree_head_hash : receipt.tolerance;
|
|
703
|
+
const signedValue = disagree === 'tree_head_hash' ? claims.tree_head_hash : claims.tolerance;
|
|
704
|
+
mismatches.push({
|
|
705
|
+
field: disagree,
|
|
706
|
+
outer: outerValue == null ? null : JSON.stringify(outerValue),
|
|
707
|
+
signed: signedValue == null ? null : JSON.stringify(signedValue),
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return mismatches;
|
|
712
|
+
}
|
|
713
|
+
function receiptViewFromClaims(receipt, claims) {
|
|
714
|
+
const payment = asRecord(claims.payment);
|
|
715
|
+
const binding = asRecord(claims.binding);
|
|
716
|
+
const route = asRecord(claims.route);
|
|
717
|
+
const output = asRecord(claims.output);
|
|
718
|
+
const caller = asRecord(claims.caller_binding);
|
|
719
|
+
const modelCommitment = claimString(route?.model_commitment);
|
|
720
|
+
return {
|
|
721
|
+
...receipt,
|
|
722
|
+
task_id: claimString(claims.task_id) || receipt.task_id,
|
|
723
|
+
payment: {
|
|
724
|
+
rail: claimString(payment?.rail) ?? undefined,
|
|
725
|
+
ref: claimString(payment?.ref),
|
|
726
|
+
gross_amount: claimString(payment?.gross_amount),
|
|
727
|
+
settled_amount: claimString(payment?.settled_amount),
|
|
728
|
+
net_amount: claimString(payment?.net_amount),
|
|
729
|
+
asset: claimString(payment?.asset),
|
|
730
|
+
payee: claimString(payment?.payee),
|
|
731
|
+
fee_amount: claimString(payment?.fee_amount),
|
|
732
|
+
protocol_fee_bps: typeof payment?.protocol_fee_bps === 'number' ? payment.protocol_fee_bps : null,
|
|
733
|
+
platform_fee: claimString(payment?.platform_fee),
|
|
734
|
+
platform_fee_bps: typeof payment?.platform_fee_bps === 'number' ? payment.platform_fee_bps : null,
|
|
735
|
+
},
|
|
736
|
+
route: {
|
|
737
|
+
model: claimString(route?.model) ?? undefined,
|
|
738
|
+
provider: claimString(route?.provider) ?? undefined,
|
|
739
|
+
model_commitment: modelCommitment ? { commitment: modelCommitment } : null,
|
|
740
|
+
},
|
|
741
|
+
output: output?.hash ? { hash: claimString(output.hash) ?? undefined } : null,
|
|
742
|
+
caller_binding: caller
|
|
743
|
+
? {
|
|
744
|
+
payer_wallet: claimString(caller.payer_wallet),
|
|
745
|
+
agent_pubkey: claimString(caller.agent_pubkey),
|
|
746
|
+
api_key_hash: claimString(caller.api_key_hash),
|
|
747
|
+
}
|
|
748
|
+
: null,
|
|
749
|
+
provider_cogs: asRecord(claims.provider_cogs)?.actual != null
|
|
750
|
+
? { actual: claimString(asRecord(claims.provider_cogs)?.actual) ?? undefined }
|
|
751
|
+
: receipt.provider_cogs,
|
|
752
|
+
binding: {
|
|
753
|
+
expected_commitment: binding && 'expected_commitment' in binding
|
|
754
|
+
? (claimString(binding.expected_commitment) ?? undefined)
|
|
755
|
+
: undefined,
|
|
756
|
+
amount: claimString(binding?.amount)
|
|
757
|
+
|| claimString(payment?.net_amount)
|
|
758
|
+
|| claimString(payment?.gross_amount)
|
|
759
|
+
|| '0',
|
|
760
|
+
rail: claimString(binding?.rail) || claimString(payment?.rail) || undefined,
|
|
761
|
+
covers: Array.isArray(binding?.covers) ? binding.covers : ['payment', 'settlement'],
|
|
762
|
+
model_commitment: claimString(binding?.model_commitment) || modelCommitment,
|
|
763
|
+
output_hash: claimString(binding?.output_hash) || claimString(output?.hash),
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function payerClaimsFromPayload(payload) {
|
|
768
|
+
const payment = asRecord(payload.payment);
|
|
769
|
+
const caller = asRecord(payload.caller_binding);
|
|
770
|
+
return {
|
|
771
|
+
payment: {
|
|
772
|
+
ref: claimString(payment?.ref),
|
|
773
|
+
gross_amount: claimString(payment?.gross_amount),
|
|
774
|
+
payee: claimString(payment?.payee),
|
|
775
|
+
asset: claimString(payment?.asset),
|
|
776
|
+
rail: claimString(payment?.rail),
|
|
777
|
+
},
|
|
778
|
+
caller_binding: caller ? { payer_wallet: claimString(caller.payer_wallet) } : null,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function factsFromClaims(claims) {
|
|
782
|
+
if (!claims) {
|
|
783
|
+
return { hub: null, model: null, amount_usdc: null, tx: null, output_hash: null };
|
|
784
|
+
}
|
|
785
|
+
const payment = asRecord(claims.payment);
|
|
786
|
+
const route = asRecord(claims.route);
|
|
787
|
+
const output = asRecord(claims.output);
|
|
788
|
+
return {
|
|
789
|
+
hub: claimString(route?.provider),
|
|
790
|
+
model: claimString(route?.model),
|
|
791
|
+
amount_usdc: claimString(payment?.gross_amount),
|
|
792
|
+
tx: claimString(payment?.ref),
|
|
793
|
+
output_hash: claimString(output?.hash),
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function issuerJwksUri(receipt) {
|
|
797
|
+
const fromVerification = receipt.verification?.jwks_uri;
|
|
798
|
+
if (typeof fromVerification === 'string' && fromVerification.startsWith('https://'))
|
|
799
|
+
return fromVerification;
|
|
800
|
+
const jws = receipt.issuer_signature?.jws;
|
|
801
|
+
const header = jws ? (0, jws_js_1.readJwsHeader)(jws) : null;
|
|
802
|
+
if (header?.jku && header.jku.startsWith('https://'))
|
|
803
|
+
return header.jku;
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
function httpsUrl(uri) {
|
|
807
|
+
try {
|
|
808
|
+
return new URL(uri).protocol === 'https:';
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
function mergeJwks(primary, extra) {
|
|
815
|
+
const keys = [...(primary?.keys || []), ...(extra?.keys || [])];
|
|
816
|
+
if (keys.length === 0)
|
|
817
|
+
return undefined;
|
|
818
|
+
const seen = new Set();
|
|
819
|
+
const deduped = [];
|
|
820
|
+
for (const key of keys) {
|
|
821
|
+
const id = `${key.kid || ''}:${key.x || ''}:${key.y || ''}`;
|
|
822
|
+
if (seen.has(id))
|
|
823
|
+
continue;
|
|
824
|
+
seen.add(id);
|
|
825
|
+
deduped.push(key);
|
|
826
|
+
}
|
|
827
|
+
return { keys: deduped };
|
|
828
|
+
}
|
|
829
|
+
/** Fetch a JWKS document. Caller decides whether the URL is a trust root. */
|
|
830
|
+
async function fetchIssuerJwks(uri, fetchImpl = globalThis.fetch) {
|
|
831
|
+
if (!httpsUrl(uri))
|
|
832
|
+
throw new Error('jwks_uri must be https');
|
|
833
|
+
const res = await fetchImpl(uri);
|
|
834
|
+
if (!res.ok)
|
|
835
|
+
throw new Error(`jwks_fetch_failed: ${res.status}`);
|
|
836
|
+
const body = await res.json();
|
|
837
|
+
if (!body || !Array.isArray(body.keys))
|
|
838
|
+
throw new Error('jwks_malformed');
|
|
839
|
+
return body;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Full receipt verification — trusted issuer signature, signed-claim facts,
|
|
843
|
+
* optional on-chain payer and nullifier checks.
|
|
333
844
|
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* @param options.rpcUrl RPC URL for on-chain checks
|
|
845
|
+
* Facts (amount, payer, payee, asset, tx, model) are taken only from a JWS
|
|
846
|
+
* that verified under a trusted key. The embedded `issuer_jwk` is not trusted
|
|
847
|
+
* unless its RFC 7638 thumbprint is a pinned kid, or the same key is in a JWKS.
|
|
338
848
|
*/
|
|
339
|
-
/** Extract JWS claims for payer binding
|
|
849
|
+
/** Extract JWS claims for payer binding. Prefers the JWS payload over the outer copy. */
|
|
340
850
|
function receiptPayerClaims(receipt) {
|
|
341
851
|
return (0, payer_js_1.receiptPayerClaimsFromEnvelope)(receipt);
|
|
342
852
|
}
|
|
853
|
+
function normalizeBoundRoot(root) {
|
|
854
|
+
if (root == null || root === '')
|
|
855
|
+
return null;
|
|
856
|
+
const hex = String(root).replace(/^0x/i, '').toLowerCase();
|
|
857
|
+
return /^[0-9a-f]{64}$/.test(hex) ? hex : null;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* True when the supplied head is the signed prefix, or a later head whose
|
|
861
|
+
* inclusion proof contains this receipt's leaf.
|
|
862
|
+
*/
|
|
863
|
+
function suppliedHeadCovers(receipt, signedRoot, head, inclusion) {
|
|
864
|
+
const signed = normalizeBoundRoot(signedRoot);
|
|
865
|
+
const supplied = normalizeBoundRoot(head?.root);
|
|
866
|
+
if (!signed || !supplied)
|
|
867
|
+
return false;
|
|
868
|
+
if (signed === supplied)
|
|
869
|
+
return true;
|
|
870
|
+
if (!inclusion || !Array.isArray(inclusion.proof) || inclusion.leaf_index == null)
|
|
871
|
+
return true;
|
|
872
|
+
if (receipt.task_id == null)
|
|
873
|
+
return false;
|
|
874
|
+
const row = inclusion.row_hash ?? receipt.book_chain?.row_hash ?? '';
|
|
875
|
+
const leaf = (0, anchor_witness_js_1.leafHash)(Buffer.from(`${receipt.task_id}|${row}`));
|
|
876
|
+
if (typeof inclusion.leaf === 'string' && /^[0-9a-fA-F]{64}$/.test(inclusion.leaf)
|
|
877
|
+
&& inclusion.leaf.toLowerCase() !== leaf.toString('hex')) {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
const size = inclusion.tree_size ?? head.tree_size;
|
|
881
|
+
if (size == null || !Number.isInteger(Number(size)))
|
|
882
|
+
return false;
|
|
883
|
+
return (0, anchor_witness_js_1.verifyMerkleInclusion)(leaf, Number(inclusion.leaf_index), Number(size), supplied, inclusion.proof);
|
|
884
|
+
}
|
|
343
885
|
async function verifyReceipt(receipt, options = {}) {
|
|
344
886
|
const errors = [];
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
887
|
+
const trustedKids = options.trustedKids ?? jws_js_1.DEFAULT_TRUSTED_ISSUER_KIDS;
|
|
888
|
+
const trustedHosts = options.trustedJwksHosts ?? jws_js_1.DEFAULT_TRUSTED_JWKS_HOSTS;
|
|
889
|
+
let jwks = options.jwks;
|
|
890
|
+
if (options.jwksUri || options.fetchJwks) {
|
|
891
|
+
const uri = options.jwksUri || issuerJwksUri(receipt);
|
|
892
|
+
const allowed = !!uri && (options.jwksUri ? httpsUrl(uri) : (0, jws_js_1.jwksHostAllowed)(uri, trustedHosts));
|
|
893
|
+
if (uri && allowed) {
|
|
894
|
+
try {
|
|
895
|
+
const fetched = await fetchIssuerJwks(uri, options.fetchImpl);
|
|
896
|
+
jwks = mergeJwks(jwks, fetched);
|
|
897
|
+
}
|
|
898
|
+
catch (err) {
|
|
899
|
+
errors.push(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
else if (options.jwksUri) {
|
|
903
|
+
errors.push('JWKS URI rejected — only https URLs are fetched');
|
|
904
|
+
}
|
|
905
|
+
else if (options.fetchJwks && uri) {
|
|
906
|
+
errors.push(`JWKS host untrusted: ${uri}`);
|
|
363
907
|
}
|
|
364
908
|
}
|
|
365
|
-
|
|
366
|
-
|
|
909
|
+
let issuer_signature;
|
|
910
|
+
if (receipt.issuer_signature?.jws || receipt.issuer_signature?.value) {
|
|
911
|
+
issuer_signature = verifyIssuerSignatureWithJwks(receipt, jwks, { trustedKids });
|
|
367
912
|
if (issuer_signature.checked && !issuer_signature.valid) {
|
|
368
|
-
errors.push(
|
|
913
|
+
errors.push(issuer_signature.reason === jws_js_1.KEY_UNTRUSTED
|
|
914
|
+
? jws_js_1.KEY_UNTRUSTED
|
|
915
|
+
: `Issuer signature invalid: ${issuer_signature.reason}`);
|
|
369
916
|
}
|
|
370
917
|
}
|
|
371
918
|
else if (receipt.issuer_signature) {
|
|
372
919
|
issuer_signature = {
|
|
373
920
|
checked: false,
|
|
374
921
|
valid: false,
|
|
922
|
+
key_trusted: false,
|
|
375
923
|
kid: receipt.issuer_signature.kid,
|
|
376
|
-
reason:
|
|
377
|
-
? 'No pinned issuer_jwk and JWKS not provided — pass jwks option or use a receipt with issuer_signature.issuer_jwk'
|
|
378
|
-
: 'JWKS not provided — pass jwks option to verify issuer signature',
|
|
924
|
+
reason: 'JWKS not provided — pass jwks option to verify issuer signature',
|
|
379
925
|
};
|
|
380
926
|
}
|
|
381
927
|
else {
|
|
382
928
|
issuer_signature = {
|
|
383
929
|
checked: false,
|
|
384
930
|
valid: false,
|
|
931
|
+
key_trusted: false,
|
|
385
932
|
reason: 'No issuer signature present on receipt',
|
|
386
933
|
};
|
|
387
934
|
}
|
|
388
|
-
|
|
935
|
+
const decoded = receipt.issuer_signature?.jws
|
|
936
|
+
? (0, payer_js_1.decodeJwsPayload)(receipt.issuer_signature.jws)
|
|
937
|
+
: null;
|
|
938
|
+
const claim_mismatches = diffOuterClaims(receipt, decoded);
|
|
939
|
+
for (const mismatch of claim_mismatches) {
|
|
940
|
+
errors.push(`outer/signed mismatch: ${mismatch.field} (outer ${mismatch.outer}, signed ${mismatch.signed})`);
|
|
941
|
+
}
|
|
942
|
+
const verifiedClaims = issuer_signature.valid ? issuer_signature.payload : undefined;
|
|
943
|
+
const claim_id = claimIdVerdict(issuer_signature.valid ? verifiedClaims : decoded);
|
|
944
|
+
if (issuer_signature.valid && claim_id === 'refused') {
|
|
945
|
+
errors.push('payment.ref is set and claim_id is null');
|
|
946
|
+
}
|
|
947
|
+
const headVerdict = (0, head_binding_js_1.headBindingVerdict)(issuer_signature.valid ? verifiedClaims : null);
|
|
948
|
+
const headMissing = headVerdict === 'missing';
|
|
949
|
+
if (headMissing) {
|
|
950
|
+
errors.push('payload v9 requires tree_head_hash and tolerance inside signed claims');
|
|
951
|
+
}
|
|
952
|
+
const signedBinding = headVerdict === 'ok' ? (0, head_binding_js_1.signedHeadBinding)(verifiedClaims) : null;
|
|
953
|
+
// A published head may differ from the signed prefix. That is not a failure.
|
|
954
|
+
// An inclusion proof, when one is supplied, must be for this receipt's leaf
|
|
955
|
+
// (`task_id|row_hash`), not an arbitrary hash already in the tree.
|
|
956
|
+
let headMismatch = false;
|
|
957
|
+
if (signedBinding?.tree_head_hash && options.head?.root && options.inclusion) {
|
|
958
|
+
if (!suppliedHeadCovers(receipt, signedBinding.tree_head_hash, options.head, options.inclusion)) {
|
|
959
|
+
headMismatch = true;
|
|
960
|
+
errors.push('tree_head_mismatch');
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
let binding;
|
|
964
|
+
if (receipt.issuer_signature?.jws) {
|
|
965
|
+
if (verifiedClaims) {
|
|
966
|
+
binding = verifyBinding(receiptViewFromClaims(receipt, verifiedClaims));
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
binding = {
|
|
970
|
+
verified: false,
|
|
971
|
+
expected: null,
|
|
972
|
+
recomputed: null,
|
|
973
|
+
matches: false,
|
|
974
|
+
covers: [],
|
|
975
|
+
reason: issuer_signature.reason === jws_js_1.KEY_UNTRUSTED
|
|
976
|
+
? 'Binding not checked — issuer key untrusted'
|
|
977
|
+
: 'Binding not checked — issuer signature not verified',
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
binding = verifyBinding(receipt);
|
|
983
|
+
}
|
|
984
|
+
if (!binding.matches && binding.expected) {
|
|
985
|
+
errors.push('Payment binding mismatch');
|
|
986
|
+
}
|
|
987
|
+
const signedPayerClaims = verifiedClaims ? payerClaimsFromPayload(verifiedClaims) : null;
|
|
389
988
|
let payer;
|
|
390
989
|
if (options.checkPayer) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
990
|
+
if (!signedPayerClaims) {
|
|
991
|
+
payer = {
|
|
992
|
+
checked: false,
|
|
993
|
+
valid: false,
|
|
994
|
+
reason: 'Payer facts come from verified signed claims — issuer signature is not trusted',
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
const payerResult = await (0, payer_js_1.verifyPayerBinding)(signedPayerClaims, {
|
|
999
|
+
rpcUrl: options.rpcUrl,
|
|
1000
|
+
solanaRpcUrl: options.solanaRpcUrl,
|
|
1001
|
+
fetchSolanaTransaction: options.fetchSolanaTransaction,
|
|
1002
|
+
fetchBaseReceipt: options.fetchBaseReceipt,
|
|
1003
|
+
});
|
|
1004
|
+
payer = {
|
|
1005
|
+
checked: payerResult.checked,
|
|
1006
|
+
valid: payerResult.valid,
|
|
1007
|
+
rail: payerResult.rail,
|
|
1008
|
+
reason: payerResult.reason,
|
|
1009
|
+
payer_wallet: payerResult.payerWallet ?? signedPayerClaims.caller_binding?.payer_wallet ?? null,
|
|
1010
|
+
payee: signedPayerClaims.payment?.payee ?? null,
|
|
1011
|
+
asset: signedPayerClaims.payment?.asset ?? null,
|
|
1012
|
+
amount: payerResult.expectedAmount ?? signedPayerClaims.payment?.gross_amount ?? null,
|
|
1013
|
+
};
|
|
1014
|
+
if (payerResult.checked && !payerResult.valid) {
|
|
1015
|
+
errors.push(`Payer binding mismatch: ${payerResult.reason}`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
else if (signedPayerClaims?.caller_binding?.payer_wallet && signedPayerClaims.payment?.ref) {
|
|
397
1020
|
payer = {
|
|
398
|
-
checked:
|
|
399
|
-
valid:
|
|
400
|
-
|
|
401
|
-
|
|
1021
|
+
checked: false,
|
|
1022
|
+
valid: false,
|
|
1023
|
+
payer_wallet: signedPayerClaims.caller_binding.payer_wallet,
|
|
1024
|
+
payee: signedPayerClaims.payment.payee ?? null,
|
|
1025
|
+
asset: signedPayerClaims.payment.asset ?? null,
|
|
1026
|
+
amount: signedPayerClaims.payment.gross_amount ?? null,
|
|
1027
|
+
reason: 'On-chain payer check not requested — pass checkPayer: true',
|
|
402
1028
|
};
|
|
403
|
-
if (payerResult.checked && !payerResult.valid) {
|
|
404
|
-
errors.push(`Payer binding mismatch: ${payerResult.reason}`);
|
|
405
|
-
}
|
|
406
1029
|
}
|
|
407
|
-
else if (receipt.caller_binding?.payer_wallet && receipt.payment?.ref) {
|
|
1030
|
+
else if (!receipt.issuer_signature?.jws && receipt.caller_binding?.payer_wallet && receipt.payment?.ref) {
|
|
408
1031
|
payer = {
|
|
409
1032
|
checked: false,
|
|
410
1033
|
valid: false,
|
|
411
1034
|
reason: 'On-chain payer check not requested — pass checkPayer: true',
|
|
412
1035
|
};
|
|
413
1036
|
}
|
|
1037
|
+
else if (receipt.issuer_signature?.jws && !issuer_signature.valid) {
|
|
1038
|
+
payer = {
|
|
1039
|
+
checked: false,
|
|
1040
|
+
valid: false,
|
|
1041
|
+
reason: 'Payer facts come from verified signed claims — issuer signature is not trusted',
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
414
1044
|
else {
|
|
415
1045
|
payer = {
|
|
416
1046
|
checked: false,
|
|
@@ -418,7 +1048,6 @@ async function verifyReceipt(receipt, options = {}) {
|
|
|
418
1048
|
reason: 'No payer_wallet or payment.ref to verify',
|
|
419
1049
|
};
|
|
420
1050
|
}
|
|
421
|
-
// Verify nullifier on-chain if requested
|
|
422
1051
|
let nullifier;
|
|
423
1052
|
if (options.checkNullifier && receipt.proof?.nullifier) {
|
|
424
1053
|
nullifier = await verifyNullifier(receipt, {
|
|
@@ -439,64 +1068,166 @@ async function verifyReceipt(receipt, options = {}) {
|
|
|
439
1068
|
: 'On-chain check not requested',
|
|
440
1069
|
};
|
|
441
1070
|
}
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const
|
|
1071
|
+
const facts = factsFromClaims(verifiedClaims);
|
|
1072
|
+
const warnings = [];
|
|
1073
|
+
const preimages = await (0, preimage_js_1.verifyPublishedPreimages)(receipt, {
|
|
1074
|
+
requirePreimages: options.requirePreimages === true,
|
|
1075
|
+
fetchImpl: options.fetchImpl,
|
|
1076
|
+
trustedHosts,
|
|
1077
|
+
});
|
|
1078
|
+
if (!preimages.ok)
|
|
1079
|
+
errors.push(...preimages.errors);
|
|
1080
|
+
const signedPayloadHash = verifiedClaims && 'payload_hash' in verifiedClaims
|
|
1081
|
+
? verifiedClaims.payload_hash
|
|
1082
|
+
: null;
|
|
1083
|
+
const storedPreimage = typeof receipt.issuer_signature?.canonical_preimage === 'string'
|
|
1084
|
+
? receipt.issuer_signature.canonical_preimage
|
|
1085
|
+
: null;
|
|
1086
|
+
let canonicalPreimageFailed = false;
|
|
1087
|
+
if (storedPreimage != null) {
|
|
1088
|
+
const storedCheck = (0, canonical_preimage_js_1.verifyCanonicalPreimageBytes)(storedPreimage, signedPayloadHash);
|
|
1089
|
+
if (!storedCheck.ok) {
|
|
1090
|
+
canonicalPreimageFailed = true;
|
|
1091
|
+
if (storedCheck.reason)
|
|
1092
|
+
errors.push(`canonical preimage: ${storedCheck.reason}`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (typeof options.canonicalPreimage === 'string') {
|
|
1096
|
+
const fileCheck = (0, canonical_preimage_js_1.verifyCanonicalPreimageBytes)(options.canonicalPreimage, signedPayloadHash);
|
|
1097
|
+
if (!fileCheck.ok) {
|
|
1098
|
+
canonicalPreimageFailed = true;
|
|
1099
|
+
if (fileCheck.reason)
|
|
1100
|
+
errors.push(`canonical preimage: ${fileCheck.reason}`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const issuedAt = (verifiedClaims && 'iat' in verifiedClaims ? verifiedClaims.iat : null)
|
|
1104
|
+
?? decoded?.iat
|
|
1105
|
+
?? receipt.created_at
|
|
1106
|
+
?? null;
|
|
1107
|
+
const historyPin = (0, issuer_history_js_1.readIssuerHistoryPin)(verifiedClaims);
|
|
1108
|
+
const payloadVersion = Number(verifiedClaims?.payload_version);
|
|
1109
|
+
// Payload v10 signs the history pin. A missing pin fails even when the
|
|
1110
|
+
// caller did not pass a history file or ask for a fetch.
|
|
1111
|
+
const pinRequired = !options.skipIssuerHistory
|
|
1112
|
+
&& Number.isFinite(payloadVersion)
|
|
1113
|
+
&& payloadVersion >= canonical_preimage_js_1.CANONICAL_PAYLOAD_VERSION;
|
|
1114
|
+
const historyAsked = pinRequired || (!options.skipIssuerHistory && !!(options.issuerHistory
|
|
1115
|
+
|| options.issuerHistoryBytes
|
|
1116
|
+
|| options.fetchIssuerHistory
|
|
1117
|
+
|| options.strictIssuerHistory
|
|
1118
|
+
|| options.issuerHistoryUrl
|
|
1119
|
+
|| historyPin));
|
|
1120
|
+
const issuer_history = historyAsked
|
|
1121
|
+
? await (0, issuer_history_js_1.checkReceiptIssuerHistory)(receipt, {
|
|
1122
|
+
document: options.issuerHistory ?? null,
|
|
1123
|
+
documentBytes: options.issuerHistoryBytes ?? null,
|
|
1124
|
+
fetchHistory: options.fetchIssuerHistory === true || options.strictIssuerHistory === true,
|
|
1125
|
+
strict: options.strictIssuerHistory === true,
|
|
1126
|
+
historyUrl: options.issuerHistoryUrl ?? null,
|
|
1127
|
+
jwks,
|
|
1128
|
+
trustedKids,
|
|
1129
|
+
fetchImpl: options.fetchImpl,
|
|
1130
|
+
trustedHosts,
|
|
1131
|
+
issuedAt,
|
|
1132
|
+
kid: issuer_signature.kid ?? receipt.issuer_signature?.kid ?? null,
|
|
1133
|
+
pin: historyPin,
|
|
1134
|
+
requirePin: pinRequired,
|
|
1135
|
+
})
|
|
1136
|
+
: {
|
|
1137
|
+
checked: false,
|
|
1138
|
+
ok: true,
|
|
1139
|
+
unreachable: false,
|
|
1140
|
+
warning: null,
|
|
1141
|
+
reason: null,
|
|
1142
|
+
kid: issuer_signature.kid ?? receipt.issuer_signature?.kid ?? null,
|
|
1143
|
+
};
|
|
1144
|
+
if (issuer_history.warning)
|
|
1145
|
+
warnings.push(issuer_history.warning);
|
|
1146
|
+
if (issuer_history.checked && !issuer_history.ok && issuer_history.reason) {
|
|
1147
|
+
errors.push(`issuer history: ${issuer_history.reason}`);
|
|
1148
|
+
}
|
|
455
1149
|
const hasIssuerSig = !!receipt.issuer_signature;
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
1150
|
+
const jwksWasSupplied = !!(options.jwks || options.jwksUri || (options.fetchJwks && jwks));
|
|
1151
|
+
const claimRefused = issuer_signature.valid && claim_id === 'refused';
|
|
1152
|
+
const signatureFailed = hasIssuerSig && !issuer_signature.valid && (!!issuer_signature.checked
|
|
1153
|
+
|| jwksWasSupplied
|
|
1154
|
+
|| issuer_signature.reason === jws_js_1.KEY_UNTRUSTED
|
|
1155
|
+
|| issuer_signature.reason === 'no_matching_key'
|
|
1156
|
+
|| issuer_signature.reason === 'empty_jwks');
|
|
1157
|
+
const mismatchFailed = claim_mismatches.length > 0;
|
|
1158
|
+
const bindingFailed = !!(binding.expected && !binding.matches);
|
|
1159
|
+
const payerFailed = !!(options.checkPayer && payer.checked && !payer.valid);
|
|
1160
|
+
const nullifierFailed = !!(options.checkNullifier
|
|
1161
|
+
&& receipt.proof?.nullifier
|
|
1162
|
+
&& nullifier.anchored === false);
|
|
1163
|
+
const signatureUnchecked = hasIssuerSig && !issuer_signature.checked;
|
|
1164
|
+
let overall;
|
|
1165
|
+
const preimageFailed = !preimages.ok;
|
|
1166
|
+
const historyFailed = issuer_history.checked && !issuer_history.ok;
|
|
1167
|
+
if (signatureFailed || mismatchFailed || bindingFailed || payerFailed || nullifierFailed || claimRefused || headMissing || headMismatch || preimageFailed || historyFailed || canonicalPreimageFailed) {
|
|
463
1168
|
overall = 'failed';
|
|
464
1169
|
}
|
|
465
|
-
else if (
|
|
466
|
-
// Receipt has signature but no offline key material → partial
|
|
1170
|
+
else if (signatureUnchecked) {
|
|
467
1171
|
overall = 'partial';
|
|
468
1172
|
}
|
|
469
|
-
else if (binding.
|
|
1173
|
+
else if (!binding.expected && !hasIssuerSig && !options.checkPayer) {
|
|
1174
|
+
overall = 'partial';
|
|
1175
|
+
}
|
|
1176
|
+
else if ((binding.matches || !binding.expected)
|
|
470
1177
|
&& (!options.checkPayer || payer.valid)
|
|
471
|
-
&& (!options.checkNullifier || nullifier.anchored)
|
|
472
|
-
|
|
1178
|
+
&& (!options.checkNullifier || !receipt.proof?.nullifier || nullifier.anchored === true)
|
|
1179
|
+
&& (issuer_signature.valid || !hasIssuerSig)) {
|
|
473
1180
|
overall = 'verified';
|
|
474
1181
|
}
|
|
475
|
-
else
|
|
476
|
-
|| issuer_signature.valid
|
|
477
|
-
|| (options.checkPayer && payer.valid)
|
|
478
|
-
|| (nullifier.verified && nullifier.anchored)) {
|
|
479
|
-
// At least one check passed → partial
|
|
480
|
-
overall = 'partial';
|
|
481
|
-
}
|
|
482
|
-
else if (!binding.expected && !receipt.proof?.nullifier && !hasIssuerSig) {
|
|
483
|
-
// No binding/sig/nullifier to verify (unmetered/demo) → partial
|
|
1182
|
+
else {
|
|
484
1183
|
overall = 'partial';
|
|
485
1184
|
}
|
|
486
|
-
|
|
1185
|
+
// A refusal is a different document. Recognition uses the signed JWS
|
|
1186
|
+
// schema, not only the unsigned outer schema. A valid issuer signature
|
|
1187
|
+
// here must not be reported as a verified payment.
|
|
1188
|
+
const verifiedSchema = typeof verifiedClaims?.schema === 'string' ? verifiedClaims.schema : null;
|
|
1189
|
+
if (verifiedSchema === refusal_js_1.REFUSAL_SCHEMA || (0, refusal_js_1.isRefusalDocument)(receipt)) {
|
|
1190
|
+
errors.push('refusal document is not a payment receipt');
|
|
487
1191
|
overall = 'failed';
|
|
488
1192
|
}
|
|
1193
|
+
const signedPayment = asRecord(verifiedClaims?.payment);
|
|
1194
|
+
const signedSettlement = asRecord(verifiedClaims?.settlement);
|
|
1195
|
+
const receipt_lane = (0, receipt_lane_js_1.receiptLaneFromVerification)({
|
|
1196
|
+
receipt,
|
|
1197
|
+
claims: {
|
|
1198
|
+
payment: signedPayment ? {
|
|
1199
|
+
ref: typeof signedPayment.ref === 'string' ? signedPayment.ref : null,
|
|
1200
|
+
rail: typeof signedPayment.rail === 'string' ? signedPayment.rail : null,
|
|
1201
|
+
collected: typeof signedPayment.collected === 'boolean' ? signedPayment.collected : null,
|
|
1202
|
+
} : null,
|
|
1203
|
+
settlement: signedSettlement ? {
|
|
1204
|
+
kind: typeof signedSettlement.kind === 'string' ? signedSettlement.kind : null,
|
|
1205
|
+
} : null,
|
|
1206
|
+
},
|
|
1207
|
+
issuerValid: issuer_signature.valid === true,
|
|
1208
|
+
payer: { checked: payer.checked, valid: payer.valid },
|
|
1209
|
+
head: options.head ?? null,
|
|
1210
|
+
});
|
|
489
1211
|
return {
|
|
490
1212
|
receipt_id: receipt.task_id,
|
|
491
1213
|
binding,
|
|
492
1214
|
issuer_signature,
|
|
493
1215
|
payer,
|
|
494
1216
|
nullifier,
|
|
495
|
-
output_hash,
|
|
496
|
-
hub,
|
|
497
|
-
model,
|
|
498
|
-
amount_usdc,
|
|
499
|
-
tx,
|
|
1217
|
+
output_hash: facts.output_hash,
|
|
1218
|
+
hub: facts.hub,
|
|
1219
|
+
model: facts.model,
|
|
1220
|
+
amount_usdc: facts.amount_usdc,
|
|
1221
|
+
tx: facts.tx,
|
|
1222
|
+
claim_mismatches,
|
|
1223
|
+
claim_id,
|
|
1224
|
+
head_binding: signedBinding
|
|
1225
|
+
? { tree_head_hash: signedBinding.tree_head_hash, tolerance: signedBinding.tolerance }
|
|
1226
|
+
: null,
|
|
1227
|
+
receipt_lane,
|
|
1228
|
+
preimages,
|
|
1229
|
+
issuer_history,
|
|
1230
|
+
warnings,
|
|
500
1231
|
overall,
|
|
501
1232
|
errors,
|
|
502
1233
|
};
|
|
@@ -504,6 +1235,51 @@ async function verifyReceipt(receipt, options = {}) {
|
|
|
504
1235
|
var jws_js_2 = require("./jws.js");
|
|
505
1236
|
Object.defineProperty(exports, "resolvePinnedIssuerJwk", { enumerable: true, get: function () { return jws_js_2.resolvePinnedIssuerJwk; } });
|
|
506
1237
|
Object.defineProperty(exports, "verifyIssuerJws", { enumerable: true, get: function () { return jws_js_2.verifyIssuerJws; } });
|
|
1238
|
+
Object.defineProperty(exports, "DEFAULT_TRUSTED_ISSUER_KIDS", { enumerable: true, get: function () { return jws_js_2.DEFAULT_TRUSTED_ISSUER_KIDS; } });
|
|
1239
|
+
Object.defineProperty(exports, "DEFAULT_TRUSTED_JWKS_HOSTS", { enumerable: true, get: function () { return jws_js_2.DEFAULT_TRUSTED_JWKS_HOSTS; } });
|
|
1240
|
+
Object.defineProperty(exports, "KEY_UNTRUSTED", { enumerable: true, get: function () { return jws_js_2.KEY_UNTRUSTED; } });
|
|
1241
|
+
Object.defineProperty(exports, "jwkThumbprint", { enumerable: true, get: function () { return jws_js_2.jwkThumbprint; } });
|
|
1242
|
+
Object.defineProperty(exports, "isPinnedTrustedJwk", { enumerable: true, get: function () { return jws_js_2.isPinnedTrustedJwk; } });
|
|
1243
|
+
Object.defineProperty(exports, "jwksHostAllowed", { enumerable: true, get: function () { return jws_js_2.jwksHostAllowed; } });
|
|
1244
|
+
var receipt_lane_js_2 = require("./receipt-lane.js");
|
|
1245
|
+
Object.defineProperty(exports, "buildReceiptLane", { enumerable: true, get: function () { return receipt_lane_js_2.buildReceiptLane; } });
|
|
1246
|
+
Object.defineProperty(exports, "receiptLaneFromVerification", { enumerable: true, get: function () { return receipt_lane_js_2.receiptLaneFromVerification; } });
|
|
1247
|
+
Object.defineProperty(exports, "receiptLaneDecision", { enumerable: true, get: function () { return receipt_lane_js_2.receiptLaneDecision; } });
|
|
1248
|
+
Object.defineProperty(exports, "deriveSettledBy", { enumerable: true, get: function () { return receipt_lane_js_2.deriveSettledBy; } });
|
|
1249
|
+
Object.defineProperty(exports, "deriveSettled", { enumerable: true, get: function () { return receipt_lane_js_2.deriveSettled; } });
|
|
1250
|
+
Object.defineProperty(exports, "anchorChangedSinceBinding", { enumerable: true, get: function () { return receipt_lane_js_2.anchorChangedSinceBinding; } });
|
|
1251
|
+
Object.defineProperty(exports, "RECEIPT_LANE_SCHEMA", { enumerable: true, get: function () { return receipt_lane_js_2.RECEIPT_LANE_SCHEMA; } });
|
|
1252
|
+
Object.defineProperty(exports, "RECEIPT_LANE_RULE", { enumerable: true, get: function () { return receipt_lane_js_2.RECEIPT_LANE_RULE; } });
|
|
1253
|
+
Object.defineProperty(exports, "RECEIPT_LANE_ORDERING", { enumerable: true, get: function () { return receipt_lane_js_2.RECEIPT_LANE_ORDERING; } });
|
|
1254
|
+
Object.defineProperty(exports, "RECEIPT_LANE_BOUNDARY", { enumerable: true, get: function () { return receipt_lane_js_2.RECEIPT_LANE_BOUNDARY; } });
|
|
1255
|
+
var head_binding_js_2 = require("./head-binding.js");
|
|
1256
|
+
Object.defineProperty(exports, "headBindingVerdict", { enumerable: true, get: function () { return head_binding_js_2.headBindingVerdict; } });
|
|
1257
|
+
Object.defineProperty(exports, "signedHeadBinding", { enumerable: true, get: function () { return head_binding_js_2.signedHeadBinding; } });
|
|
1258
|
+
Object.defineProperty(exports, "outerHeadDisagrees", { enumerable: true, get: function () { return head_binding_js_2.outerHeadDisagrees; } });
|
|
1259
|
+
Object.defineProperty(exports, "HEAD_BINDING_PAYLOAD_VERSION", { enumerable: true, get: function () { return head_binding_js_2.HEAD_BINDING_PAYLOAD_VERSION; } });
|
|
1260
|
+
var refusal_js_2 = require("./refusal.js");
|
|
1261
|
+
Object.defineProperty(exports, "isRefusalDocument", { enumerable: true, get: function () { return refusal_js_2.isRefusalDocument; } });
|
|
1262
|
+
Object.defineProperty(exports, "verifyRefusal", { enumerable: true, get: function () { return refusal_js_2.verifyRefusal; } });
|
|
1263
|
+
Object.defineProperty(exports, "REFUSAL_SCHEMA", { enumerable: true, get: function () { return refusal_js_2.REFUSAL_SCHEMA; } });
|
|
1264
|
+
Object.defineProperty(exports, "REFUSAL_PAYLOAD_VERSION", { enumerable: true, get: function () { return refusal_js_2.REFUSAL_PAYLOAD_VERSION; } });
|
|
1265
|
+
Object.defineProperty(exports, "REFUSAL_PROVES", { enumerable: true, get: function () { return refusal_js_2.REFUSAL_PROVES; } });
|
|
1266
|
+
Object.defineProperty(exports, "REFUSAL_DOES_NOT_PROVE", { enumerable: true, get: function () { return refusal_js_2.REFUSAL_DOES_NOT_PROVE; } });
|
|
1267
|
+
var anchor_witness_js_2 = require("./anchor-witness.js");
|
|
1268
|
+
Object.defineProperty(exports, "verifyAnchoredRoot", { enumerable: true, get: function () { return anchor_witness_js_2.verifyAnchoredRoot; } });
|
|
1269
|
+
Object.defineProperty(exports, "verifyMerkleInclusion", { enumerable: true, get: function () { return anchor_witness_js_2.verifyMerkleInclusion; } });
|
|
1270
|
+
Object.defineProperty(exports, "extractMemos", { enumerable: true, get: function () { return anchor_witness_js_2.extractMemos; } });
|
|
1271
|
+
Object.defineProperty(exports, "parseAnchorMemo", { enumerable: true, get: function () { return anchor_witness_js_2.parseAnchorMemo; } });
|
|
1272
|
+
Object.defineProperty(exports, "leafHash", { enumerable: true, get: function () { return anchor_witness_js_2.leafHash; } });
|
|
1273
|
+
Object.defineProperty(exports, "ANCHOR_PROVES", { enumerable: true, get: function () { return anchor_witness_js_2.ANCHOR_PROVES; } });
|
|
1274
|
+
Object.defineProperty(exports, "ANCHOR_DOES_NOT_PROVE", { enumerable: true, get: function () { return anchor_witness_js_2.ANCHOR_DOES_NOT_PROVE; } });
|
|
1275
|
+
Object.defineProperty(exports, "SOLANA_GENESIS", { enumerable: true, get: function () { return anchor_witness_js_2.SOLANA_GENESIS; } });
|
|
1276
|
+
Object.defineProperty(exports, "BASE_MAINNET_CHAIN_ID", { enumerable: true, get: function () { return anchor_witness_js_2.BASE_MAINNET_CHAIN_ID; } });
|
|
1277
|
+
var canonical_preimage_js_2 = require("./canonical-preimage.js");
|
|
1278
|
+
Object.defineProperty(exports, "verifyCanonicalPreimageBytes", { enumerable: true, get: function () { return canonical_preimage_js_2.verifyCanonicalPreimageBytes; } });
|
|
1279
|
+
Object.defineProperty(exports, "CANONICAL_PAYLOAD_VERSION", { enumerable: true, get: function () { return canonical_preimage_js_2.CANONICAL_PAYLOAD_VERSION; } });
|
|
1280
|
+
var issuer_history_js_2 = require("./issuer-history.js");
|
|
1281
|
+
Object.defineProperty(exports, "readIssuerHistoryPin", { enumerable: true, get: function () { return issuer_history_js_2.readIssuerHistoryPin; } });
|
|
1282
|
+
Object.defineProperty(exports, "issuerHistoryDocumentHash", { enumerable: true, get: function () { return issuer_history_js_2.issuerHistoryDocumentHash; } });
|
|
507
1283
|
exports.default = {
|
|
508
1284
|
verifyBinding,
|
|
509
1285
|
verifyIssuerSignature,
|
|
@@ -521,7 +1297,10 @@ exports.default = {
|
|
|
521
1297
|
computePaymentCommitment: binding_js_1.computePaymentCommitment,
|
|
522
1298
|
computeInferenceBinding: binding_js_1.computeInferenceBinding,
|
|
523
1299
|
canonicalIssuerPayload,
|
|
1300
|
+
canonicalPayloadVersion,
|
|
524
1301
|
canonicalSignedPayload: exports.canonicalSignedPayload,
|
|
1302
|
+
reconcileSettledTransfer,
|
|
1303
|
+
sumUsdcTransfersToPayee,
|
|
525
1304
|
hashCanonicalPayload,
|
|
526
1305
|
ZK_VERIFIER_ADDRESS: exports.ZK_VERIFIER_ADDRESS,
|
|
527
1306
|
BASE_RPC_URL: base_payer_js_1.BASE_RPC_URL,
|