@xfuel/verify 0.1.1 → 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 +35 -17
- 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/cli.js +66 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +140 -47
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +675 -142
- package/dist/index.js.map +1 -1
- 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/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -12,13 +12,18 @@
|
|
|
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.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;
|
|
22
27
|
exports.receiptPayerClaims = receiptPayerClaims;
|
|
23
28
|
exports.verifyReceipt = verifyReceipt;
|
|
24
29
|
const ethers_1 = require("ethers");
|
|
@@ -59,28 +64,28 @@ const ZK_VERIFIER_ABI = [
|
|
|
59
64
|
'function usedNullifiers(bytes32) view returns (bool)',
|
|
60
65
|
];
|
|
61
66
|
/**
|
|
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
|
|
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).
|
|
81
72
|
*/
|
|
82
|
-
function
|
|
83
|
-
|
|
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 [
|
|
84
89
|
receipt.task_id ?? null,
|
|
85
90
|
receipt.payment?.rail ?? null,
|
|
86
91
|
receipt.payment?.ref ?? null,
|
|
@@ -96,7 +101,204 @@ function canonicalIssuerPayload(receipt) {
|
|
|
96
101
|
receipt.route?.provider ?? null,
|
|
97
102
|
receipt.output?.hash ?? null,
|
|
98
103
|
receipt.binding?.expected_commitment ?? null,
|
|
99
|
-
]
|
|
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
|
+
};
|
|
100
302
|
}
|
|
101
303
|
/**
|
|
102
304
|
* Verify a receipt's ES256 issuer signature against a JWK (public key).
|
|
@@ -146,59 +348,122 @@ function verifyIssuerSignature(receipt, jwk) {
|
|
|
146
348
|
* @param receipt - Receipt JSON with issuer_signature
|
|
147
349
|
* @param jwks - JWKS with keys array
|
|
148
350
|
*/
|
|
149
|
-
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 = {}) {
|
|
150
368
|
const sig = receipt.issuer_signature;
|
|
151
369
|
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
|
-
};
|
|
370
|
+
return { checked: false, valid: false, key_trusted: false, reason: 'no_issuer_signature' };
|
|
163
371
|
}
|
|
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));
|
|
164
377
|
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) {
|
|
378
|
+
const fromJwks = jwksCandidates(jwks, kid);
|
|
379
|
+
for (const jwk of fromJwks) {
|
|
175
380
|
const jwsResult = (0, jws_js_1.verifyIssuerJws)(sig.jws, jwk);
|
|
176
381
|
if (jwsResult.valid) {
|
|
177
|
-
return {
|
|
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
|
+
};
|
|
178
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
|
+
};
|
|
179
429
|
}
|
|
180
|
-
return {
|
|
430
|
+
return {
|
|
431
|
+
checked: true,
|
|
432
|
+
valid: false,
|
|
433
|
+
key_trusted: fromJwks.length > 0 || pinned,
|
|
434
|
+
kid,
|
|
435
|
+
reason: 'signature_invalid',
|
|
436
|
+
};
|
|
181
437
|
}
|
|
182
438
|
if (!sig.value) {
|
|
183
|
-
return { checked: false, valid: false, reason: 'no_issuer_signature' };
|
|
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
|
+
};
|
|
184
449
|
}
|
|
185
|
-
if (
|
|
186
|
-
return { checked: false, valid: false, reason: 'empty_jwks' };
|
|
450
|
+
if (jwks.keys.length === 0) {
|
|
451
|
+
return { checked: false, valid: false, key_trusted: false, kid: sig.kid, reason: 'empty_jwks' };
|
|
187
452
|
}
|
|
188
453
|
// Find matching key by kid, or try all ES256 keys if no kid on signature
|
|
189
454
|
const candidates = sig.kid
|
|
190
455
|
? jwks.keys.filter(k => k.kid === sig.kid && k.alg === 'ES256')
|
|
191
456
|
: jwks.keys.filter(k => k.alg === 'ES256');
|
|
192
457
|
if (candidates.length === 0) {
|
|
193
|
-
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' };
|
|
194
459
|
}
|
|
195
460
|
for (const jwk of candidates) {
|
|
196
461
|
const result = verifyIssuerSignature(receipt, jwk);
|
|
197
462
|
if (result.valid) {
|
|
198
|
-
return result;
|
|
463
|
+
return { ...result, key_trusted: true, trust: 'jwks', kid: jwk.kid || sig.kid };
|
|
199
464
|
}
|
|
200
465
|
}
|
|
201
|
-
return { checked: true, valid: false, reason: 'signature_invalid' };
|
|
466
|
+
return { checked: true, valid: false, key_trusted: true, kid: sig.kid, reason: 'signature_invalid' };
|
|
202
467
|
}
|
|
203
468
|
/**
|
|
204
469
|
* Verify a receipt's payment binding locally (no network required).
|
|
@@ -206,8 +471,18 @@ function verifyIssuerSignatureWithJwks(receipt, jwks) {
|
|
|
206
471
|
* This recomputes the commitment from the receipt's fields and compares
|
|
207
472
|
* it to the stored `binding.expected_commitment`.
|
|
208
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
|
+
}
|
|
209
483
|
function verifyBinding(receipt) {
|
|
210
484
|
const binding = receipt.binding;
|
|
485
|
+
const railHint = binding?.rail || receipt.payment?.rail || null;
|
|
211
486
|
if (!binding) {
|
|
212
487
|
return {
|
|
213
488
|
verified: false,
|
|
@@ -215,7 +490,17 @@ function verifyBinding(receipt) {
|
|
|
215
490
|
recomputed: null,
|
|
216
491
|
matches: false,
|
|
217
492
|
covers: [],
|
|
218
|
-
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),
|
|
219
504
|
};
|
|
220
505
|
}
|
|
221
506
|
const paymentRef = receipt.payment?.ref ?? null;
|
|
@@ -328,89 +613,341 @@ exports.canonicalSignedPayload = canonicalIssuerPayload;
|
|
|
328
613
|
function hashCanonicalPayload(receipt) {
|
|
329
614
|
return (0, ethers_1.keccak256)((0, ethers_1.toUtf8Bytes)((0, exports.canonicalSignedPayload)(receipt)));
|
|
330
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
|
+
}
|
|
331
632
|
/**
|
|
332
|
-
*
|
|
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
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Full receipt verification — trusted issuer signature, signed-claim facts,
|
|
802
|
+
* optional on-chain payer and nullifier checks.
|
|
333
803
|
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* @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.
|
|
338
807
|
*/
|
|
339
|
-
/** Extract JWS claims for payer binding
|
|
808
|
+
/** Extract JWS claims for payer binding. Prefers the JWS payload over the outer copy. */
|
|
340
809
|
function receiptPayerClaims(receipt) {
|
|
341
810
|
return (0, payer_js_1.receiptPayerClaimsFromEnvelope)(receipt);
|
|
342
811
|
}
|
|
343
812
|
async function verifyReceipt(receipt, options = {}) {
|
|
344
813
|
const errors = [];
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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}`);
|
|
363
834
|
}
|
|
364
835
|
}
|
|
365
|
-
|
|
366
|
-
|
|
836
|
+
let issuer_signature;
|
|
837
|
+
if (receipt.issuer_signature?.jws || receipt.issuer_signature?.value) {
|
|
838
|
+
issuer_signature = verifyIssuerSignatureWithJwks(receipt, jwks, { trustedKids });
|
|
367
839
|
if (issuer_signature.checked && !issuer_signature.valid) {
|
|
368
|
-
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}`);
|
|
369
843
|
}
|
|
370
844
|
}
|
|
371
845
|
else if (receipt.issuer_signature) {
|
|
372
846
|
issuer_signature = {
|
|
373
847
|
checked: false,
|
|
374
848
|
valid: false,
|
|
849
|
+
key_trusted: false,
|
|
375
850
|
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',
|
|
851
|
+
reason: 'JWKS not provided — pass jwks option to verify issuer signature',
|
|
379
852
|
};
|
|
380
853
|
}
|
|
381
854
|
else {
|
|
382
855
|
issuer_signature = {
|
|
383
856
|
checked: false,
|
|
384
857
|
valid: false,
|
|
858
|
+
key_trusted: false,
|
|
385
859
|
reason: 'No issuer signature present on receipt',
|
|
386
860
|
};
|
|
387
861
|
}
|
|
388
|
-
|
|
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;
|
|
389
895
|
let payer;
|
|
390
896
|
if (options.checkPayer) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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) {
|
|
397
927
|
payer = {
|
|
398
|
-
checked:
|
|
399
|
-
valid:
|
|
400
|
-
|
|
401
|
-
|
|
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',
|
|
402
935
|
};
|
|
403
|
-
if (payerResult.checked && !payerResult.valid) {
|
|
404
|
-
errors.push(`Payer binding mismatch: ${payerResult.reason}`);
|
|
405
|
-
}
|
|
406
936
|
}
|
|
407
|
-
else if (receipt.caller_binding?.payer_wallet && receipt.payment?.ref) {
|
|
937
|
+
else if (!receipt.issuer_signature?.jws && receipt.caller_binding?.payer_wallet && receipt.payment?.ref) {
|
|
408
938
|
payer = {
|
|
409
939
|
checked: false,
|
|
410
940
|
valid: false,
|
|
411
941
|
reason: 'On-chain payer check not requested — pass checkPayer: true',
|
|
412
942
|
};
|
|
413
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
|
+
}
|
|
414
951
|
else {
|
|
415
952
|
payer = {
|
|
416
953
|
checked: false,
|
|
@@ -418,7 +955,6 @@ async function verifyReceipt(receipt, options = {}) {
|
|
|
418
955
|
reason: 'No payer_wallet or payment.ref to verify',
|
|
419
956
|
};
|
|
420
957
|
}
|
|
421
|
-
// Verify nullifier on-chain if requested
|
|
422
958
|
let nullifier;
|
|
423
959
|
if (options.checkNullifier && receipt.proof?.nullifier) {
|
|
424
960
|
nullifier = await verifyNullifier(receipt, {
|
|
@@ -439,52 +975,39 @@ async function verifyReceipt(receipt, options = {}) {
|
|
|
439
975
|
: 'On-chain check not requested',
|
|
440
976
|
};
|
|
441
977
|
}
|
|
442
|
-
|
|
443
|
-
const hub = receipt.route?.provider ?? null;
|
|
444
|
-
const model = receipt.route?.model ?? null;
|
|
445
|
-
const amount_usdc = receipt.payment?.gross_amount ?? null;
|
|
446
|
-
const tx = receipt.payment?.ref ?? null;
|
|
447
|
-
const output_hash = receipt.output?.hash ?? null;
|
|
448
|
-
// Determine overall status with strict signature semantics:
|
|
449
|
-
// - If JWKS provided and signature invalid/kid mismatch/no matching key → 'failed'
|
|
450
|
-
// - If receipt has issuer_signature but no JWKS provided → 'partial' (cannot verify)
|
|
451
|
-
// - Unsigned receipts can be 'partial' or 'verified' based on binding
|
|
452
|
-
let overall;
|
|
453
|
-
// Check for signature verification failure (JWKS provided but signature invalid)
|
|
454
|
-
const jwksProvided = !!options.jwks;
|
|
978
|
+
const facts = factsFromClaims(verifiedClaims);
|
|
455
979
|
const hasIssuerSig = !!receipt.issuer_signature;
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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) {
|
|
463
995
|
overall = 'failed';
|
|
464
996
|
}
|
|
465
|
-
else if (
|
|
466
|
-
// Receipt has signature but no offline key material → partial
|
|
997
|
+
else if (signatureUnchecked) {
|
|
467
998
|
overall = 'partial';
|
|
468
999
|
}
|
|
469
|
-
else if (binding.
|
|
470
|
-
&& (!options.checkPayer || payer.valid)
|
|
471
|
-
&& (!options.checkNullifier || nullifier.anchored)) {
|
|
472
|
-
// Binding matches, signature OK (or no signature), nullifier OK → verified
|
|
473
|
-
overall = 'verified';
|
|
474
|
-
}
|
|
475
|
-
else if (binding.matches
|
|
476
|
-
|| issuer_signature.valid
|
|
477
|
-
|| (options.checkPayer && payer.valid)
|
|
478
|
-
|| (nullifier.verified && nullifier.anchored)) {
|
|
479
|
-
// At least one check passed → partial
|
|
1000
|
+
else if (!binding.expected && !hasIssuerSig && !options.checkPayer) {
|
|
480
1001
|
overall = 'partial';
|
|
481
1002
|
}
|
|
482
|
-
else if (
|
|
483
|
-
|
|
484
|
-
|
|
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';
|
|
485
1008
|
}
|
|
486
1009
|
else {
|
|
487
|
-
overall = '
|
|
1010
|
+
overall = 'partial';
|
|
488
1011
|
}
|
|
489
1012
|
return {
|
|
490
1013
|
receipt_id: receipt.task_id,
|
|
@@ -492,11 +1015,12 @@ async function verifyReceipt(receipt, options = {}) {
|
|
|
492
1015
|
issuer_signature,
|
|
493
1016
|
payer,
|
|
494
1017
|
nullifier,
|
|
495
|
-
output_hash,
|
|
496
|
-
hub,
|
|
497
|
-
model,
|
|
498
|
-
amount_usdc,
|
|
499
|
-
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,
|
|
500
1024
|
overall,
|
|
501
1025
|
errors,
|
|
502
1026
|
};
|
|
@@ -504,6 +1028,12 @@ async function verifyReceipt(receipt, options = {}) {
|
|
|
504
1028
|
var jws_js_2 = require("./jws.js");
|
|
505
1029
|
Object.defineProperty(exports, "resolvePinnedIssuerJwk", { enumerable: true, get: function () { return jws_js_2.resolvePinnedIssuerJwk; } });
|
|
506
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; } });
|
|
507
1037
|
exports.default = {
|
|
508
1038
|
verifyBinding,
|
|
509
1039
|
verifyIssuerSignature,
|
|
@@ -521,7 +1051,10 @@ exports.default = {
|
|
|
521
1051
|
computePaymentCommitment: binding_js_1.computePaymentCommitment,
|
|
522
1052
|
computeInferenceBinding: binding_js_1.computeInferenceBinding,
|
|
523
1053
|
canonicalIssuerPayload,
|
|
1054
|
+
canonicalPayloadVersion,
|
|
524
1055
|
canonicalSignedPayload: exports.canonicalSignedPayload,
|
|
1056
|
+
reconcileSettledTransfer,
|
|
1057
|
+
sumUsdcTransfersToPayee,
|
|
525
1058
|
hashCanonicalPayload,
|
|
526
1059
|
ZK_VERIFIER_ADDRESS: exports.ZK_VERIFIER_ADDRESS,
|
|
527
1060
|
BASE_RPC_URL: base_payer_js_1.BASE_RPC_URL,
|