uvd-x402-sdk 2.95.0 → 2.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -5
- package/dist/adapters/index.d.mts +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +2 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/index.mjs +2 -1
- package/dist/adapters/index.mjs.map +1 -1
- package/dist/backend/index.d.mts +3 -2
- package/dist/backend/index.d.ts +3 -2
- package/dist/backend/index.js +498 -30
- package/dist/backend/index.js.map +1 -1
- package/dist/backend/index.mjs +475 -31
- package/dist/backend/index.mjs.map +1 -1
- package/dist/{index-CF2HBEAJ.d.mts → index--bevvYU9.d.ts} +3 -1
- package/dist/{index-CkHu2s9r.d.ts → index-B2i1mGpW.d.mts} +3 -1
- package/dist/{index-DnxubrYX.d.mts → index-BsKsZzP3.d.mts} +1 -1
- package/dist/{index-DnxubrYX.d.ts → index-BsKsZzP3.d.ts} +1 -1
- package/dist/{index-CIUGPbu8.d.mts → index-ClOAmprt.d.mts} +232 -18
- package/dist/{index-DTbVW7EU.d.ts → index-DNTFp6WT.d.ts} +232 -18
- package/dist/index.d.mts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +512 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +489 -44
- package/dist/index.mjs.map +1 -1
- package/dist/providers/algorand/index.d.mts +1 -1
- package/dist/providers/algorand/index.d.ts +1 -1
- package/dist/providers/algorand/index.js +2 -1
- package/dist/providers/algorand/index.js.map +1 -1
- package/dist/providers/algorand/index.mjs +2 -1
- package/dist/providers/algorand/index.mjs.map +1 -1
- package/dist/providers/evm/index.d.mts +1 -1
- package/dist/providers/evm/index.d.ts +1 -1
- package/dist/providers/evm/index.js +2 -1
- package/dist/providers/evm/index.js.map +1 -1
- package/dist/providers/evm/index.mjs +2 -1
- package/dist/providers/evm/index.mjs.map +1 -1
- package/dist/providers/hedera/index.d.mts +1 -1
- package/dist/providers/hedera/index.d.ts +1 -1
- package/dist/providers/near/index.d.mts +1 -1
- package/dist/providers/near/index.d.ts +1 -1
- package/dist/providers/near/index.js +2 -1
- package/dist/providers/near/index.js.map +1 -1
- package/dist/providers/near/index.mjs +2 -1
- package/dist/providers/near/index.mjs.map +1 -1
- package/dist/providers/solana/index.d.mts +1 -1
- package/dist/providers/solana/index.d.ts +1 -1
- package/dist/providers/solana/index.js +2 -1
- package/dist/providers/solana/index.js.map +1 -1
- package/dist/providers/solana/index.mjs +2 -1
- package/dist/providers/solana/index.mjs.map +1 -1
- package/dist/providers/stellar/index.d.mts +1 -1
- package/dist/providers/stellar/index.d.ts +1 -1
- package/dist/providers/stellar/index.js +2 -1
- package/dist/providers/stellar/index.js.map +1 -1
- package/dist/providers/stellar/index.mjs +2 -1
- package/dist/providers/stellar/index.mjs.map +1 -1
- package/dist/providers/sui/index.d.mts +1 -1
- package/dist/providers/sui/index.d.ts +1 -1
- package/dist/providers/sui/index.js +2 -1
- package/dist/providers/sui/index.js.map +1 -1
- package/dist/providers/sui/index.mjs +2 -1
- package/dist/providers/sui/index.mjs.map +1 -1
- package/dist/providers/xrpl/index.d.mts +1 -1
- package/dist/providers/xrpl/index.d.ts +1 -1
- package/dist/providers/xrpl/index.js +2 -1
- package/dist/providers/xrpl/index.js.map +1 -1
- package/dist/providers/xrpl/index.mjs +2 -1
- package/dist/providers/xrpl/index.mjs.map +1 -1
- package/dist/react/index.d.mts +4 -3
- package/dist/react/index.d.ts +4 -3
- package/dist/react/index.js +153 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +153 -1
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/picker/index.d.mts +1 -1
- package/dist/react/picker/index.d.ts +1 -1
- package/dist/react/picker/index.js +2 -1
- package/dist/react/picker/index.js.map +1 -1
- package/dist/react/picker/index.mjs +2 -1
- package/dist/react/picker/index.mjs.map +1 -1
- package/dist/receipts-Co6C9FPO.d.ts +111 -0
- package/dist/receipts-D0zg-8uK.d.mts +111 -0
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +2 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs +2 -1
- package/dist/utils/index.mjs.map +1 -1
- package/dist/{validity-SD6nwegA.d.mts → validity-BcPTzuK8.d.mts} +1 -1
- package/dist/{validity-BrvXzCEF.d.ts → validity-CoCrj9pZ.d.ts} +1 -1
- package/package.json +1 -1
- package/src/backend/facilitator-error.ts +87 -2
- package/src/backend/index.ts +445 -41
- package/src/chains/index.ts +2 -1
- package/src/client/X402Client.ts +5 -0
- package/src/fixtures/facilitator-receipts-v1.json +299 -0
- package/src/index.ts +18 -0
- package/src/receipts.ts +237 -0
package/dist/backend/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var ed25519 = require('@noble/curves/ed25519');
|
|
4
|
+
var sha256 = require('@noble/hashes/sha256');
|
|
5
|
+
var utils = require('@noble/hashes/utils');
|
|
3
6
|
var ethers = require('ethers');
|
|
4
7
|
|
|
5
8
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
@@ -8,6 +11,205 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
8
11
|
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
9
12
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
10
13
|
});
|
|
14
|
+
var RECEIPT_ISSUER = "https://facilitator.ultravioletadao.xyz";
|
|
15
|
+
var encoder = new TextEncoder();
|
|
16
|
+
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
17
|
+
var digest = (value) => utils.bytesToHex(sha256.sha256(value));
|
|
18
|
+
function canonicalReceiptJson(value) {
|
|
19
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
|
20
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return String(value);
|
|
21
|
+
if (Array.isArray(value)) return `[${value.map(canonicalReceiptJson).join(",")}]`;
|
|
22
|
+
if (typeof value === "object" && value) {
|
|
23
|
+
const object = value;
|
|
24
|
+
const keys = Object.keys(object).sort();
|
|
25
|
+
if (keys.some((key) => !/^[\x00-\x7f]*$/.test(key))) throw new Error("non-ASCII canonical key");
|
|
26
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalReceiptJson(object[key])}`).join(",")}}`;
|
|
27
|
+
}
|
|
28
|
+
throw new Error("unsupported canonical value");
|
|
29
|
+
}
|
|
30
|
+
function receiptCommitment(domain, value) {
|
|
31
|
+
return digest(encoder.encode(`${domain}
|
|
32
|
+
${canonicalReceiptJson(value)}`));
|
|
33
|
+
}
|
|
34
|
+
function encode64(bytes) {
|
|
35
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
|
|
36
|
+
return btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join(""));
|
|
37
|
+
}
|
|
38
|
+
function decode64(value) {
|
|
39
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) throw new Error("invalid base64");
|
|
40
|
+
if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(value, "base64"));
|
|
41
|
+
return Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
|
|
42
|
+
}
|
|
43
|
+
function decodeUrl64(value) {
|
|
44
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
45
|
+
return decode64(padded + "=".repeat((4 - padded.length % 4) % 4));
|
|
46
|
+
}
|
|
47
|
+
function parseFacilitatorReceipt(value) {
|
|
48
|
+
if (value === null || value === void 0) return null;
|
|
49
|
+
const r = value;
|
|
50
|
+
if (r.schemaVersion !== 1 || !["verified", "pending", "confirmed", "rejected", "unknown"].includes(r.status)) throw new Error("unsupported receipt version or state");
|
|
51
|
+
for (const key of ["receiptId", "issuer", "operation", "network", "scheme", "asset", "amount", "payTo", "paymentRequestHash", "authorizationId"]) {
|
|
52
|
+
if (typeof r[key] !== "string" || !r[key]) throw new Error(`invalid receipt ${key}`);
|
|
53
|
+
}
|
|
54
|
+
if (!Number.isSafeInteger(r.revision) || r.revision < 1 || !r.request || !r.retry) throw new Error("incomplete receipt");
|
|
55
|
+
if (r.requestHashVersion !== "uvd-x402-request-v1" || r.requestHash !== receiptCommitment(r.requestHashVersion, r.request)) throw new Error("receipt request hash mismatch");
|
|
56
|
+
for (const key of ["network", "scheme", "asset", "amount", "payTo", "purchaseId"]) {
|
|
57
|
+
if (r[key] !== r.request[key]) throw new Error("receipt terms mismatch");
|
|
58
|
+
}
|
|
59
|
+
if (!/^[0-9]+$/.test(r.amount)) throw new Error("receipt amount must be an atomic integer string");
|
|
60
|
+
if (r.status === "confirmed" && (r.operation !== "settle" || !r.settlement?.id)) throw new Error("confirmation has no settlement");
|
|
61
|
+
return r;
|
|
62
|
+
}
|
|
63
|
+
function verifyFacilitatorReceipt(receipt, jwks, issuer = RECEIPT_ISSUER) {
|
|
64
|
+
try {
|
|
65
|
+
parseFacilitatorReceipt(receipt);
|
|
66
|
+
if (receipt.issuer !== issuer || receipt.proof?.type !== "jws" || !receipt.proof.jws) return false;
|
|
67
|
+
const parts = receipt.proof.jws.split(".");
|
|
68
|
+
if (parts.length !== 3) return false;
|
|
69
|
+
const [protectedHeader, payload, signature] = parts;
|
|
70
|
+
const header = JSON.parse(decoder.decode(decodeUrl64(protectedHeader)));
|
|
71
|
+
if (header.alg !== "EdDSA" || header.typ !== "uvd-facilitator-receipt+jws") return false;
|
|
72
|
+
const keys = jwks.keys.filter((k) => k.kid === header.kid && k.kty === "OKP" && k.crv === "Ed25519");
|
|
73
|
+
if (keys.length !== 1 || !keys[0].x) return false;
|
|
74
|
+
if (decoder.decode(decodeUrl64(payload)) !== canonicalReceiptJson({ ...receipt, proof: null })) return false;
|
|
75
|
+
return ed25519.ed25519.verify(decodeUrl64(signature), encoder.encode(`${protectedHeader}.${payload}`), decodeUrl64(keys[0].x));
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function createPurchaseContext() {
|
|
81
|
+
return { purchaseId: utils.bytesToHex(utils.randomBytes(16)), accessToken: utils.bytesToHex(utils.randomBytes(32)) };
|
|
82
|
+
}
|
|
83
|
+
function purchaseContextHeader(context) {
|
|
84
|
+
return encode64(encoder.encode(canonicalReceiptJson({
|
|
85
|
+
purchaseId: context.purchaseId,
|
|
86
|
+
accessToken: context.accessToken,
|
|
87
|
+
method: context.method,
|
|
88
|
+
url: context.url,
|
|
89
|
+
bodySha256: context.bodySha256
|
|
90
|
+
})));
|
|
91
|
+
}
|
|
92
|
+
function validatePurchaseContext(header, method, url, body) {
|
|
93
|
+
if (!header) return void 0;
|
|
94
|
+
if (header.length > 8192) throw new Error("invalid purchase context");
|
|
95
|
+
const context = JSON.parse(decoder.decode(decode64(header)));
|
|
96
|
+
if (typeof context.purchaseId !== "string" || !context.purchaseId || context.purchaseId.length > 128 || typeof context.accessToken !== "string" || !/^[0-9a-f]{64}$/.test(context.accessToken) || context.method !== method.toUpperCase() || context.url !== new URL(url).toString() || context.bodySha256 !== digest(body)) throw new Error("purchase context does not match the HTTP request");
|
|
97
|
+
return header;
|
|
98
|
+
}
|
|
99
|
+
function paymentResponseHeaders(result) {
|
|
100
|
+
const encoded = encode64(encoder.encode(JSON.stringify(result)));
|
|
101
|
+
if (encoded.length > 32768) throw new Error("payment response too large");
|
|
102
|
+
return {
|
|
103
|
+
"PAYMENT-RESPONSE": encoded,
|
|
104
|
+
"X-PAYMENT-RESPONSE": encoded,
|
|
105
|
+
"Access-Control-Expose-Headers": "PAYMENT-RESPONSE, X-PAYMENT-RESPONSE",
|
|
106
|
+
"Cache-Control": "no-store"
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
var LIST_HEADERS = ["Access-Control-Expose-Headers", "Cache-Control"];
|
|
110
|
+
function mergeHeaderList(existing, additions) {
|
|
111
|
+
const name = (entry) => entry.split("=")[0].trim().toLowerCase();
|
|
112
|
+
const kept = (existing || "").split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
113
|
+
const seen = new Set(kept.map(name));
|
|
114
|
+
for (const entry of additions.split(",").map((value) => value.trim()).filter(Boolean)) {
|
|
115
|
+
if (!seen.has(name(entry))) {
|
|
116
|
+
kept.push(entry);
|
|
117
|
+
seen.add(name(entry));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return kept.join(", ");
|
|
121
|
+
}
|
|
122
|
+
function mergePaymentResponseHeaders(result, current) {
|
|
123
|
+
const headers = paymentResponseHeaders(result);
|
|
124
|
+
for (const name of LIST_HEADERS) headers[name] = mergeHeaderList(current(name), headers[name]);
|
|
125
|
+
return headers;
|
|
126
|
+
}
|
|
127
|
+
function receiptFromResponse(response) {
|
|
128
|
+
const values = [response.headers.get("PAYMENT-RESPONSE"), response.headers.get("X-PAYMENT-RESPONSE")].filter((v) => v !== null);
|
|
129
|
+
if (!values.length) return null;
|
|
130
|
+
if (new Set(values).size !== 1 || values[0].includes(",") || values[0].length > 32768) throw new Error("ambiguous or oversized payment response header");
|
|
131
|
+
return parseFacilitatorReceipt(JSON.parse(decoder.decode(decode64(values[0]))).receipt);
|
|
132
|
+
}
|
|
133
|
+
function receiptFromErrorBody(body) {
|
|
134
|
+
try {
|
|
135
|
+
return parseFacilitatorReceipt(JSON.parse(body).receipt);
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function fetchWithReceipt(fetchClient, url, options) {
|
|
141
|
+
const { context, persist, trustedKeys, issuer = RECEIPT_ISSUER } = options;
|
|
142
|
+
const transport = options.fetchImpl || globalThis.fetch;
|
|
143
|
+
const method = (options.method || "GET").toUpperCase();
|
|
144
|
+
const request = new Request(url, { ...options.init, method });
|
|
145
|
+
const bytes = new Uint8Array(await request.arrayBuffer());
|
|
146
|
+
const bodyHash = digest(bytes);
|
|
147
|
+
if (context.method && (context.method !== method || context.url !== request.url || context.bodySha256 !== bodyHash)) throw new Error("purchase context cannot be reused for another HTTP request");
|
|
148
|
+
context.method = method;
|
|
149
|
+
context.url = request.url;
|
|
150
|
+
context.bodySha256 = bodyHash;
|
|
151
|
+
const init = { ...options.init, method, headers: new Headers(request.headers), body: ["GET", "HEAD"].includes(method) ? void 0 : bytes };
|
|
152
|
+
let receipt = parseFacilitatorReceipt(context.receipt);
|
|
153
|
+
let transportError;
|
|
154
|
+
const persistCopy = async () => {
|
|
155
|
+
await persist(JSON.parse(JSON.stringify(context)));
|
|
156
|
+
};
|
|
157
|
+
const send = async (input, sendOptions) => {
|
|
158
|
+
const headers = new Headers(sendOptions?.headers);
|
|
159
|
+
const paid = headers.get("X-PAYMENT") || headers.get("PAYMENT-SIGNATURE");
|
|
160
|
+
if (paid) {
|
|
161
|
+
const paymentHeaders = {};
|
|
162
|
+
for (const name of ["X-PAYMENT", "PAYMENT-SIGNATURE"]) {
|
|
163
|
+
const value = headers.get(name);
|
|
164
|
+
if (value) paymentHeaders[name] = value;
|
|
165
|
+
}
|
|
166
|
+
if (context.paymentHeaders && JSON.stringify(context.paymentHeaders) !== JSON.stringify(paymentHeaders)) throw new Error("refusing a new authorization for an existing purchase");
|
|
167
|
+
context.paymentHeaders = paymentHeaders;
|
|
168
|
+
headers.set("X-UVD-Purchase", purchaseContextHeader(context));
|
|
169
|
+
await persistCopy();
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
return await transport(input, { ...sendOptions, headers });
|
|
173
|
+
} catch (error) {
|
|
174
|
+
transportError = error;
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
try {
|
|
179
|
+
let response;
|
|
180
|
+
if (context.paymentHeaders) {
|
|
181
|
+
const headers = new Headers(init.headers);
|
|
182
|
+
for (const [name, value] of Object.entries(context.paymentHeaders)) headers.set(name, value);
|
|
183
|
+
response = await send(request.url, { ...init, headers });
|
|
184
|
+
} else {
|
|
185
|
+
response = await fetchClient(request.url, { ...options, method, init, fetchImpl: send });
|
|
186
|
+
}
|
|
187
|
+
const incoming = receiptFromResponse(response);
|
|
188
|
+
if (incoming) {
|
|
189
|
+
if (incoming.issuer !== issuer || Object.entries({ purchaseId: context.purchaseId, method, url: context.url, bodySha256: bodyHash }).some(([key, value]) => incoming.request[key] !== value)) throw new Error("receipt does not bind this purchase");
|
|
190
|
+
if (receipt?.operation === "settle" && (incoming.receiptId !== receipt.receiptId || incoming.revision < receipt.revision)) throw new Error("receipt revision conflict");
|
|
191
|
+
if (trustedKeys && !verifyFacilitatorReceipt(incoming, trustedKeys, issuer)) throw new Error("receipt signature verification failed");
|
|
192
|
+
receipt = incoming;
|
|
193
|
+
context.receipt = incoming;
|
|
194
|
+
await persistCopy();
|
|
195
|
+
}
|
|
196
|
+
const proofVerified = !!(receipt && trustedKeys && verifyFacilitatorReceipt(receipt, trustedKeys, issuer));
|
|
197
|
+
if (receipt && trustedKeys && !proofVerified) throw new Error("receipt signature verification failed");
|
|
198
|
+
return { response, receipt, paymentState: receipt ? receipt.status : context.paymentHeaders ? "unknown" : "not_required", context, proofVerified };
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (!transportError) throw error;
|
|
201
|
+
return { response: null, receipt, paymentState: "unknown", context, proofVerified: false, error };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function getFacilitatorReceipt(receiptId, context, options = {}) {
|
|
205
|
+
if (!/^[0-9a-f-]{36}$/.test(receiptId)) throw new Error("invalid receipt ID");
|
|
206
|
+
const issuer = options.issuer || RECEIPT_ISSUER;
|
|
207
|
+
const response = await (options.fetchImpl || globalThis.fetch)(`${issuer}/receipts/${receiptId}`, { headers: { Authorization: `Bearer ${context.accessToken}` } });
|
|
208
|
+
if (!response.ok) throw new Error(`receipt lookup failed: ${response.status}`);
|
|
209
|
+
const receipt = parseFacilitatorReceipt((await response.json()).receipt);
|
|
210
|
+
if (!receipt || receipt.issuer !== issuer || receipt.purchaseId !== context.purchaseId) throw new Error("receipt lookup mismatch");
|
|
211
|
+
return receipt;
|
|
212
|
+
}
|
|
11
213
|
|
|
12
214
|
// src/utils/base64.ts
|
|
13
215
|
function decodeBase64Utf8(encoded) {
|
|
@@ -809,7 +1011,8 @@ var SUPPORTED_CHAINS = {
|
|
|
809
1011
|
// Pinned by src/arc-testnet.test.ts.
|
|
810
1012
|
//
|
|
811
1013
|
// Mainnet (5042) and testnet (5042002) were verified independently against
|
|
812
|
-
// their official RPCs on 2026-09-16. EURC domains checked on 2026-09-17;
|
|
1014
|
+
// their official RPCs on 2026-09-16. EURC domains checked on 2026-09-17; funded EURC payments settled on Arc mainnet on
|
|
1015
|
+
// 2026-09-22 (x402 v1/v2, docs/networks/arc.md); testnet pending.
|
|
813
1016
|
arc: {
|
|
814
1017
|
chainId: 5042,
|
|
815
1018
|
chainIdHex: "0x13b2",
|
|
@@ -1524,6 +1727,9 @@ var REPLAYABLE_LEASE_REASONS = [
|
|
|
1524
1727
|
];
|
|
1525
1728
|
var AMBIGUOUS_LEASE_REASONS = ["forward_failed"];
|
|
1526
1729
|
var SETTLEMENT_UNCONFIRMED = "settlement_unconfirmed";
|
|
1730
|
+
var AUTHORIZATION_ALREADY_SETTLED = "authorization_already_settled";
|
|
1731
|
+
var AUTHORIZATION_IN_FLIGHT = "authorization_in_flight";
|
|
1732
|
+
var RECEIPT_REQUEST_CONFLICT = "receipt_request_conflict";
|
|
1527
1733
|
var MAX_RETRY_AFTER_SECONDS = 15;
|
|
1528
1734
|
var DEFAULT_RETRY_AFTER_SECONDS = 5;
|
|
1529
1735
|
var DEFAULT_FACILITATOR_RETRIES = 2;
|
|
@@ -1584,6 +1790,12 @@ function isDeclaredUnretryable(parsed) {
|
|
|
1584
1790
|
function isSettlementUnconfirmed(failure) {
|
|
1585
1791
|
return failure.errorCode === SETTLEMENT_UNCONFIRMED;
|
|
1586
1792
|
}
|
|
1793
|
+
function isAuthorizationAlreadyUsed(failure) {
|
|
1794
|
+
return failure.errorCode === AUTHORIZATION_ALREADY_SETTLED || failure.errorCode === RECEIPT_REQUEST_CONFLICT;
|
|
1795
|
+
}
|
|
1796
|
+
function isAuthorizationInFlight(failure) {
|
|
1797
|
+
return failure.errorCode === AUTHORIZATION_IN_FLIGHT;
|
|
1798
|
+
}
|
|
1587
1799
|
async function readFacilitatorError(response) {
|
|
1588
1800
|
let body = "";
|
|
1589
1801
|
try {
|
|
@@ -1595,7 +1807,8 @@ async function readFacilitatorError(response) {
|
|
|
1595
1807
|
const parsed = parseFacilitatorErrorBody(body);
|
|
1596
1808
|
const reason = parsed.reason;
|
|
1597
1809
|
const transportRetryable = status === 429 || status === 502 || status === 503 || status === 504;
|
|
1598
|
-
const
|
|
1810
|
+
const inFlight = status === 409 && parsed.errorCode === AUTHORIZATION_IN_FLIGHT;
|
|
1811
|
+
const retryable = inFlight || transportRetryable && !isDeclaredUnretryable(parsed);
|
|
1599
1812
|
const retryAfterSeconds = retryable ? parseRetryAfterSeconds(response) ?? DEFAULT_RETRY_AFTER_SECONDS : void 0;
|
|
1600
1813
|
const safeToReplay = retryable && (status === 429 || status === 503 && isReplayableLeaseReason(reason));
|
|
1601
1814
|
return {
|
|
@@ -1853,7 +2066,7 @@ function buildSettleRequestForVersion(paymentHeader, requirements, version) {
|
|
|
1853
2066
|
return buildSettleRequest(paymentHeader, requirements);
|
|
1854
2067
|
}
|
|
1855
2068
|
var X402_CORS_HEADERS = {
|
|
1856
|
-
"Access-Control-Allow-Headers": "Content-Type, X-PAYMENT, PAYMENT-SIGNATURE, Authorization",
|
|
2069
|
+
"Access-Control-Allow-Headers": "Content-Type, X-PAYMENT, PAYMENT-SIGNATURE, X-UVD-Purchase, Authorization",
|
|
1857
2070
|
"Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE, PAYMENT-RESPONSE, PAYMENT-REQUIRED",
|
|
1858
2071
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS"
|
|
1859
2072
|
};
|
|
@@ -1862,7 +2075,8 @@ var X402_HEADER_NAMES = [
|
|
|
1862
2075
|
"PAYMENT-SIGNATURE",
|
|
1863
2076
|
"X-PAYMENT-RESPONSE",
|
|
1864
2077
|
"PAYMENT-RESPONSE",
|
|
1865
|
-
"PAYMENT-REQUIRED"
|
|
2078
|
+
"PAYMENT-REQUIRED",
|
|
2079
|
+
"X-UVD-Purchase"
|
|
1866
2080
|
];
|
|
1867
2081
|
function getCorsHeaders(origin = "*") {
|
|
1868
2082
|
return {
|
|
@@ -1870,6 +2084,78 @@ function getCorsHeaders(origin = "*") {
|
|
|
1870
2084
|
...X402_CORS_HEADERS
|
|
1871
2085
|
};
|
|
1872
2086
|
}
|
|
2087
|
+
var IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
|
|
2088
|
+
var IDEMPOTENT_REPLAYED_HEADER = "Idempotent-Replayed";
|
|
2089
|
+
function createIdempotencyKey() {
|
|
2090
|
+
return `x402-${utils.bytesToHex(utils.randomBytes(32))}`;
|
|
2091
|
+
}
|
|
2092
|
+
function checkIdempotencyKey(key) {
|
|
2093
|
+
if (typeof key !== "string" || !/^[\x21-\x7e]{1,255}$/.test(key) || key.startsWith("receipt:")) {
|
|
2094
|
+
throw new Error('idempotencyKey must be 1-255 visible ASCII characters and must not start with "receipt:"');
|
|
2095
|
+
}
|
|
2096
|
+
return key;
|
|
2097
|
+
}
|
|
2098
|
+
function isReplayed(response) {
|
|
2099
|
+
try {
|
|
2100
|
+
return response?.headers?.get?.(IDEMPOTENT_REPLAYED_HEADER)?.trim().toLowerCase() === "true";
|
|
2101
|
+
} catch {
|
|
2102
|
+
return false;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
function refuseUnboundReplay(result) {
|
|
2106
|
+
if (!result.replayed) return result;
|
|
2107
|
+
const pending = result.receipt?.status === "pending" || result.receipt?.status === "unknown" || result.errorCode === "settlement_in_progress";
|
|
2108
|
+
if (!result.success && !pending) return result;
|
|
2109
|
+
const {
|
|
2110
|
+
transactionHash: _tx,
|
|
2111
|
+
proofOfPayment: _proof,
|
|
2112
|
+
paymentId: _id,
|
|
2113
|
+
transaction: _t,
|
|
2114
|
+
retryAfterSeconds,
|
|
2115
|
+
...rest
|
|
2116
|
+
} = result;
|
|
2117
|
+
if (result.success) {
|
|
2118
|
+
return {
|
|
2119
|
+
...rest,
|
|
2120
|
+
success: false,
|
|
2121
|
+
error: AUTHORIZATION_ALREADY_SETTLED,
|
|
2122
|
+
errorCode: AUTHORIZATION_ALREADY_SETTLED,
|
|
2123
|
+
retryable: false,
|
|
2124
|
+
safeToReplay: false
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
return {
|
|
2128
|
+
...rest,
|
|
2129
|
+
success: false,
|
|
2130
|
+
error: AUTHORIZATION_IN_FLIGHT,
|
|
2131
|
+
errorCode: AUTHORIZATION_IN_FLIGHT,
|
|
2132
|
+
retryable: true,
|
|
2133
|
+
safeToReplay: false,
|
|
2134
|
+
retryAfterSeconds: retryAfterSeconds ?? DEFAULT_RETRY_AFTER_SECONDS
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
function facilitatorHeaders(idempotencyKey, receiptContext) {
|
|
2138
|
+
return {
|
|
2139
|
+
"Content-Type": "application/json",
|
|
2140
|
+
[IDEMPOTENCY_KEY_HEADER]: idempotencyKey,
|
|
2141
|
+
...receiptContext ? { "X-UVD-Purchase": receiptContext } : {}
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
function admittedVerdict(result) {
|
|
2145
|
+
if (result.isValid !== false) return {};
|
|
2146
|
+
if (result.invalidReason === AUTHORIZATION_IN_FLIGHT) {
|
|
2147
|
+
return {
|
|
2148
|
+
errorCode: AUTHORIZATION_IN_FLIGHT,
|
|
2149
|
+
retryable: true,
|
|
2150
|
+
safeToReplay: false,
|
|
2151
|
+
retryAfterSeconds: DEFAULT_RETRY_AFTER_SECONDS
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
if (result.invalidReason === AUTHORIZATION_ALREADY_SETTLED) {
|
|
2155
|
+
return { errorCode: AUTHORIZATION_ALREADY_SETTLED, retryable: false, safeToReplay: false };
|
|
2156
|
+
}
|
|
2157
|
+
return {};
|
|
2158
|
+
}
|
|
1873
2159
|
var FacilitatorClient = class {
|
|
1874
2160
|
baseUrl;
|
|
1875
2161
|
timeout;
|
|
@@ -1902,11 +2188,19 @@ var FacilitatorClient = class {
|
|
|
1902
2188
|
*
|
|
1903
2189
|
* Call this before providing the paid resource to validate the payment.
|
|
1904
2190
|
*
|
|
2191
|
+
* An authorization the facilitator already admitted for another request
|
|
2192
|
+
* comes back invalid with `errorCode` {@link AUTHORIZATION_ALREADY_SETTLED}
|
|
2193
|
+
* (do not serve, do not ask for a new signature) or
|
|
2194
|
+
* {@link AUTHORIZATION_IN_FLIGHT} (`retryable`: the same request learns the
|
|
2195
|
+
* verdict later). Neither is a rejected payment: never answer them with 402.
|
|
2196
|
+
*
|
|
1905
2197
|
* @param paymentHeader - Parsed x402 payment header
|
|
1906
2198
|
* @param requirements - Payment requirements
|
|
2199
|
+
* @param options - Purchase context and the payment's `idempotencyKey`
|
|
1907
2200
|
* @returns Verification result
|
|
1908
2201
|
*/
|
|
1909
|
-
async verify(paymentHeader, requirements) {
|
|
2202
|
+
async verify(paymentHeader, requirements, options = {}) {
|
|
2203
|
+
const idempotencyKey = checkIdempotencyKey(options.idempotencyKey ?? createIdempotencyKey());
|
|
1910
2204
|
const body = buildVerifyRequestForVersion(
|
|
1911
2205
|
paymentHeader,
|
|
1912
2206
|
requirements,
|
|
@@ -1917,7 +2211,7 @@ var FacilitatorClient = class {
|
|
|
1917
2211
|
`${this.baseUrl}/verify`,
|
|
1918
2212
|
{
|
|
1919
2213
|
method: "POST",
|
|
1920
|
-
headers:
|
|
2214
|
+
headers: facilitatorHeaders(idempotencyKey, options.receiptContext),
|
|
1921
2215
|
body: JSON.stringify(body)
|
|
1922
2216
|
},
|
|
1923
2217
|
{ timeoutMs: this.timeout, retries: this.retries }
|
|
@@ -1926,17 +2220,21 @@ var FacilitatorClient = class {
|
|
|
1926
2220
|
return {
|
|
1927
2221
|
isValid: false,
|
|
1928
2222
|
invalidReason: error.error,
|
|
1929
|
-
...failureFields(error)
|
|
2223
|
+
...failureFields(error),
|
|
2224
|
+
receipt: receiptFromErrorBody(error.body),
|
|
2225
|
+
idempotencyKey
|
|
1930
2226
|
};
|
|
1931
2227
|
}
|
|
1932
|
-
|
|
2228
|
+
const result = await response.json();
|
|
2229
|
+
return { ...result, ...admittedVerdict(result), receipt: parseFacilitatorReceipt(result.receipt), idempotencyKey };
|
|
1933
2230
|
} catch (error) {
|
|
1934
2231
|
return {
|
|
1935
2232
|
isValid: false,
|
|
1936
2233
|
invalidReason: error instanceof Error ? error.message : "Unknown error",
|
|
1937
2234
|
retryable: true,
|
|
1938
2235
|
safeToReplay: false,
|
|
1939
|
-
retryAfterSeconds: DEFAULT_RETRY_AFTER_SECONDS
|
|
2236
|
+
retryAfterSeconds: DEFAULT_RETRY_AFTER_SECONDS,
|
|
2237
|
+
idempotencyKey
|
|
1940
2238
|
};
|
|
1941
2239
|
}
|
|
1942
2240
|
}
|
|
@@ -1945,11 +2243,23 @@ var FacilitatorClient = class {
|
|
|
1945
2243
|
*
|
|
1946
2244
|
* Call this after providing the paid resource to execute the on-chain transfer.
|
|
1947
2245
|
*
|
|
2246
|
+
* Send the `idempotencyKey` the payment was verified with, and the same one
|
|
2247
|
+
* on any retry: it is the binding that earns the original answer back after
|
|
2248
|
+
* a lost response (`replayed: true`). Without it, or with another one, an
|
|
2249
|
+
* authorization already admitted comes back `409`
|
|
2250
|
+
* {@link AUTHORIZATION_ALREADY_SETTLED}, {@link AUTHORIZATION_IN_FLIGHT} or
|
|
2251
|
+
* {@link RECEIPT_REQUEST_CONFLICT} -- the X-PAYMENT was used; do not serve it
|
|
2252
|
+
* again. With no key and no context the call uses a fresh key, so a replay
|
|
2253
|
+
* it receives is refused the same way.
|
|
2254
|
+
*
|
|
1948
2255
|
* @param paymentHeader - Parsed x402 payment header
|
|
1949
2256
|
* @param requirements - Payment requirements
|
|
2257
|
+
* @param options - Purchase context and the payment's `idempotencyKey`
|
|
1950
2258
|
* @returns Settlement result with transaction hash
|
|
1951
2259
|
*/
|
|
1952
|
-
async settle(paymentHeader, requirements) {
|
|
2260
|
+
async settle(paymentHeader, requirements, options = {}) {
|
|
2261
|
+
const unbound = options.idempotencyKey === void 0 && !options.receiptContext;
|
|
2262
|
+
const idempotencyKey = checkIdempotencyKey(options.idempotencyKey ?? createIdempotencyKey());
|
|
1953
2263
|
const body = buildSettleRequestForVersion(
|
|
1954
2264
|
paymentHeader,
|
|
1955
2265
|
requirements,
|
|
@@ -1961,7 +2271,8 @@ var FacilitatorClient = class {
|
|
|
1961
2271
|
`${this.baseUrl}/settle`,
|
|
1962
2272
|
{
|
|
1963
2273
|
method: "POST",
|
|
1964
|
-
|
|
2274
|
+
// The same init, key included, goes out on every automatic retry.
|
|
2275
|
+
headers: facilitatorHeaders(idempotencyKey, options.receiptContext),
|
|
1965
2276
|
body: JSON.stringify(body)
|
|
1966
2277
|
},
|
|
1967
2278
|
{ timeoutMs: settleTimeout, retries: this.retries }
|
|
@@ -1970,7 +2281,9 @@ var FacilitatorClient = class {
|
|
|
1970
2281
|
return {
|
|
1971
2282
|
success: false,
|
|
1972
2283
|
error: error.error,
|
|
1973
|
-
...failureFields(error)
|
|
2284
|
+
...failureFields(error),
|
|
2285
|
+
receipt: receiptFromErrorBody(error.body),
|
|
2286
|
+
idempotencyKey
|
|
1974
2287
|
};
|
|
1975
2288
|
}
|
|
1976
2289
|
const result = await response.json();
|
|
@@ -1980,7 +2293,7 @@ var FacilitatorClient = class {
|
|
|
1980
2293
|
"[x402] settle reported success but carried no transaction hash under transaction/transactionHash/transaction_hash \u2014 treat delivery as unconfirmed"
|
|
1981
2294
|
);
|
|
1982
2295
|
}
|
|
1983
|
-
|
|
2296
|
+
const settled = {
|
|
1984
2297
|
// Read the facilitator's verdict instead of asserting one.
|
|
1985
2298
|
//
|
|
1986
2299
|
// This was the literal `true`, so a settle was reported successful
|
|
@@ -2008,15 +2321,24 @@ var FacilitatorClient = class {
|
|
|
2008
2321
|
// facilitator returns it, and this method threw it away — so a seller
|
|
2009
2322
|
// using the SDK end to end could only ever produce provisional anchors.
|
|
2010
2323
|
proofOfPayment: result.proofOfPayment ?? result.proof_of_payment,
|
|
2011
|
-
payer: result.payer
|
|
2324
|
+
payer: result.payer,
|
|
2325
|
+
receipt: parseFacilitatorReceipt(result.receipt),
|
|
2326
|
+
paymentId: result.paymentId,
|
|
2327
|
+
...result.receipt?.status === "unknown" || result.receipt?.status === "pending" ? { retryable: true, safeToReplay: false } : {},
|
|
2328
|
+
// `202 settlement_in_progress` and friends: a 2xx that names its state.
|
|
2329
|
+
...!result.success && typeof result.error === "string" && result.error ? { errorCode: result.error } : {},
|
|
2330
|
+
...isReplayed(response) ? { replayed: true } : {},
|
|
2331
|
+
idempotencyKey
|
|
2012
2332
|
};
|
|
2333
|
+
return unbound ? refuseUnboundReplay(settled) : settled;
|
|
2013
2334
|
} catch (error) {
|
|
2014
2335
|
return {
|
|
2015
2336
|
success: false,
|
|
2016
2337
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
2017
2338
|
retryable: true,
|
|
2018
2339
|
safeToReplay: false,
|
|
2019
|
-
retryAfterSeconds: DEFAULT_RETRY_AFTER_SECONDS
|
|
2340
|
+
retryAfterSeconds: DEFAULT_RETRY_AFTER_SECONDS,
|
|
2341
|
+
idempotencyKey
|
|
2020
2342
|
};
|
|
2021
2343
|
}
|
|
2022
2344
|
}
|
|
@@ -2026,27 +2348,41 @@ var FacilitatorClient = class {
|
|
|
2026
2348
|
* Convenience method that verifies first, then settles if valid.
|
|
2027
2349
|
* Use this for simple payment flows where you don't need custom logic between verify and settle.
|
|
2028
2350
|
*
|
|
2351
|
+
* Both steps carry ONE `Idempotency-Key` (`options.idempotencyKey`, or a
|
|
2352
|
+
* fresh one reported back as `idempotencyKey`). With a fresh key and no
|
|
2353
|
+
* purchase context, a replay of an earlier settle is not this purchase and
|
|
2354
|
+
* comes back `settled: false` with `errorCode`
|
|
2355
|
+
* {@link AUTHORIZATION_ALREADY_SETTLED}.
|
|
2356
|
+
*
|
|
2029
2357
|
* @param paymentHeader - Parsed x402 payment header
|
|
2030
2358
|
* @param requirements - Payment requirements
|
|
2359
|
+
* @param options - Purchase context and the payment's `idempotencyKey`
|
|
2031
2360
|
* @returns Combined result with verify and settle status
|
|
2032
2361
|
*/
|
|
2033
|
-
async verifyAndSettle(paymentHeader, requirements) {
|
|
2034
|
-
const
|
|
2362
|
+
async verifyAndSettle(paymentHeader, requirements, options = {}) {
|
|
2363
|
+
const unbound = options.idempotencyKey === void 0 && !options.receiptContext;
|
|
2364
|
+
const binding = { ...options, idempotencyKey: checkIdempotencyKey(options.idempotencyKey ?? createIdempotencyKey()) };
|
|
2365
|
+
const { idempotencyKey } = binding;
|
|
2366
|
+
const verifyResult = await this.verify(paymentHeader, requirements, binding);
|
|
2035
2367
|
if (!verifyResult.isValid) {
|
|
2036
2368
|
return {
|
|
2037
2369
|
verified: false,
|
|
2038
2370
|
settled: false,
|
|
2039
2371
|
error: verifyResult.invalidReason,
|
|
2040
|
-
...carryFailureFields(verifyResult)
|
|
2372
|
+
...carryFailureFields(verifyResult),
|
|
2373
|
+
idempotencyKey
|
|
2041
2374
|
};
|
|
2042
2375
|
}
|
|
2043
|
-
const
|
|
2376
|
+
const answered = await this.settle(paymentHeader, requirements, binding);
|
|
2377
|
+
const settleResult = unbound ? refuseUnboundReplay(answered) : answered;
|
|
2044
2378
|
return {
|
|
2045
2379
|
verified: true,
|
|
2046
2380
|
settled: settleResult.success,
|
|
2047
2381
|
transactionHash: settleResult.transactionHash,
|
|
2048
2382
|
error: settleResult.error,
|
|
2049
|
-
...carryFailureFields(settleResult)
|
|
2383
|
+
...carryFailureFields(settleResult),
|
|
2384
|
+
...settleResult.replayed ? { replayed: true } : {},
|
|
2385
|
+
idempotencyKey
|
|
2050
2386
|
};
|
|
2051
2387
|
}
|
|
2052
2388
|
/**
|
|
@@ -2434,20 +2770,51 @@ async function resolvePaymentRequirement(payment, requirements, resolver) {
|
|
|
2434
2770
|
}
|
|
2435
2771
|
return { requirement: matches[0] };
|
|
2436
2772
|
}
|
|
2437
|
-
function createVerifiedPaymentState(client, payment, requirements, verifyResult) {
|
|
2773
|
+
function createVerifiedPaymentState(client, payment, requirements, verifyResult, binding, onSettled) {
|
|
2438
2774
|
let settlePromise = null;
|
|
2439
2775
|
return {
|
|
2440
2776
|
payment,
|
|
2441
2777
|
requirements,
|
|
2442
2778
|
verifyResult,
|
|
2779
|
+
idempotencyKey: binding.idempotencyKey,
|
|
2443
2780
|
settle: () => {
|
|
2444
2781
|
if (!settlePromise) {
|
|
2445
|
-
settlePromise = client.settle(payment, requirements)
|
|
2782
|
+
settlePromise = client.settle(payment, requirements, binding).then((answer) => {
|
|
2783
|
+
const result = binding.receiptContext ? answer : refuseUnboundReplay(answer);
|
|
2784
|
+
try {
|
|
2785
|
+
onSettled?.(result);
|
|
2786
|
+
} catch (error) {
|
|
2787
|
+
console.warn("[x402] settled, but the payment response headers could not be attached:", error);
|
|
2788
|
+
}
|
|
2789
|
+
return result;
|
|
2790
|
+
});
|
|
2446
2791
|
}
|
|
2447
2792
|
return settlePromise;
|
|
2448
2793
|
}
|
|
2449
2794
|
};
|
|
2450
2795
|
}
|
|
2796
|
+
function propagatedResult(result) {
|
|
2797
|
+
const { idempotencyKey: _key, ...propagated } = result;
|
|
2798
|
+
return propagated;
|
|
2799
|
+
}
|
|
2800
|
+
function buildPaymentConflictResponse(failure) {
|
|
2801
|
+
if (isAuthorizationInFlight(failure)) {
|
|
2802
|
+
const { headers, body } = buildUnavailableResponse("Payment authorization in flight", failure);
|
|
2803
|
+
return { status: 503, headers, body: { ...body, reason: AUTHORIZATION_IN_FLIGHT } };
|
|
2804
|
+
}
|
|
2805
|
+
const used = isAuthorizationAlreadyUsed(failure);
|
|
2806
|
+
if (!used && failure.status !== 409) return null;
|
|
2807
|
+
return {
|
|
2808
|
+
status: 409,
|
|
2809
|
+
headers: {},
|
|
2810
|
+
body: {
|
|
2811
|
+
error: used ? "Payment authorization already used" : "Payment request conflict",
|
|
2812
|
+
reason: failure.errorCode ?? failure.invalidReason ?? failure.error,
|
|
2813
|
+
retryable: false,
|
|
2814
|
+
safeToReplay: false
|
|
2815
|
+
}
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2451
2818
|
function buildUnavailableResponse(message, failure) {
|
|
2452
2819
|
const seconds = Math.max(1, Math.ceil(failure.retryAfterSeconds ?? DEFAULT_RETRY_AFTER_SECONDS));
|
|
2453
2820
|
return {
|
|
@@ -2464,14 +2831,25 @@ function buildUnavailableResponse(message, failure) {
|
|
|
2464
2831
|
};
|
|
2465
2832
|
}
|
|
2466
2833
|
function respondUnavailable(res, message, failure) {
|
|
2467
|
-
|
|
2834
|
+
respondExpress(res, buildUnavailableResponse(message, failure));
|
|
2835
|
+
}
|
|
2836
|
+
function respondExpress(res, { status, headers, body }) {
|
|
2468
2837
|
const staged = res.status(status);
|
|
2469
|
-
if (typeof staged.set === "function") {
|
|
2838
|
+
if (Object.keys(headers).length > 0 && typeof staged.set === "function") {
|
|
2470
2839
|
staged.set(headers).json(body);
|
|
2471
2840
|
return;
|
|
2472
2841
|
}
|
|
2473
2842
|
staged.json(body);
|
|
2474
2843
|
}
|
|
2844
|
+
function setExpressPaymentHeaders(res, result) {
|
|
2845
|
+
if (!result.receipt || !res.set || res.headersSent) return;
|
|
2846
|
+
const current = (name) => {
|
|
2847
|
+
const value = res.getHeader ? res.getHeader(name) : res.get?.(name);
|
|
2848
|
+
if (value === void 0 || value === null) return void 0;
|
|
2849
|
+
return Array.isArray(value) ? value.join(", ") : String(value);
|
|
2850
|
+
};
|
|
2851
|
+
res.set(mergePaymentResponseHeaders(propagatedResult(result), current));
|
|
2852
|
+
}
|
|
2475
2853
|
function createPaymentMiddleware(getRequirements, options = {}) {
|
|
2476
2854
|
const client = new FacilitatorClient({
|
|
2477
2855
|
baseUrl: options.facilitatorUrl || options.baseUrl,
|
|
@@ -2489,8 +2867,28 @@ function createPaymentMiddleware(getRequirements, options = {}) {
|
|
|
2489
2867
|
}
|
|
2490
2868
|
const reqOptions = getRequirements(req);
|
|
2491
2869
|
const requirements = buildPaymentRequirements(reqOptions);
|
|
2492
|
-
|
|
2870
|
+
let receiptContext;
|
|
2871
|
+
try {
|
|
2872
|
+
const entry = Object.entries(req.headers).find(([name]) => name.toLowerCase() === "x-uvd-purchase")?.[1];
|
|
2873
|
+
if (entry !== void 0) {
|
|
2874
|
+
if (typeof entry !== "string" || !req.method || !["GET", "HEAD"].includes(req.method.toUpperCase()) && !req.rawBody) throw new Error("receipt context requires method and exact raw body");
|
|
2875
|
+
const actualUrl = new URL(req.originalUrl || req.url || reqOptions.resource, reqOptions.resource).toString();
|
|
2876
|
+
receiptContext = validatePurchaseContext(entry, req.method, actualUrl, req.rawBody || new Uint8Array());
|
|
2877
|
+
requirements.resource = actualUrl;
|
|
2878
|
+
}
|
|
2879
|
+
} catch {
|
|
2880
|
+
res.status(400).json({ error: "receipt_context_mismatch" });
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
const binding = { idempotencyKey: createIdempotencyKey(), receiptContext };
|
|
2884
|
+
const verifyResult = await client.verify(payment, requirements, binding);
|
|
2885
|
+
setExpressPaymentHeaders(res, verifyResult);
|
|
2493
2886
|
if (!verifyResult.isValid) {
|
|
2887
|
+
const conflict = buildPaymentConflictResponse(verifyResult);
|
|
2888
|
+
if (conflict) {
|
|
2889
|
+
respondExpress(res, conflict);
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2494
2892
|
if (verifyResult.retryable) {
|
|
2495
2893
|
respondUnavailable(res, "Payment verification unavailable", verifyResult);
|
|
2496
2894
|
return;
|
|
@@ -2501,10 +2899,22 @@ function createPaymentMiddleware(getRequirements, options = {}) {
|
|
|
2501
2899
|
});
|
|
2502
2900
|
return;
|
|
2503
2901
|
}
|
|
2504
|
-
req.x402 = createVerifiedPaymentState(
|
|
2902
|
+
req.x402 = createVerifiedPaymentState(
|
|
2903
|
+
client,
|
|
2904
|
+
payment,
|
|
2905
|
+
requirements,
|
|
2906
|
+
verifyResult,
|
|
2907
|
+
binding,
|
|
2908
|
+
(result) => setExpressPaymentHeaders(res, result)
|
|
2909
|
+
);
|
|
2505
2910
|
if (settlementStrategy === "before-handler") {
|
|
2506
2911
|
const settleResult = await req.x402.settle();
|
|
2507
2912
|
if (!settleResult.success) {
|
|
2913
|
+
const conflict = buildPaymentConflictResponse(settleResult);
|
|
2914
|
+
if (conflict) {
|
|
2915
|
+
respondExpress(res, conflict);
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2508
2918
|
if (settleResult.retryable) {
|
|
2509
2919
|
respondUnavailable(res, "Payment settlement unavailable", settleResult);
|
|
2510
2920
|
return;
|
|
@@ -2527,12 +2937,21 @@ function settlementFailureBody(failure, fallbackReason) {
|
|
|
2527
2937
|
};
|
|
2528
2938
|
}
|
|
2529
2939
|
function honoUnavailable(c, message, failure) {
|
|
2530
|
-
|
|
2940
|
+
return respondHono(c, buildUnavailableResponse(message, failure));
|
|
2941
|
+
}
|
|
2942
|
+
function respondHono(c, { status, headers, body }) {
|
|
2531
2943
|
for (const [name, value] of Object.entries(headers)) {
|
|
2532
2944
|
c.header?.(name, value);
|
|
2533
2945
|
}
|
|
2534
2946
|
return c.json(body, status);
|
|
2535
2947
|
}
|
|
2948
|
+
function setHonoPaymentHeaders(c, result) {
|
|
2949
|
+
if (!result.receipt || !c.header || c.finalized) return;
|
|
2950
|
+
const current = (name) => c.res?.headers.get(name) ?? void 0;
|
|
2951
|
+
for (const [name, value] of Object.entries(mergePaymentResponseHeaders(propagatedResult(result), current))) {
|
|
2952
|
+
c.header(name, value);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2536
2955
|
function createHonoMiddleware(options) {
|
|
2537
2956
|
const client = new FacilitatorClient({
|
|
2538
2957
|
baseUrl: options.facilitatorUrl || options.baseUrl,
|
|
@@ -2544,7 +2963,7 @@ function createHonoMiddleware(options) {
|
|
|
2544
2963
|
throw new Error("At least one accept entry is required");
|
|
2545
2964
|
}
|
|
2546
2965
|
return async (c, next) => {
|
|
2547
|
-
const paymentHeader = c.req.header("X-PAYMENT") || c.req.header("x-payment");
|
|
2966
|
+
const paymentHeader = c.req.header("PAYMENT-SIGNATURE") || c.req.header("X-PAYMENT") || c.req.header("x-payment");
|
|
2548
2967
|
const advertisedVersion = resolveAdvertisedVersion(options.accepts, options.x402Version);
|
|
2549
2968
|
const advertisedRequirements = options.accepts.map(
|
|
2550
2969
|
(accept) => buildRequirementFromAcceptance(accept, c.req.url, advertisedVersion)
|
|
@@ -2575,8 +2994,24 @@ function createHonoMiddleware(options) {
|
|
|
2575
2994
|
reason
|
|
2576
2995
|
}, 402);
|
|
2577
2996
|
}
|
|
2578
|
-
|
|
2997
|
+
let receiptContext;
|
|
2998
|
+
try {
|
|
2999
|
+
const header = c.req.header("X-UVD-Purchase");
|
|
3000
|
+
if (header) {
|
|
3001
|
+
const method = c.req.method || c.req.raw?.method;
|
|
3002
|
+
if (!method || !["GET", "HEAD"].includes(method) && !c.req.raw) throw new Error("raw request required");
|
|
3003
|
+
const bytes = c.req.raw ? new Uint8Array(await c.req.raw.clone().arrayBuffer()) : new Uint8Array();
|
|
3004
|
+
receiptContext = validatePurchaseContext(header, method, c.req.url, bytes);
|
|
3005
|
+
}
|
|
3006
|
+
} catch {
|
|
3007
|
+
return c.json({ error: "receipt_context_mismatch" }, 400);
|
|
3008
|
+
}
|
|
3009
|
+
const binding = { idempotencyKey: createIdempotencyKey(), receiptContext };
|
|
3010
|
+
const verifyResult = await client.verify(parsed, requirement, binding);
|
|
3011
|
+
setHonoPaymentHeaders(c, verifyResult);
|
|
2579
3012
|
if (!verifyResult.isValid) {
|
|
3013
|
+
const conflict = buildPaymentConflictResponse(verifyResult);
|
|
3014
|
+
if (conflict) return respondHono(c, conflict);
|
|
2580
3015
|
if (verifyResult.retryable) {
|
|
2581
3016
|
return honoUnavailable(c, "Payment verification unavailable", verifyResult);
|
|
2582
3017
|
}
|
|
@@ -2585,11 +3020,20 @@ function createHonoMiddleware(options) {
|
|
|
2585
3020
|
reason: verifyResult.invalidReason
|
|
2586
3021
|
}, 402);
|
|
2587
3022
|
}
|
|
2588
|
-
const verifiedPayment = createVerifiedPaymentState(
|
|
3023
|
+
const verifiedPayment = createVerifiedPaymentState(
|
|
3024
|
+
client,
|
|
3025
|
+
parsed,
|
|
3026
|
+
requirement,
|
|
3027
|
+
verifyResult,
|
|
3028
|
+
binding,
|
|
3029
|
+
(result) => setHonoPaymentHeaders(c, result)
|
|
3030
|
+
);
|
|
2589
3031
|
c.set?.("x402", verifiedPayment);
|
|
2590
3032
|
if (settlementStrategy === "before-handler") {
|
|
2591
3033
|
const settleResult = await verifiedPayment.settle();
|
|
2592
3034
|
if (!settleResult.success) {
|
|
3035
|
+
const conflict = buildPaymentConflictResponse(settleResult);
|
|
3036
|
+
if (conflict) return respondHono(c, conflict);
|
|
2593
3037
|
if (settleResult.retryable) {
|
|
2594
3038
|
return honoUnavailable(c, "Payment settlement unavailable", settleResult);
|
|
2595
3039
|
}
|
|
@@ -5481,6 +5925,8 @@ var AdvancedEscrowClient = class {
|
|
|
5481
5925
|
};
|
|
5482
5926
|
|
|
5483
5927
|
exports.AMBIGUOUS_LEASE_REASONS = AMBIGUOUS_LEASE_REASONS;
|
|
5928
|
+
exports.AUTHORIZATION_ALREADY_SETTLED = AUTHORIZATION_ALREADY_SETTLED;
|
|
5929
|
+
exports.AUTHORIZATION_IN_FLIGHT = AUTHORIZATION_IN_FLIGHT;
|
|
5484
5930
|
exports.AdvancedEscrowClient = AdvancedEscrowClient;
|
|
5485
5931
|
exports.BASE_MAINNET_CONTRACTS = BASE_MAINNET_CONTRACTS;
|
|
5486
5932
|
exports.BazaarClient = BazaarClient;
|
|
@@ -5496,11 +5942,15 @@ exports.Erc8004LookupError = Erc8004LookupError;
|
|
|
5496
5942
|
exports.EscrowClient = EscrowClient;
|
|
5497
5943
|
exports.FacilitatorClient = FacilitatorClient;
|
|
5498
5944
|
exports.HEALTH_FILTERS = HEALTH_FILTERS;
|
|
5945
|
+
exports.IDEMPOTENCY_KEY_HEADER = IDEMPOTENCY_KEY_HEADER;
|
|
5946
|
+
exports.IDEMPOTENT_REPLAYED_HEADER = IDEMPOTENT_REPLAYED_HEADER;
|
|
5499
5947
|
exports.MAX_RETRY_AFTER_SECONDS = MAX_RETRY_AFTER_SECONDS;
|
|
5500
5948
|
exports.MAX_SEARCH_LEN = MAX_SEARCH_LEN;
|
|
5501
5949
|
exports.OPERATOR_ABI = OPERATOR_ABI;
|
|
5502
5950
|
exports.OPERATOR_ABI_CREATE3 = OPERATOR_ABI_CREATE3;
|
|
5503
5951
|
exports.PAYMENT_INFO_TYPEHASH = PAYMENT_INFO_TYPEHASH;
|
|
5952
|
+
exports.RECEIPT_ISSUER = RECEIPT_ISSUER;
|
|
5953
|
+
exports.RECEIPT_REQUEST_CONFLICT = RECEIPT_REQUEST_CONFLICT;
|
|
5504
5954
|
exports.RELAYED_FEEDBACK_NETWORKS = RELAYED_FEEDBACK_NETWORKS;
|
|
5505
5955
|
exports.REPLAYABLE_LEASE_REASONS = REPLAYABLE_LEASE_REASONS;
|
|
5506
5956
|
exports.RegistrationPendingError = RegistrationPendingError;
|
|
@@ -5514,6 +5964,7 @@ exports.X402_CORS_HEADERS = X402_CORS_HEADERS;
|
|
|
5514
5964
|
exports.X402_HEADER_NAMES = X402_HEADER_NAMES;
|
|
5515
5965
|
exports.ZERO_ADDRESS = ZERO_ADDRESS;
|
|
5516
5966
|
exports.buildErc8004PaymentRequirements = buildErc8004PaymentRequirements;
|
|
5967
|
+
exports.buildPaymentConflictResponse = buildPaymentConflictResponse;
|
|
5517
5968
|
exports.buildPaymentRequirements = buildPaymentRequirements;
|
|
5518
5969
|
exports.buildSettleRequest = buildSettleRequest;
|
|
5519
5970
|
exports.buildSettleRequestForVersion = buildSettleRequestForVersion;
|
|
@@ -5524,33 +5975,50 @@ exports.buildVerifyRequestForVersion = buildVerifyRequestForVersion;
|
|
|
5524
5975
|
exports.buildVerifyRequestV2 = buildVerifyRequestV2;
|
|
5525
5976
|
exports.canRefundEscrow = canRefundEscrow;
|
|
5526
5977
|
exports.canReleaseEscrow = canReleaseEscrow;
|
|
5978
|
+
exports.canonicalReceiptJson = canonicalReceiptJson;
|
|
5527
5979
|
exports.carryFailureFields = carryFailureFields;
|
|
5528
5980
|
exports.create402Response = create402Response;
|
|
5529
5981
|
exports.createHonoMiddleware = createHonoMiddleware;
|
|
5982
|
+
exports.createIdempotencyKey = createIdempotencyKey;
|
|
5530
5983
|
exports.createPaymentMiddleware = createPaymentMiddleware;
|
|
5984
|
+
exports.createPurchaseContext = createPurchaseContext;
|
|
5531
5985
|
exports.epochToDate = epochToDate;
|
|
5532
5986
|
exports.escrowTimeRemaining = escrowTimeRemaining;
|
|
5533
5987
|
exports.extractPaymentFromHeaders = extractPaymentFromHeaders;
|
|
5534
5988
|
exports.facilitatorFetch = facilitatorFetch;
|
|
5989
|
+
exports.fetchWithReceipt = fetchWithReceipt;
|
|
5535
5990
|
exports.getCorsHeaders = getCorsHeaders;
|
|
5536
5991
|
exports.getEscrowContractsByChainId = getEscrowContractsByChainId;
|
|
5537
5992
|
exports.getEscrowSupportedChainIds = getEscrowSupportedChainIds;
|
|
5993
|
+
exports.getFacilitatorReceipt = getFacilitatorReceipt;
|
|
5538
5994
|
exports.isAlive = isAlive;
|
|
5539
5995
|
exports.isAmbiguousLeaseReason = isAmbiguousLeaseReason;
|
|
5996
|
+
exports.isAuthorizationAlreadyUsed = isAuthorizationAlreadyUsed;
|
|
5997
|
+
exports.isAuthorizationInFlight = isAuthorizationInFlight;
|
|
5540
5998
|
exports.isEscrowExpired = isEscrowExpired;
|
|
5541
5999
|
exports.isEscrowSupportedOnChain = isEscrowSupportedOnChain;
|
|
5542
6000
|
exports.isRegisterJobTerminal = isRegisterJobTerminal;
|
|
5543
6001
|
exports.isReplayableLeaseReason = isReplayableLeaseReason;
|
|
5544
6002
|
exports.isSettlementUnconfirmed = isSettlementUnconfirmed;
|
|
6003
|
+
exports.mergeHeaderList = mergeHeaderList;
|
|
6004
|
+
exports.mergePaymentResponseHeaders = mergePaymentResponseHeaders;
|
|
5545
6005
|
exports.parseFacilitatorErrorBody = parseFacilitatorErrorBody;
|
|
6006
|
+
exports.parseFacilitatorReceipt = parseFacilitatorReceipt;
|
|
5546
6007
|
exports.parsePaymentHeader = parsePaymentHeader;
|
|
5547
6008
|
exports.parseRetryAfterSeconds = parseRetryAfterSeconds;
|
|
6009
|
+
exports.paymentResponseHeaders = paymentResponseHeaders;
|
|
6010
|
+
exports.purchaseContextHeader = purchaseContextHeader;
|
|
5548
6011
|
exports.readFacilitatorError = readFacilitatorError;
|
|
6012
|
+
exports.receiptCommitment = receiptCommitment;
|
|
6013
|
+
exports.receiptFromErrorBody = receiptFromErrorBody;
|
|
6014
|
+
exports.receiptFromResponse = receiptFromResponse;
|
|
5549
6015
|
exports.resolveEnvelopeVersion = resolveEnvelopeVersion;
|
|
5550
6016
|
exports.supportsRelayedFeedback = supportsRelayedFeedback;
|
|
5551
6017
|
exports.supportsSolanaFeedback = supportsSolanaFeedback;
|
|
5552
6018
|
exports.toPaymentRequirementsV2 = toPaymentRequirementsV2;
|
|
5553
6019
|
exports.toResourceInfoV2 = toResourceInfoV2;
|
|
6020
|
+
exports.validatePurchaseContext = validatePurchaseContext;
|
|
6021
|
+
exports.verifyFacilitatorReceipt = verifyFacilitatorReceipt;
|
|
5554
6022
|
exports.wireNetwork = wireNetwork;
|
|
5555
6023
|
//# sourceMappingURL=index.js.map
|
|
5556
6024
|
//# sourceMappingURL=index.js.map
|