@xfuel/verify 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -8
- package/dist/base-payer.d.ts +65 -0
- package/dist/base-payer.d.ts.map +1 -0
- package/dist/base-payer.js +222 -0
- package/dist/base-payer.js.map +1 -0
- package/dist/cli.js +93 -11
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +185 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +744 -85
- package/dist/index.js.map +1 -1
- package/dist/jws.d.ts +46 -0
- package/dist/jws.d.ts.map +1 -0
- package/dist/jws.js +129 -0
- package/dist/jws.js.map +1 -0
- package/dist/payer.d.ts +65 -0
- package/dist/payer.d.ts.map +1 -0
- package/dist/payer.js +148 -0
- package/dist/payer.js.map +1 -0
- package/dist/solana-payer.d.ts +99 -0
- package/dist/solana-payer.d.ts.map +1 -0
- package/dist/solana-payer.js +258 -0
- package/dist/solana-payer.js.map +1 -0
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -12,23 +12,51 @@
|
|
|
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.canonicalSignedPayload = exports.BASE_SEPOLIA_RPC_URL = exports.BASE_RPC_URL = exports.
|
|
15
|
+
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.canonicalPayloadVersion = canonicalPayloadVersion;
|
|
16
17
|
exports.canonicalIssuerPayload = canonicalIssuerPayload;
|
|
18
|
+
exports.sumUsdcTransfersToPayee = sumUsdcTransfersToPayee;
|
|
19
|
+
exports.reconcileSettledTransfer = reconcileSettledTransfer;
|
|
17
20
|
exports.verifyIssuerSignature = verifyIssuerSignature;
|
|
18
21
|
exports.verifyIssuerSignatureWithJwks = verifyIssuerSignatureWithJwks;
|
|
19
22
|
exports.verifyBinding = verifyBinding;
|
|
20
23
|
exports.verifyNullifier = verifyNullifier;
|
|
21
24
|
exports.hashCanonicalPayload = hashCanonicalPayload;
|
|
25
|
+
exports.diffOuterClaims = diffOuterClaims;
|
|
26
|
+
exports.fetchIssuerJwks = fetchIssuerJwks;
|
|
27
|
+
exports.receiptPayerClaims = receiptPayerClaims;
|
|
22
28
|
exports.verifyReceipt = verifyReceipt;
|
|
23
29
|
const ethers_1 = require("ethers");
|
|
24
30
|
const node_crypto_1 = require("node:crypto");
|
|
25
31
|
const binding_js_1 = require("./binding.js");
|
|
26
32
|
Object.defineProperty(exports, "computePaymentCommitment", { enumerable: true, get: function () { return binding_js_1.computePaymentCommitment; } });
|
|
27
33
|
Object.defineProperty(exports, "computeInferenceBinding", { enumerable: true, get: function () { return binding_js_1.computeInferenceBinding; } });
|
|
34
|
+
const base_payer_js_1 = require("./base-payer.js");
|
|
35
|
+
Object.defineProperty(exports, "verifyBasePayer", { enumerable: true, get: function () { return base_payer_js_1.verifyBasePayer; } });
|
|
36
|
+
Object.defineProperty(exports, "parseBasePaymentRef", { enumerable: true, get: function () { return base_payer_js_1.parseBasePaymentRef; } });
|
|
37
|
+
Object.defineProperty(exports, "isEvmAddress", { enumerable: true, get: function () { return base_payer_js_1.isEvmAddress; } });
|
|
38
|
+
Object.defineProperty(exports, "sumUsdcTransfersFromPayer", { enumerable: true, get: function () { return base_payer_js_1.sumUsdcTransfersFromPayer; } });
|
|
39
|
+
Object.defineProperty(exports, "fetchBaseTransactionReceipt", { enumerable: true, get: function () { return base_payer_js_1.fetchBaseTransactionReceipt; } });
|
|
40
|
+
Object.defineProperty(exports, "USDC_ADDRESSES", { enumerable: true, get: function () { return base_payer_js_1.USDC_ADDRESSES; } });
|
|
41
|
+
Object.defineProperty(exports, "ERC20_TRANSFER_TOPIC", { enumerable: true, get: function () { return base_payer_js_1.ERC20_TRANSFER_TOPIC; } });
|
|
42
|
+
Object.defineProperty(exports, "BASE_RPC_URL", { enumerable: true, get: function () { return base_payer_js_1.BASE_RPC_URL; } });
|
|
43
|
+
const solana_payer_js_1 = require("./solana-payer.js");
|
|
44
|
+
Object.defineProperty(exports, "verifySolanaPayer", { enumerable: true, get: function () { return solana_payer_js_1.verifySolanaPayer; } });
|
|
45
|
+
Object.defineProperty(exports, "parseSolanaPaymentRef", { enumerable: true, get: function () { return solana_payer_js_1.parseSolanaPaymentRef; } });
|
|
46
|
+
Object.defineProperty(exports, "isSolanaBase58Pubkey", { enumerable: true, get: function () { return solana_payer_js_1.isSolanaBase58Pubkey; } });
|
|
47
|
+
Object.defineProperty(exports, "extractUsdcTransfersFromTx", { enumerable: true, get: function () { return solana_payer_js_1.extractUsdcTransfersFromTx; } });
|
|
48
|
+
Object.defineProperty(exports, "inferUsdcOutflowFromBalances", { enumerable: true, get: function () { return solana_payer_js_1.inferUsdcOutflowFromBalances; } });
|
|
49
|
+
Object.defineProperty(exports, "fetchSolanaTransaction", { enumerable: true, get: function () { return solana_payer_js_1.fetchSolanaTransaction; } });
|
|
50
|
+
Object.defineProperty(exports, "SOLANA_RPC_URL", { enumerable: true, get: function () { return solana_payer_js_1.SOLANA_RPC_URL; } });
|
|
51
|
+
Object.defineProperty(exports, "SOLANA_USDC_MINT_MAINNET", { enumerable: true, get: function () { return solana_payer_js_1.SOLANA_USDC_MINT_MAINNET; } });
|
|
52
|
+
Object.defineProperty(exports, "SOLANA_USDC_MINT_DEVNET", { enumerable: true, get: function () { return solana_payer_js_1.SOLANA_USDC_MINT_DEVNET; } });
|
|
53
|
+
const payer_js_1 = require("./payer.js");
|
|
54
|
+
Object.defineProperty(exports, "verifyPayerBinding", { enumerable: true, get: function () { return payer_js_1.verifyPayerBinding; } });
|
|
55
|
+
Object.defineProperty(exports, "receiptPayerClaimsFromEnvelope", { enumerable: true, get: function () { return payer_js_1.receiptPayerClaimsFromEnvelope; } });
|
|
56
|
+
Object.defineProperty(exports, "decodeJwsPayload", { enumerable: true, get: function () { return payer_js_1.decodeJwsPayload; } });
|
|
57
|
+
const jws_js_1 = require("./jws.js");
|
|
28
58
|
/** ZKVerifierSP1 contract on Base mainnet. */
|
|
29
59
|
exports.ZK_VERIFIER_ADDRESS = '0x9373499645292715a2275A78eD65B14215C41c06';
|
|
30
|
-
/** Default Base mainnet RPC. */
|
|
31
|
-
exports.BASE_RPC_URL = 'https://mainnet.base.org';
|
|
32
60
|
/** Base Sepolia testnet RPC. */
|
|
33
61
|
exports.BASE_SEPOLIA_RPC_URL = 'https://sepolia.base.org';
|
|
34
62
|
/** Minimal ABI for nullifier check. */
|
|
@@ -36,28 +64,28 @@ const ZK_VERIFIER_ABI = [
|
|
|
36
64
|
'function usedNullifiers(bytes32) view returns (bool)',
|
|
37
65
|
];
|
|
38
66
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* 2. payment.rail
|
|
45
|
-
* 3. payment.ref
|
|
46
|
-
* 4. payment.gross_amount
|
|
47
|
-
* 5. payment.net_amount
|
|
48
|
-
* 6. payment.fee_amount
|
|
49
|
-
* 7. payment.protocol_fee_bps ?? payment.fee_bps
|
|
50
|
-
* 8. payment.platform_fee
|
|
51
|
-
* 9. payment.platform_fee_bps
|
|
52
|
-
* 10. provider_cogs.actual
|
|
53
|
-
* 11. route.model
|
|
54
|
-
* 12. route.model_commitment.commitment
|
|
55
|
-
* 13. route.provider
|
|
56
|
-
* 14. output.hash
|
|
57
|
-
* 15. binding.expected_commitment
|
|
67
|
+
* HMAC payload version.
|
|
68
|
+
* Versions <= 7 keep the historical net/fee field list.
|
|
69
|
+
* Version 8 signs settled_amount plus the internal accounting breakdown.
|
|
70
|
+
* Lockstep with `canonicalPayloadVersion` in services/gateway/src/receipt.js
|
|
71
|
+
* for the v8 list (the v7 list here is the historical 15-field public formula).
|
|
58
72
|
*/
|
|
59
|
-
function
|
|
60
|
-
|
|
73
|
+
function canonicalPayloadVersion(receipt) {
|
|
74
|
+
const stamped = receipt.hmac_attestation?.payload_version
|
|
75
|
+
?? receipt.signature?.payload_version;
|
|
76
|
+
if (stamped != null)
|
|
77
|
+
return Number(stamped);
|
|
78
|
+
const issuerV = receipt.issuer_signature?.payload_version;
|
|
79
|
+
if (issuerV != null)
|
|
80
|
+
return Number(issuerV);
|
|
81
|
+
const payment = receipt.payment;
|
|
82
|
+
if (payment?.accounting || (payment && Object.prototype.hasOwnProperty.call(payment, 'settled_amount'))) {
|
|
83
|
+
return 8;
|
|
84
|
+
}
|
|
85
|
+
return 7;
|
|
86
|
+
}
|
|
87
|
+
function canonicalFieldsV7(receipt) {
|
|
88
|
+
return [
|
|
61
89
|
receipt.task_id ?? null,
|
|
62
90
|
receipt.payment?.rail ?? null,
|
|
63
91
|
receipt.payment?.ref ?? null,
|
|
@@ -73,7 +101,204 @@ function canonicalIssuerPayload(receipt) {
|
|
|
73
101
|
receipt.route?.provider ?? null,
|
|
74
102
|
receipt.output?.hash ?? null,
|
|
75
103
|
receipt.binding?.expected_commitment ?? null,
|
|
76
|
-
]
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
function canonicalFieldsV8(receipt) {
|
|
107
|
+
const breakdown = receipt.payment?.accounting?.internal_breakdown;
|
|
108
|
+
const caller = receipt.caller_binding;
|
|
109
|
+
return [
|
|
110
|
+
receipt.task_id ?? null,
|
|
111
|
+
receipt.payment?.rail ?? null,
|
|
112
|
+
receipt.payment?.ref ?? null,
|
|
113
|
+
receipt.payment?.gross_amount ?? null,
|
|
114
|
+
receipt.payment?.settled_amount ?? null,
|
|
115
|
+
breakdown?.route_margin_bps ?? null,
|
|
116
|
+
breakdown?.route_margin_amount ?? null,
|
|
117
|
+
breakdown?.receipt_floor_amount ?? null,
|
|
118
|
+
breakdown?.provider_cogs_amount ?? null,
|
|
119
|
+
breakdown?.tier2_proof_amount ?? null,
|
|
120
|
+
receipt.provider_cogs?.actual ?? null,
|
|
121
|
+
receipt.route?.model ?? null,
|
|
122
|
+
receipt.route?.model_commitment?.commitment ?? null,
|
|
123
|
+
receipt.route?.provider ?? null,
|
|
124
|
+
receipt.output?.hash ?? null,
|
|
125
|
+
receipt.binding?.expected_commitment ?? null,
|
|
126
|
+
caller?.payer_wallet ?? null,
|
|
127
|
+
caller?.agent_pubkey ?? null,
|
|
128
|
+
caller?.api_key_hash ?? null,
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Canonical, order-stable payload an HMAC issuer signature covers.
|
|
133
|
+
* v8 matches `canonicalSignedPayload` in services/gateway/src/receipt.js.
|
|
134
|
+
* v7 and earlier stay on the historical 15-field list so old signatures verify.
|
|
135
|
+
*/
|
|
136
|
+
function canonicalIssuerPayload(receipt) {
|
|
137
|
+
const fields = canonicalPayloadVersion(receipt) >= 8
|
|
138
|
+
? canonicalFieldsV8(receipt)
|
|
139
|
+
: canonicalFieldsV7(receipt);
|
|
140
|
+
return JSON.stringify(fields);
|
|
141
|
+
}
|
|
142
|
+
function topicAddress(topic) {
|
|
143
|
+
const hex = topic.toLowerCase().replace(/^0x/, '');
|
|
144
|
+
return `0x${hex.slice(-40)}`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Sum USDC Transfer values to `payee` in a transaction's logs.
|
|
148
|
+
* Non-Transfer logs are ignored. When `usdcAddress` is set, other contracts are ignored.
|
|
149
|
+
*/
|
|
150
|
+
function sumUsdcTransfersToPayee(logs, payee, usdcAddress) {
|
|
151
|
+
if (!payee || !(0, base_payer_js_1.isEvmAddress)(payee))
|
|
152
|
+
return null;
|
|
153
|
+
const payeeLower = payee.toLowerCase();
|
|
154
|
+
const usdcLower = usdcAddress?.toLowerCase();
|
|
155
|
+
let total = 0n;
|
|
156
|
+
let found = false;
|
|
157
|
+
for (const log of logs || []) {
|
|
158
|
+
if (usdcLower && log.address?.toLowerCase() !== usdcLower)
|
|
159
|
+
continue;
|
|
160
|
+
const topics = log.topics || [];
|
|
161
|
+
if (topics[0]?.toLowerCase() !== base_payer_js_1.ERC20_TRANSFER_TOPIC)
|
|
162
|
+
continue;
|
|
163
|
+
if (topics.length < 3 || !topics[2])
|
|
164
|
+
continue;
|
|
165
|
+
if (topicAddress(topics[2]) !== payeeLower)
|
|
166
|
+
continue;
|
|
167
|
+
found = true;
|
|
168
|
+
total += BigInt(log.data || '0x0');
|
|
169
|
+
}
|
|
170
|
+
return found ? total : null;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Payment facts for on-chain reconciliation.
|
|
174
|
+
* A JWS is used only after `verifyIssuerSignatureWithJwks` accepts the key.
|
|
175
|
+
* An untrusted or invalid JWS is not decoded into the comparison, and the
|
|
176
|
+
* unsigned outer `payment` copy is not a fallback for those claims.
|
|
177
|
+
* Receipts with no JWS still use the outer payment (legacy envelopes).
|
|
178
|
+
*/
|
|
179
|
+
function trustedPaymentForReconcile(receipt, options) {
|
|
180
|
+
const outerVersion = Number(receipt.issuer_signature?.payload_version ?? canonicalPayloadVersion(receipt));
|
|
181
|
+
const jws = receipt.issuer_signature?.jws;
|
|
182
|
+
if (!jws) {
|
|
183
|
+
return { ok: true, version: outerVersion, payment: receipt.payment || {} };
|
|
184
|
+
}
|
|
185
|
+
const verification = verifyIssuerSignatureWithJwks(receipt, options.jwks, {
|
|
186
|
+
trustedKids: options.trustedKids,
|
|
187
|
+
});
|
|
188
|
+
if (!verification.valid || verification.key_trusted === false) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
version: outerVersion,
|
|
192
|
+
reason: verification.reason || jws_js_1.KEY_UNTRUSTED,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const claims = verification.payload;
|
|
196
|
+
const claimPayment = (claims?.payment && typeof claims.payment === 'object')
|
|
197
|
+
? claims.payment
|
|
198
|
+
: null;
|
|
199
|
+
if (!claimPayment) {
|
|
200
|
+
return { ok: false, version: outerVersion, reason: 'no_signed_amount' };
|
|
201
|
+
}
|
|
202
|
+
const version = typeof claims?.payload_version === 'number'
|
|
203
|
+
? claims.payload_version
|
|
204
|
+
: outerVersion;
|
|
205
|
+
return { ok: true, version: Number(version), payment: claimPayment };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Optional on-chain reconciliation. Given a tx receipt's logs, the signed
|
|
209
|
+
* amount the payee was supposed to receive must equal the USDC Transfer to payee.
|
|
210
|
+
*
|
|
211
|
+
* The amount and payee come from JWS claims only after a key-trust check
|
|
212
|
+
* (JWKS by kid, or an embedded key whose thumbprint is a pinned kid). An
|
|
213
|
+
* untrusted signature does not contribute those facts.
|
|
214
|
+
*
|
|
215
|
+
* v8 compares `settled_amount` (falling back to `gross_amount`): that is the
|
|
216
|
+
* on-chain transfer. v7 and earlier compare `net_amount`, which those receipts
|
|
217
|
+
* defined as "amount after fees". A single direct transfer of gross will not
|
|
218
|
+
* match a net that subtracts a fee that never moved on chain.
|
|
219
|
+
*/
|
|
220
|
+
function reconcileSettledTransfer(receipt, logs, options = {}) {
|
|
221
|
+
const trusted = trustedPaymentForReconcile(receipt, options);
|
|
222
|
+
if (!trusted.ok) {
|
|
223
|
+
return {
|
|
224
|
+
checked: false,
|
|
225
|
+
matches: false,
|
|
226
|
+
payload_version: trusted.version,
|
|
227
|
+
signed_field: null,
|
|
228
|
+
signed_amount: null,
|
|
229
|
+
transfer_amount: null,
|
|
230
|
+
payee: null,
|
|
231
|
+
reason: trusted.reason,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const { version, payment } = trusted;
|
|
235
|
+
const payee = payment.payee ?? null;
|
|
236
|
+
let signed_field = null;
|
|
237
|
+
let signed_amount = null;
|
|
238
|
+
if (version >= 8) {
|
|
239
|
+
if (payment.settled_amount != null && payment.settled_amount !== '') {
|
|
240
|
+
signed_field = 'settled_amount';
|
|
241
|
+
signed_amount = String(payment.settled_amount);
|
|
242
|
+
}
|
|
243
|
+
else if (payment.gross_amount != null && payment.gross_amount !== '') {
|
|
244
|
+
signed_field = 'gross_amount';
|
|
245
|
+
signed_amount = String(payment.gross_amount);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
signed_field = 'net_amount';
|
|
250
|
+
signed_amount = payment.net_amount != null ? String(payment.net_amount) : null;
|
|
251
|
+
}
|
|
252
|
+
if (!payee || !(0, base_payer_js_1.isEvmAddress)(payee)) {
|
|
253
|
+
return {
|
|
254
|
+
checked: false,
|
|
255
|
+
matches: false,
|
|
256
|
+
payload_version: version,
|
|
257
|
+
signed_field,
|
|
258
|
+
signed_amount,
|
|
259
|
+
transfer_amount: null,
|
|
260
|
+
payee,
|
|
261
|
+
reason: 'no_payee',
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (signed_amount == null) {
|
|
265
|
+
return {
|
|
266
|
+
checked: false,
|
|
267
|
+
matches: false,
|
|
268
|
+
payload_version: version,
|
|
269
|
+
signed_field,
|
|
270
|
+
signed_amount,
|
|
271
|
+
transfer_amount: null,
|
|
272
|
+
payee,
|
|
273
|
+
reason: 'no_signed_amount',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const transfer = sumUsdcTransfersToPayee(logs, payee, options.usdcAddress);
|
|
277
|
+
if (transfer == null) {
|
|
278
|
+
return {
|
|
279
|
+
checked: true,
|
|
280
|
+
matches: false,
|
|
281
|
+
payload_version: version,
|
|
282
|
+
signed_field,
|
|
283
|
+
signed_amount,
|
|
284
|
+
transfer_amount: null,
|
|
285
|
+
payee,
|
|
286
|
+
reason: 'no_transfer_to_payee',
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const matches = BigInt(signed_amount) === transfer;
|
|
290
|
+
return {
|
|
291
|
+
checked: true,
|
|
292
|
+
matches,
|
|
293
|
+
payload_version: version,
|
|
294
|
+
signed_field,
|
|
295
|
+
signed_amount,
|
|
296
|
+
transfer_amount: transfer.toString(),
|
|
297
|
+
payee,
|
|
298
|
+
reason: matches
|
|
299
|
+
? undefined
|
|
300
|
+
: `amount_mismatch: signed ${signed_field} ${signed_amount} !== transfer ${transfer.toString()}`,
|
|
301
|
+
};
|
|
77
302
|
}
|
|
78
303
|
/**
|
|
79
304
|
* Verify a receipt's ES256 issuer signature against a JWK (public key).
|
|
@@ -123,28 +348,122 @@ function verifyIssuerSignature(receipt, jwk) {
|
|
|
123
348
|
* @param receipt - Receipt JSON with issuer_signature
|
|
124
349
|
* @param jwks - JWKS with keys array
|
|
125
350
|
*/
|
|
126
|
-
function
|
|
351
|
+
function jwksCandidates(jwks, kid) {
|
|
352
|
+
const keys = jwks?.keys || [];
|
|
353
|
+
const es256 = keys.filter((k) => (0, jws_js_1.isEs256PublicJwk)(k) && (k.alg == null || k.alg === 'ES256'));
|
|
354
|
+
if (!kid)
|
|
355
|
+
return es256;
|
|
356
|
+
return es256.filter((k) => k.kid === kid);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Verify an issuer signature against trusted keys only.
|
|
360
|
+
*
|
|
361
|
+
* Trust, in order:
|
|
362
|
+
* 1. A JWKS entry matched by `kid` (supplied file or fetched issuer JWKS).
|
|
363
|
+
* 2. An embedded `issuer_jwk` whose RFC 7638 thumbprint equals a pinned trusted kid.
|
|
364
|
+
*
|
|
365
|
+
* An embedded key that merely verifies the bytes is `key untrusted`.
|
|
366
|
+
*/
|
|
367
|
+
function verifyIssuerSignatureWithJwks(receipt, jwks, options = {}) {
|
|
127
368
|
const sig = receipt.issuer_signature;
|
|
128
|
-
if (!sig
|
|
129
|
-
return { checked: false, valid: false, reason: 'no_issuer_signature' };
|
|
369
|
+
if (!sig) {
|
|
370
|
+
return { checked: false, valid: false, key_trusted: false, reason: 'no_issuer_signature' };
|
|
130
371
|
}
|
|
131
|
-
|
|
132
|
-
|
|
372
|
+
const trustedKids = options.trustedKids ?? jws_js_1.DEFAULT_TRUSTED_ISSUER_KIDS;
|
|
373
|
+
const header = sig.jws ? (0, jws_js_1.readJwsHeader)(sig.jws) : null;
|
|
374
|
+
const kid = header?.kid || sig.kid;
|
|
375
|
+
const embedded = (0, jws_js_1.resolvePinnedIssuerJwk)(receipt);
|
|
376
|
+
const pinned = !!(embedded && (0, jws_js_1.isPinnedTrustedJwk)(embedded, trustedKids));
|
|
377
|
+
if (sig.jws) {
|
|
378
|
+
const fromJwks = jwksCandidates(jwks, kid);
|
|
379
|
+
for (const jwk of fromJwks) {
|
|
380
|
+
const jwsResult = (0, jws_js_1.verifyIssuerJws)(sig.jws, jwk);
|
|
381
|
+
if (jwsResult.valid) {
|
|
382
|
+
return {
|
|
383
|
+
checked: true,
|
|
384
|
+
valid: true,
|
|
385
|
+
key_trusted: true,
|
|
386
|
+
trust: 'jwks',
|
|
387
|
+
kid: jwk.kid || kid,
|
|
388
|
+
payload: jwsResult.payload,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (pinned && embedded) {
|
|
393
|
+
const pinResult = (0, jws_js_1.verifyIssuerJws)(sig.jws, embedded);
|
|
394
|
+
if (pinResult.valid) {
|
|
395
|
+
return {
|
|
396
|
+
checked: true,
|
|
397
|
+
valid: true,
|
|
398
|
+
key_trusted: true,
|
|
399
|
+
trust: 'pinned_kid',
|
|
400
|
+
kid: embedded.kid || (0, jws_js_1.jwkThumbprint)(embedded),
|
|
401
|
+
payload: pinResult.payload,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
checked: true,
|
|
406
|
+
valid: false,
|
|
407
|
+
key_trusted: true,
|
|
408
|
+
kid: kid || embedded.kid,
|
|
409
|
+
reason: pinResult.reason || 'signature_invalid',
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (embedded && (0, jws_js_1.verifyIssuerJws)(sig.jws, embedded).valid) {
|
|
413
|
+
return {
|
|
414
|
+
checked: true,
|
|
415
|
+
valid: false,
|
|
416
|
+
key_trusted: false,
|
|
417
|
+
kid: embedded.kid || kid,
|
|
418
|
+
reason: jws_js_1.KEY_UNTRUSTED,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
if (fromJwks.length === 0 && !pinned) {
|
|
422
|
+
return {
|
|
423
|
+
checked: true,
|
|
424
|
+
valid: false,
|
|
425
|
+
key_trusted: false,
|
|
426
|
+
kid,
|
|
427
|
+
reason: jws_js_1.KEY_UNTRUSTED,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
checked: true,
|
|
432
|
+
valid: false,
|
|
433
|
+
key_trusted: fromJwks.length > 0 || pinned,
|
|
434
|
+
kid,
|
|
435
|
+
reason: 'signature_invalid',
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
if (!sig.value) {
|
|
439
|
+
return { checked: false, valid: false, key_trusted: false, reason: 'no_issuer_signature' };
|
|
440
|
+
}
|
|
441
|
+
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
442
|
+
return {
|
|
443
|
+
checked: false,
|
|
444
|
+
valid: false,
|
|
445
|
+
key_trusted: false,
|
|
446
|
+
kid: sig.kid,
|
|
447
|
+
reason: 'JWKS not provided — pass jwks option to verify issuer signature',
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (jwks.keys.length === 0) {
|
|
451
|
+
return { checked: false, valid: false, key_trusted: false, kid: sig.kid, reason: 'empty_jwks' };
|
|
133
452
|
}
|
|
134
453
|
// Find matching key by kid, or try all ES256 keys if no kid on signature
|
|
135
454
|
const candidates = sig.kid
|
|
136
455
|
? jwks.keys.filter(k => k.kid === sig.kid && k.alg === 'ES256')
|
|
137
456
|
: jwks.keys.filter(k => k.alg === 'ES256');
|
|
138
457
|
if (candidates.length === 0) {
|
|
139
|
-
return { checked: false, valid: false, reason: 'no_matching_key' };
|
|
458
|
+
return { checked: false, valid: false, key_trusted: false, kid: sig.kid, reason: 'no_matching_key' };
|
|
140
459
|
}
|
|
141
460
|
for (const jwk of candidates) {
|
|
142
461
|
const result = verifyIssuerSignature(receipt, jwk);
|
|
143
462
|
if (result.valid) {
|
|
144
|
-
return result;
|
|
463
|
+
return { ...result, key_trusted: true, trust: 'jwks', kid: jwk.kid || sig.kid };
|
|
145
464
|
}
|
|
146
465
|
}
|
|
147
|
-
return { checked: true, valid: false, reason: 'signature_invalid' };
|
|
466
|
+
return { checked: true, valid: false, key_trusted: true, kid: sig.kid, reason: 'signature_invalid' };
|
|
148
467
|
}
|
|
149
468
|
/**
|
|
150
469
|
* Verify a receipt's payment binding locally (no network required).
|
|
@@ -152,8 +471,18 @@ function verifyIssuerSignatureWithJwks(receipt, jwks) {
|
|
|
152
471
|
* This recomputes the commitment from the receipt's fields and compares
|
|
153
472
|
* it to the stored `binding.expected_commitment`.
|
|
154
473
|
*/
|
|
474
|
+
function noCommitmentReason(rail) {
|
|
475
|
+
if (rail === 'unmetered' || rail === 'tfuel') {
|
|
476
|
+
return 'No binding present on receipt (unmetered or TFUEL rail)';
|
|
477
|
+
}
|
|
478
|
+
if (rail === 'usdc' || rail === 'reported') {
|
|
479
|
+
return 'No payment-binding commitment (expected_commitment is null)';
|
|
480
|
+
}
|
|
481
|
+
return 'No binding present on receipt (may be unmetered or TFUEL rail)';
|
|
482
|
+
}
|
|
155
483
|
function verifyBinding(receipt) {
|
|
156
484
|
const binding = receipt.binding;
|
|
485
|
+
const railHint = binding?.rail || receipt.payment?.rail || null;
|
|
157
486
|
if (!binding) {
|
|
158
487
|
return {
|
|
159
488
|
verified: false,
|
|
@@ -161,7 +490,17 @@ function verifyBinding(receipt) {
|
|
|
161
490
|
recomputed: null,
|
|
162
491
|
matches: false,
|
|
163
492
|
covers: [],
|
|
164
|
-
reason:
|
|
493
|
+
reason: noCommitmentReason(railHint),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
if (binding.expected_commitment == null || binding.expected_commitment === '') {
|
|
497
|
+
return {
|
|
498
|
+
verified: false,
|
|
499
|
+
expected: null,
|
|
500
|
+
recomputed: null,
|
|
501
|
+
matches: false,
|
|
502
|
+
covers: binding.covers || [],
|
|
503
|
+
reason: noCommitmentReason(railHint),
|
|
165
504
|
};
|
|
166
505
|
}
|
|
167
506
|
const paymentRef = receipt.payment?.ref ?? null;
|
|
@@ -241,7 +580,7 @@ async function verifyNullifier(receipt, options = {}) {
|
|
|
241
580
|
reason: 'Invalid nullifier format',
|
|
242
581
|
};
|
|
243
582
|
}
|
|
244
|
-
const rpcUrl = options.rpcUrl ||
|
|
583
|
+
const rpcUrl = options.rpcUrl || base_payer_js_1.BASE_RPC_URL;
|
|
245
584
|
const verifierAddress = options.verifierAddress || exports.ZK_VERIFIER_ADDRESS;
|
|
246
585
|
try {
|
|
247
586
|
const provider = new ethers_1.JsonRpcProvider(rpcUrl);
|
|
@@ -274,33 +613,240 @@ exports.canonicalSignedPayload = canonicalIssuerPayload;
|
|
|
274
613
|
function hashCanonicalPayload(receipt) {
|
|
275
614
|
return (0, ethers_1.keccak256)((0, ethers_1.toUtf8Bytes)((0, exports.canonicalSignedPayload)(receipt)));
|
|
276
615
|
}
|
|
616
|
+
function claimString(value) {
|
|
617
|
+
if (value == null || value === '')
|
|
618
|
+
return null;
|
|
619
|
+
return String(value);
|
|
620
|
+
}
|
|
621
|
+
function asRecord(value) {
|
|
622
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
623
|
+
return null;
|
|
624
|
+
return value;
|
|
625
|
+
}
|
|
626
|
+
function normFact(value, address = false) {
|
|
627
|
+
const text = claimString(value);
|
|
628
|
+
if (text == null)
|
|
629
|
+
return null;
|
|
630
|
+
return address ? text.toLowerCase() : text;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Fields a verifier may quote. Compared only when both the unsigned outer
|
|
634
|
+
* copy and the JWS payload carry a value.
|
|
635
|
+
*/
|
|
636
|
+
const CLAIM_COMPARE = [
|
|
637
|
+
{ field: 'task_id', outer: (r) => r.task_id, signed: (c) => c.task_id },
|
|
638
|
+
{ field: 'payment.rail', outer: (r) => r.payment?.rail, signed: (c) => asRecord(c.payment)?.rail },
|
|
639
|
+
{ field: 'payment.ref', outer: (r) => r.payment?.ref, signed: (c) => asRecord(c.payment)?.ref },
|
|
640
|
+
{ field: 'payment.gross_amount', outer: (r) => r.payment?.gross_amount, signed: (c) => asRecord(c.payment)?.gross_amount },
|
|
641
|
+
{ field: 'payment.settled_amount', outer: (r) => r.payment?.settled_amount, signed: (c) => asRecord(c.payment)?.settled_amount },
|
|
642
|
+
{ field: 'payment.net_amount', outer: (r) => r.payment?.net_amount, signed: (c) => asRecord(c.payment)?.net_amount },
|
|
643
|
+
{ field: 'payment.asset', outer: (r) => r.payment?.asset, signed: (c) => asRecord(c.payment)?.asset, address: true },
|
|
644
|
+
{ field: 'payment.payee', outer: (r) => r.payment?.payee, signed: (c) => asRecord(c.payment)?.payee, address: true },
|
|
645
|
+
{ field: 'caller_binding.payer_wallet', outer: (r) => r.caller_binding?.payer_wallet, signed: (c) => asRecord(c.caller_binding)?.payer_wallet, address: true },
|
|
646
|
+
{ field: 'caller_binding.agent_pubkey', outer: (r) => r.caller_binding?.agent_pubkey, signed: (c) => asRecord(c.caller_binding)?.agent_pubkey },
|
|
647
|
+
{ field: 'caller_binding.api_key_hash', outer: (r) => r.caller_binding?.api_key_hash, signed: (c) => asRecord(c.caller_binding)?.api_key_hash },
|
|
648
|
+
{ field: 'route.model', outer: (r) => r.route?.model, signed: (c) => asRecord(c.route)?.model },
|
|
649
|
+
{ field: 'route.provider', outer: (r) => r.route?.provider, signed: (c) => asRecord(c.route)?.provider },
|
|
650
|
+
{ field: 'output.hash', outer: (r) => r.output?.hash, signed: (c) => asRecord(c.output)?.hash },
|
|
651
|
+
{ field: 'binding.expected_commitment', outer: (r) => r.binding?.expected_commitment, signed: (c) => asRecord(c.binding)?.expected_commitment },
|
|
652
|
+
{ field: 'provider_cogs.actual', outer: (r) => r.provider_cogs?.actual, signed: (c) => asRecord(c.provider_cogs)?.actual },
|
|
653
|
+
];
|
|
654
|
+
/** Flag unsigned outer copies that disagree with the JWS payload. */
|
|
655
|
+
function diffOuterClaims(receipt, claims) {
|
|
656
|
+
if (!claims)
|
|
657
|
+
return [];
|
|
658
|
+
const mismatches = [];
|
|
659
|
+
for (const spec of CLAIM_COMPARE) {
|
|
660
|
+
const outer = normFact(spec.outer(receipt), spec.address);
|
|
661
|
+
const signed = normFact(spec.signed(claims), spec.address);
|
|
662
|
+
if (outer == null || signed == null || outer === signed)
|
|
663
|
+
continue;
|
|
664
|
+
mismatches.push({
|
|
665
|
+
field: spec.field,
|
|
666
|
+
outer: claimString(spec.outer(receipt)),
|
|
667
|
+
signed: claimString(spec.signed(claims)),
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
return mismatches;
|
|
671
|
+
}
|
|
672
|
+
function receiptViewFromClaims(receipt, claims) {
|
|
673
|
+
const payment = asRecord(claims.payment);
|
|
674
|
+
const binding = asRecord(claims.binding);
|
|
675
|
+
const route = asRecord(claims.route);
|
|
676
|
+
const output = asRecord(claims.output);
|
|
677
|
+
const caller = asRecord(claims.caller_binding);
|
|
678
|
+
const modelCommitment = claimString(route?.model_commitment);
|
|
679
|
+
return {
|
|
680
|
+
...receipt,
|
|
681
|
+
task_id: claimString(claims.task_id) || receipt.task_id,
|
|
682
|
+
payment: {
|
|
683
|
+
rail: claimString(payment?.rail) ?? undefined,
|
|
684
|
+
ref: claimString(payment?.ref),
|
|
685
|
+
gross_amount: claimString(payment?.gross_amount),
|
|
686
|
+
settled_amount: claimString(payment?.settled_amount),
|
|
687
|
+
net_amount: claimString(payment?.net_amount),
|
|
688
|
+
asset: claimString(payment?.asset),
|
|
689
|
+
payee: claimString(payment?.payee),
|
|
690
|
+
fee_amount: claimString(payment?.fee_amount),
|
|
691
|
+
protocol_fee_bps: typeof payment?.protocol_fee_bps === 'number' ? payment.protocol_fee_bps : null,
|
|
692
|
+
platform_fee: claimString(payment?.platform_fee),
|
|
693
|
+
platform_fee_bps: typeof payment?.platform_fee_bps === 'number' ? payment.platform_fee_bps : null,
|
|
694
|
+
},
|
|
695
|
+
route: {
|
|
696
|
+
model: claimString(route?.model) ?? undefined,
|
|
697
|
+
provider: claimString(route?.provider) ?? undefined,
|
|
698
|
+
model_commitment: modelCommitment ? { commitment: modelCommitment } : null,
|
|
699
|
+
},
|
|
700
|
+
output: output?.hash ? { hash: claimString(output.hash) ?? undefined } : null,
|
|
701
|
+
caller_binding: caller
|
|
702
|
+
? {
|
|
703
|
+
payer_wallet: claimString(caller.payer_wallet),
|
|
704
|
+
agent_pubkey: claimString(caller.agent_pubkey),
|
|
705
|
+
api_key_hash: claimString(caller.api_key_hash),
|
|
706
|
+
}
|
|
707
|
+
: null,
|
|
708
|
+
provider_cogs: asRecord(claims.provider_cogs)?.actual != null
|
|
709
|
+
? { actual: claimString(asRecord(claims.provider_cogs)?.actual) ?? undefined }
|
|
710
|
+
: receipt.provider_cogs,
|
|
711
|
+
binding: {
|
|
712
|
+
expected_commitment: binding && 'expected_commitment' in binding
|
|
713
|
+
? (claimString(binding.expected_commitment) ?? undefined)
|
|
714
|
+
: undefined,
|
|
715
|
+
amount: claimString(binding?.amount)
|
|
716
|
+
|| claimString(payment?.net_amount)
|
|
717
|
+
|| claimString(payment?.gross_amount)
|
|
718
|
+
|| '0',
|
|
719
|
+
rail: claimString(binding?.rail) || claimString(payment?.rail) || undefined,
|
|
720
|
+
covers: Array.isArray(binding?.covers) ? binding.covers : ['payment', 'settlement'],
|
|
721
|
+
model_commitment: claimString(binding?.model_commitment) || modelCommitment,
|
|
722
|
+
output_hash: claimString(binding?.output_hash) || claimString(output?.hash),
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function payerClaimsFromPayload(payload) {
|
|
727
|
+
const payment = asRecord(payload.payment);
|
|
728
|
+
const caller = asRecord(payload.caller_binding);
|
|
729
|
+
return {
|
|
730
|
+
payment: {
|
|
731
|
+
ref: claimString(payment?.ref),
|
|
732
|
+
gross_amount: claimString(payment?.gross_amount),
|
|
733
|
+
payee: claimString(payment?.payee),
|
|
734
|
+
asset: claimString(payment?.asset),
|
|
735
|
+
rail: claimString(payment?.rail),
|
|
736
|
+
},
|
|
737
|
+
caller_binding: caller ? { payer_wallet: claimString(caller.payer_wallet) } : null,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function factsFromClaims(claims) {
|
|
741
|
+
if (!claims) {
|
|
742
|
+
return { hub: null, model: null, amount_usdc: null, tx: null, output_hash: null };
|
|
743
|
+
}
|
|
744
|
+
const payment = asRecord(claims.payment);
|
|
745
|
+
const route = asRecord(claims.route);
|
|
746
|
+
const output = asRecord(claims.output);
|
|
747
|
+
return {
|
|
748
|
+
hub: claimString(route?.provider),
|
|
749
|
+
model: claimString(route?.model),
|
|
750
|
+
amount_usdc: claimString(payment?.gross_amount),
|
|
751
|
+
tx: claimString(payment?.ref),
|
|
752
|
+
output_hash: claimString(output?.hash),
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function issuerJwksUri(receipt) {
|
|
756
|
+
const fromVerification = receipt.verification?.jwks_uri;
|
|
757
|
+
if (typeof fromVerification === 'string' && fromVerification.startsWith('https://'))
|
|
758
|
+
return fromVerification;
|
|
759
|
+
const jws = receipt.issuer_signature?.jws;
|
|
760
|
+
const header = jws ? (0, jws_js_1.readJwsHeader)(jws) : null;
|
|
761
|
+
if (header?.jku && header.jku.startsWith('https://'))
|
|
762
|
+
return header.jku;
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
function httpsUrl(uri) {
|
|
766
|
+
try {
|
|
767
|
+
return new URL(uri).protocol === 'https:';
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function mergeJwks(primary, extra) {
|
|
774
|
+
const keys = [...(primary?.keys || []), ...(extra?.keys || [])];
|
|
775
|
+
if (keys.length === 0)
|
|
776
|
+
return undefined;
|
|
777
|
+
const seen = new Set();
|
|
778
|
+
const deduped = [];
|
|
779
|
+
for (const key of keys) {
|
|
780
|
+
const id = `${key.kid || ''}:${key.x || ''}:${key.y || ''}`;
|
|
781
|
+
if (seen.has(id))
|
|
782
|
+
continue;
|
|
783
|
+
seen.add(id);
|
|
784
|
+
deduped.push(key);
|
|
785
|
+
}
|
|
786
|
+
return { keys: deduped };
|
|
787
|
+
}
|
|
788
|
+
/** Fetch a JWKS document. Caller decides whether the URL is a trust root. */
|
|
789
|
+
async function fetchIssuerJwks(uri, fetchImpl = globalThis.fetch) {
|
|
790
|
+
if (!httpsUrl(uri))
|
|
791
|
+
throw new Error('jwks_uri must be https');
|
|
792
|
+
const res = await fetchImpl(uri);
|
|
793
|
+
if (!res.ok)
|
|
794
|
+
throw new Error(`jwks_fetch_failed: ${res.status}`);
|
|
795
|
+
const body = await res.json();
|
|
796
|
+
if (!body || !Array.isArray(body.keys))
|
|
797
|
+
throw new Error('jwks_malformed');
|
|
798
|
+
return body;
|
|
799
|
+
}
|
|
277
800
|
/**
|
|
278
|
-
* Full receipt verification —
|
|
801
|
+
* Full receipt verification — trusted issuer signature, signed-claim facts,
|
|
802
|
+
* optional on-chain payer and nullifier checks.
|
|
279
803
|
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* @param options.rpcUrl RPC URL for on-chain checks
|
|
804
|
+
* Facts (amount, payer, payee, asset, tx, model) are taken only from a JWS
|
|
805
|
+
* that verified under a trusted key. The embedded `issuer_jwk` is not trusted
|
|
806
|
+
* unless its RFC 7638 thumbprint is a pinned kid, or the same key is in a JWKS.
|
|
284
807
|
*/
|
|
808
|
+
/** Extract JWS claims for payer binding. Prefers the JWS payload over the outer copy. */
|
|
809
|
+
function receiptPayerClaims(receipt) {
|
|
810
|
+
return (0, payer_js_1.receiptPayerClaimsFromEnvelope)(receipt);
|
|
811
|
+
}
|
|
285
812
|
async function verifyReceipt(receipt, options = {}) {
|
|
286
813
|
const errors = [];
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
814
|
+
const trustedKids = options.trustedKids ?? jws_js_1.DEFAULT_TRUSTED_ISSUER_KIDS;
|
|
815
|
+
const trustedHosts = options.trustedJwksHosts ?? jws_js_1.DEFAULT_TRUSTED_JWKS_HOSTS;
|
|
816
|
+
let jwks = options.jwks;
|
|
817
|
+
if (options.jwksUri || options.fetchJwks) {
|
|
818
|
+
const uri = options.jwksUri || issuerJwksUri(receipt);
|
|
819
|
+
const allowed = !!uri && (options.jwksUri ? httpsUrl(uri) : (0, jws_js_1.jwksHostAllowed)(uri, trustedHosts));
|
|
820
|
+
if (uri && allowed) {
|
|
821
|
+
try {
|
|
822
|
+
const fetched = await fetchIssuerJwks(uri, options.fetchImpl);
|
|
823
|
+
jwks = mergeJwks(jwks, fetched);
|
|
824
|
+
}
|
|
825
|
+
catch (err) {
|
|
826
|
+
errors.push(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
else if (options.jwksUri) {
|
|
830
|
+
errors.push('JWKS URI rejected — only https URLs are fetched');
|
|
831
|
+
}
|
|
832
|
+
else if (options.fetchJwks && uri) {
|
|
833
|
+
errors.push(`JWKS host untrusted: ${uri}`);
|
|
834
|
+
}
|
|
291
835
|
}
|
|
292
|
-
// Verify issuer signature if JWKS provided
|
|
293
836
|
let issuer_signature;
|
|
294
|
-
if (
|
|
295
|
-
issuer_signature = verifyIssuerSignatureWithJwks(receipt,
|
|
837
|
+
if (receipt.issuer_signature?.jws || receipt.issuer_signature?.value) {
|
|
838
|
+
issuer_signature = verifyIssuerSignatureWithJwks(receipt, jwks, { trustedKids });
|
|
296
839
|
if (issuer_signature.checked && !issuer_signature.valid) {
|
|
297
|
-
errors.push(
|
|
840
|
+
errors.push(issuer_signature.reason === jws_js_1.KEY_UNTRUSTED
|
|
841
|
+
? jws_js_1.KEY_UNTRUSTED
|
|
842
|
+
: `Issuer signature invalid: ${issuer_signature.reason}`);
|
|
298
843
|
}
|
|
299
844
|
}
|
|
300
845
|
else if (receipt.issuer_signature) {
|
|
301
846
|
issuer_signature = {
|
|
302
847
|
checked: false,
|
|
303
848
|
valid: false,
|
|
849
|
+
key_trusted: false,
|
|
304
850
|
kid: receipt.issuer_signature.kid,
|
|
305
851
|
reason: 'JWKS not provided — pass jwks option to verify issuer signature',
|
|
306
852
|
};
|
|
@@ -309,10 +855,106 @@ async function verifyReceipt(receipt, options = {}) {
|
|
|
309
855
|
issuer_signature = {
|
|
310
856
|
checked: false,
|
|
311
857
|
valid: false,
|
|
858
|
+
key_trusted: false,
|
|
312
859
|
reason: 'No issuer signature present on receipt',
|
|
313
860
|
};
|
|
314
861
|
}
|
|
315
|
-
|
|
862
|
+
const decoded = receipt.issuer_signature?.jws
|
|
863
|
+
? (0, payer_js_1.decodeJwsPayload)(receipt.issuer_signature.jws)
|
|
864
|
+
: null;
|
|
865
|
+
const claim_mismatches = diffOuterClaims(receipt, decoded);
|
|
866
|
+
for (const mismatch of claim_mismatches) {
|
|
867
|
+
errors.push(`outer/signed mismatch: ${mismatch.field} (outer ${mismatch.outer}, signed ${mismatch.signed})`);
|
|
868
|
+
}
|
|
869
|
+
const verifiedClaims = issuer_signature.valid ? issuer_signature.payload : undefined;
|
|
870
|
+
let binding;
|
|
871
|
+
if (receipt.issuer_signature?.jws) {
|
|
872
|
+
if (verifiedClaims) {
|
|
873
|
+
binding = verifyBinding(receiptViewFromClaims(receipt, verifiedClaims));
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
binding = {
|
|
877
|
+
verified: false,
|
|
878
|
+
expected: null,
|
|
879
|
+
recomputed: null,
|
|
880
|
+
matches: false,
|
|
881
|
+
covers: [],
|
|
882
|
+
reason: issuer_signature.reason === jws_js_1.KEY_UNTRUSTED
|
|
883
|
+
? 'Binding not checked — issuer key untrusted'
|
|
884
|
+
: 'Binding not checked — issuer signature not verified',
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
binding = verifyBinding(receipt);
|
|
890
|
+
}
|
|
891
|
+
if (!binding.matches && binding.expected) {
|
|
892
|
+
errors.push('Payment binding mismatch');
|
|
893
|
+
}
|
|
894
|
+
const signedPayerClaims = verifiedClaims ? payerClaimsFromPayload(verifiedClaims) : null;
|
|
895
|
+
let payer;
|
|
896
|
+
if (options.checkPayer) {
|
|
897
|
+
if (!signedPayerClaims) {
|
|
898
|
+
payer = {
|
|
899
|
+
checked: false,
|
|
900
|
+
valid: false,
|
|
901
|
+
reason: 'Payer facts come from verified signed claims — issuer signature is not trusted',
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
const payerResult = await (0, payer_js_1.verifyPayerBinding)(signedPayerClaims, {
|
|
906
|
+
rpcUrl: options.rpcUrl,
|
|
907
|
+
solanaRpcUrl: options.solanaRpcUrl,
|
|
908
|
+
fetchSolanaTransaction: options.fetchSolanaTransaction,
|
|
909
|
+
fetchBaseReceipt: options.fetchBaseReceipt,
|
|
910
|
+
});
|
|
911
|
+
payer = {
|
|
912
|
+
checked: payerResult.checked,
|
|
913
|
+
valid: payerResult.valid,
|
|
914
|
+
rail: payerResult.rail,
|
|
915
|
+
reason: payerResult.reason,
|
|
916
|
+
payer_wallet: payerResult.payerWallet ?? signedPayerClaims.caller_binding?.payer_wallet ?? null,
|
|
917
|
+
payee: signedPayerClaims.payment?.payee ?? null,
|
|
918
|
+
asset: signedPayerClaims.payment?.asset ?? null,
|
|
919
|
+
amount: payerResult.expectedAmount ?? signedPayerClaims.payment?.gross_amount ?? null,
|
|
920
|
+
};
|
|
921
|
+
if (payerResult.checked && !payerResult.valid) {
|
|
922
|
+
errors.push(`Payer binding mismatch: ${payerResult.reason}`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
else if (signedPayerClaims?.caller_binding?.payer_wallet && signedPayerClaims.payment?.ref) {
|
|
927
|
+
payer = {
|
|
928
|
+
checked: false,
|
|
929
|
+
valid: false,
|
|
930
|
+
payer_wallet: signedPayerClaims.caller_binding.payer_wallet,
|
|
931
|
+
payee: signedPayerClaims.payment.payee ?? null,
|
|
932
|
+
asset: signedPayerClaims.payment.asset ?? null,
|
|
933
|
+
amount: signedPayerClaims.payment.gross_amount ?? null,
|
|
934
|
+
reason: 'On-chain payer check not requested — pass checkPayer: true',
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
else if (!receipt.issuer_signature?.jws && receipt.caller_binding?.payer_wallet && receipt.payment?.ref) {
|
|
938
|
+
payer = {
|
|
939
|
+
checked: false,
|
|
940
|
+
valid: false,
|
|
941
|
+
reason: 'On-chain payer check not requested — pass checkPayer: true',
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
else if (receipt.issuer_signature?.jws && !issuer_signature.valid) {
|
|
945
|
+
payer = {
|
|
946
|
+
checked: false,
|
|
947
|
+
valid: false,
|
|
948
|
+
reason: 'Payer facts come from verified signed claims — issuer signature is not trusted',
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
payer = {
|
|
953
|
+
checked: false,
|
|
954
|
+
valid: false,
|
|
955
|
+
reason: 'No payer_wallet or payment.ref to verify',
|
|
956
|
+
};
|
|
957
|
+
}
|
|
316
958
|
let nullifier;
|
|
317
959
|
if (options.checkNullifier && receipt.proof?.nullifier) {
|
|
318
960
|
nullifier = await verifyNullifier(receipt, {
|
|
@@ -333,73 +975,90 @@ async function verifyReceipt(receipt, options = {}) {
|
|
|
333
975
|
: 'On-chain check not requested',
|
|
334
976
|
};
|
|
335
977
|
}
|
|
336
|
-
|
|
337
|
-
const hub = receipt.route?.provider ?? null;
|
|
338
|
-
const model = receipt.route?.model ?? null;
|
|
339
|
-
const amount_usdc = receipt.payment?.gross_amount ?? null;
|
|
340
|
-
const tx = receipt.payment?.ref ?? null;
|
|
341
|
-
const output_hash = receipt.output?.hash ?? null;
|
|
342
|
-
// Determine overall status with strict signature semantics:
|
|
343
|
-
// - If JWKS provided and signature invalid/kid mismatch/no matching key → 'failed'
|
|
344
|
-
// - If receipt has issuer_signature but no JWKS provided → 'partial' (cannot verify)
|
|
345
|
-
// - Unsigned receipts can be 'partial' or 'verified' based on binding
|
|
346
|
-
let overall;
|
|
347
|
-
// Check for signature verification failure (JWKS provided but signature invalid)
|
|
348
|
-
const jwksProvided = !!options.jwks;
|
|
978
|
+
const facts = factsFromClaims(verifiedClaims);
|
|
349
979
|
const hasIssuerSig = !!receipt.issuer_signature;
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
980
|
+
const jwksWasSupplied = !!(options.jwks || options.jwksUri || (options.fetchJwks && jwks));
|
|
981
|
+
const signatureFailed = hasIssuerSig && !issuer_signature.valid && (!!issuer_signature.checked
|
|
982
|
+
|| jwksWasSupplied
|
|
983
|
+
|| issuer_signature.reason === jws_js_1.KEY_UNTRUSTED
|
|
984
|
+
|| issuer_signature.reason === 'no_matching_key'
|
|
985
|
+
|| issuer_signature.reason === 'empty_jwks');
|
|
986
|
+
const mismatchFailed = claim_mismatches.length > 0;
|
|
987
|
+
const bindingFailed = !!(binding.expected && !binding.matches);
|
|
988
|
+
const payerFailed = !!(options.checkPayer && payer.checked && !payer.valid);
|
|
989
|
+
const nullifierFailed = !!(options.checkNullifier
|
|
990
|
+
&& receipt.proof?.nullifier
|
|
991
|
+
&& nullifier.anchored === false);
|
|
992
|
+
const signatureUnchecked = hasIssuerSig && !issuer_signature.checked;
|
|
993
|
+
let overall;
|
|
994
|
+
if (signatureFailed || mismatchFailed || bindingFailed || payerFailed || nullifierFailed) {
|
|
355
995
|
overall = 'failed';
|
|
356
996
|
}
|
|
357
|
-
else if (
|
|
358
|
-
// Receipt has signature but no JWKS provided → partial (cannot verify signature)
|
|
997
|
+
else if (signatureUnchecked) {
|
|
359
998
|
overall = 'partial';
|
|
360
999
|
}
|
|
361
|
-
else if (binding.
|
|
362
|
-
// Binding matches, signature OK (or no signature), nullifier OK → verified
|
|
363
|
-
overall = 'verified';
|
|
364
|
-
}
|
|
365
|
-
else if (binding.matches || issuer_signature.valid || (nullifier.verified && nullifier.anchored)) {
|
|
366
|
-
// At least one check passed → partial
|
|
1000
|
+
else if (!binding.expected && !hasIssuerSig && !options.checkPayer) {
|
|
367
1001
|
overall = 'partial';
|
|
368
1002
|
}
|
|
369
|
-
else if (
|
|
370
|
-
|
|
371
|
-
|
|
1003
|
+
else if ((binding.matches || !binding.expected)
|
|
1004
|
+
&& (!options.checkPayer || payer.valid)
|
|
1005
|
+
&& (!options.checkNullifier || !receipt.proof?.nullifier || nullifier.anchored === true)
|
|
1006
|
+
&& (issuer_signature.valid || !hasIssuerSig)) {
|
|
1007
|
+
overall = 'verified';
|
|
372
1008
|
}
|
|
373
1009
|
else {
|
|
374
|
-
overall = '
|
|
1010
|
+
overall = 'partial';
|
|
375
1011
|
}
|
|
376
1012
|
return {
|
|
377
1013
|
receipt_id: receipt.task_id,
|
|
378
1014
|
binding,
|
|
379
1015
|
issuer_signature,
|
|
1016
|
+
payer,
|
|
380
1017
|
nullifier,
|
|
381
|
-
output_hash,
|
|
382
|
-
hub,
|
|
383
|
-
model,
|
|
384
|
-
amount_usdc,
|
|
385
|
-
tx,
|
|
1018
|
+
output_hash: facts.output_hash,
|
|
1019
|
+
hub: facts.hub,
|
|
1020
|
+
model: facts.model,
|
|
1021
|
+
amount_usdc: facts.amount_usdc,
|
|
1022
|
+
tx: facts.tx,
|
|
1023
|
+
claim_mismatches,
|
|
386
1024
|
overall,
|
|
387
1025
|
errors,
|
|
388
1026
|
};
|
|
389
1027
|
}
|
|
1028
|
+
var jws_js_2 = require("./jws.js");
|
|
1029
|
+
Object.defineProperty(exports, "resolvePinnedIssuerJwk", { enumerable: true, get: function () { return jws_js_2.resolvePinnedIssuerJwk; } });
|
|
1030
|
+
Object.defineProperty(exports, "verifyIssuerJws", { enumerable: true, get: function () { return jws_js_2.verifyIssuerJws; } });
|
|
1031
|
+
Object.defineProperty(exports, "DEFAULT_TRUSTED_ISSUER_KIDS", { enumerable: true, get: function () { return jws_js_2.DEFAULT_TRUSTED_ISSUER_KIDS; } });
|
|
1032
|
+
Object.defineProperty(exports, "DEFAULT_TRUSTED_JWKS_HOSTS", { enumerable: true, get: function () { return jws_js_2.DEFAULT_TRUSTED_JWKS_HOSTS; } });
|
|
1033
|
+
Object.defineProperty(exports, "KEY_UNTRUSTED", { enumerable: true, get: function () { return jws_js_2.KEY_UNTRUSTED; } });
|
|
1034
|
+
Object.defineProperty(exports, "jwkThumbprint", { enumerable: true, get: function () { return jws_js_2.jwkThumbprint; } });
|
|
1035
|
+
Object.defineProperty(exports, "isPinnedTrustedJwk", { enumerable: true, get: function () { return jws_js_2.isPinnedTrustedJwk; } });
|
|
1036
|
+
Object.defineProperty(exports, "jwksHostAllowed", { enumerable: true, get: function () { return jws_js_2.jwksHostAllowed; } });
|
|
390
1037
|
exports.default = {
|
|
391
1038
|
verifyBinding,
|
|
392
1039
|
verifyIssuerSignature,
|
|
393
1040
|
verifyIssuerSignatureWithJwks,
|
|
1041
|
+
verifyIssuerJws: jws_js_1.verifyIssuerJws,
|
|
1042
|
+
resolvePinnedIssuerJwk: jws_js_1.resolvePinnedIssuerJwk,
|
|
394
1043
|
verifyNullifier,
|
|
1044
|
+
verifyPayerBinding: payer_js_1.verifyPayerBinding,
|
|
1045
|
+
verifySolanaPayer: solana_payer_js_1.verifySolanaPayer,
|
|
1046
|
+
verifyBasePayer: base_payer_js_1.verifyBasePayer,
|
|
395
1047
|
verifyReceipt,
|
|
1048
|
+
receiptPayerClaims,
|
|
1049
|
+
receiptPayerClaimsFromEnvelope: payer_js_1.receiptPayerClaimsFromEnvelope,
|
|
1050
|
+
decodeJwsPayload: payer_js_1.decodeJwsPayload,
|
|
396
1051
|
computePaymentCommitment: binding_js_1.computePaymentCommitment,
|
|
397
1052
|
computeInferenceBinding: binding_js_1.computeInferenceBinding,
|
|
398
1053
|
canonicalIssuerPayload,
|
|
1054
|
+
canonicalPayloadVersion,
|
|
399
1055
|
canonicalSignedPayload: exports.canonicalSignedPayload,
|
|
1056
|
+
reconcileSettledTransfer,
|
|
1057
|
+
sumUsdcTransfersToPayee,
|
|
400
1058
|
hashCanonicalPayload,
|
|
401
1059
|
ZK_VERIFIER_ADDRESS: exports.ZK_VERIFIER_ADDRESS,
|
|
402
|
-
BASE_RPC_URL:
|
|
1060
|
+
BASE_RPC_URL: base_payer_js_1.BASE_RPC_URL,
|
|
403
1061
|
BASE_SEPOLIA_RPC_URL: exports.BASE_SEPOLIA_RPC_URL,
|
|
1062
|
+
SOLANA_RPC_URL: solana_payer_js_1.SOLANA_RPC_URL,
|
|
404
1063
|
};
|
|
405
1064
|
//# sourceMappingURL=index.js.map
|