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.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { ed25519 } from '@noble/curves/ed25519';
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
3
|
+
import { bytesToHex, randomBytes } from '@noble/hashes/utils';
|
|
1
4
|
import { parseUnits, ethers } from 'ethers';
|
|
2
5
|
|
|
3
6
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
@@ -6,6 +9,205 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
6
9
|
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
7
10
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
8
11
|
});
|
|
12
|
+
var RECEIPT_ISSUER = "https://facilitator.ultravioletadao.xyz";
|
|
13
|
+
var encoder = new TextEncoder();
|
|
14
|
+
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
15
|
+
var digest = (value) => bytesToHex(sha256(value));
|
|
16
|
+
function canonicalReceiptJson(value) {
|
|
17
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
|
18
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return String(value);
|
|
19
|
+
if (Array.isArray(value)) return `[${value.map(canonicalReceiptJson).join(",")}]`;
|
|
20
|
+
if (typeof value === "object" && value) {
|
|
21
|
+
const object = value;
|
|
22
|
+
const keys = Object.keys(object).sort();
|
|
23
|
+
if (keys.some((key) => !/^[\x00-\x7f]*$/.test(key))) throw new Error("non-ASCII canonical key");
|
|
24
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalReceiptJson(object[key])}`).join(",")}}`;
|
|
25
|
+
}
|
|
26
|
+
throw new Error("unsupported canonical value");
|
|
27
|
+
}
|
|
28
|
+
function receiptCommitment(domain, value) {
|
|
29
|
+
return digest(encoder.encode(`${domain}
|
|
30
|
+
${canonicalReceiptJson(value)}`));
|
|
31
|
+
}
|
|
32
|
+
function encode64(bytes) {
|
|
33
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
|
|
34
|
+
return btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join(""));
|
|
35
|
+
}
|
|
36
|
+
function decode64(value) {
|
|
37
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) throw new Error("invalid base64");
|
|
38
|
+
if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(value, "base64"));
|
|
39
|
+
return Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
|
|
40
|
+
}
|
|
41
|
+
function decodeUrl64(value) {
|
|
42
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
43
|
+
return decode64(padded + "=".repeat((4 - padded.length % 4) % 4));
|
|
44
|
+
}
|
|
45
|
+
function parseFacilitatorReceipt(value) {
|
|
46
|
+
if (value === null || value === void 0) return null;
|
|
47
|
+
const r = value;
|
|
48
|
+
if (r.schemaVersion !== 1 || !["verified", "pending", "confirmed", "rejected", "unknown"].includes(r.status)) throw new Error("unsupported receipt version or state");
|
|
49
|
+
for (const key of ["receiptId", "issuer", "operation", "network", "scheme", "asset", "amount", "payTo", "paymentRequestHash", "authorizationId"]) {
|
|
50
|
+
if (typeof r[key] !== "string" || !r[key]) throw new Error(`invalid receipt ${key}`);
|
|
51
|
+
}
|
|
52
|
+
if (!Number.isSafeInteger(r.revision) || r.revision < 1 || !r.request || !r.retry) throw new Error("incomplete receipt");
|
|
53
|
+
if (r.requestHashVersion !== "uvd-x402-request-v1" || r.requestHash !== receiptCommitment(r.requestHashVersion, r.request)) throw new Error("receipt request hash mismatch");
|
|
54
|
+
for (const key of ["network", "scheme", "asset", "amount", "payTo", "purchaseId"]) {
|
|
55
|
+
if (r[key] !== r.request[key]) throw new Error("receipt terms mismatch");
|
|
56
|
+
}
|
|
57
|
+
if (!/^[0-9]+$/.test(r.amount)) throw new Error("receipt amount must be an atomic integer string");
|
|
58
|
+
if (r.status === "confirmed" && (r.operation !== "settle" || !r.settlement?.id)) throw new Error("confirmation has no settlement");
|
|
59
|
+
return r;
|
|
60
|
+
}
|
|
61
|
+
function verifyFacilitatorReceipt(receipt, jwks, issuer = RECEIPT_ISSUER) {
|
|
62
|
+
try {
|
|
63
|
+
parseFacilitatorReceipt(receipt);
|
|
64
|
+
if (receipt.issuer !== issuer || receipt.proof?.type !== "jws" || !receipt.proof.jws) return false;
|
|
65
|
+
const parts = receipt.proof.jws.split(".");
|
|
66
|
+
if (parts.length !== 3) return false;
|
|
67
|
+
const [protectedHeader, payload, signature] = parts;
|
|
68
|
+
const header = JSON.parse(decoder.decode(decodeUrl64(protectedHeader)));
|
|
69
|
+
if (header.alg !== "EdDSA" || header.typ !== "uvd-facilitator-receipt+jws") return false;
|
|
70
|
+
const keys = jwks.keys.filter((k) => k.kid === header.kid && k.kty === "OKP" && k.crv === "Ed25519");
|
|
71
|
+
if (keys.length !== 1 || !keys[0].x) return false;
|
|
72
|
+
if (decoder.decode(decodeUrl64(payload)) !== canonicalReceiptJson({ ...receipt, proof: null })) return false;
|
|
73
|
+
return ed25519.verify(decodeUrl64(signature), encoder.encode(`${protectedHeader}.${payload}`), decodeUrl64(keys[0].x));
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function createPurchaseContext() {
|
|
79
|
+
return { purchaseId: bytesToHex(randomBytes(16)), accessToken: bytesToHex(randomBytes(32)) };
|
|
80
|
+
}
|
|
81
|
+
function purchaseContextHeader(context) {
|
|
82
|
+
return encode64(encoder.encode(canonicalReceiptJson({
|
|
83
|
+
purchaseId: context.purchaseId,
|
|
84
|
+
accessToken: context.accessToken,
|
|
85
|
+
method: context.method,
|
|
86
|
+
url: context.url,
|
|
87
|
+
bodySha256: context.bodySha256
|
|
88
|
+
})));
|
|
89
|
+
}
|
|
90
|
+
function validatePurchaseContext(header, method, url, body) {
|
|
91
|
+
if (!header) return void 0;
|
|
92
|
+
if (header.length > 8192) throw new Error("invalid purchase context");
|
|
93
|
+
const context = JSON.parse(decoder.decode(decode64(header)));
|
|
94
|
+
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");
|
|
95
|
+
return header;
|
|
96
|
+
}
|
|
97
|
+
function paymentResponseHeaders(result) {
|
|
98
|
+
const encoded = encode64(encoder.encode(JSON.stringify(result)));
|
|
99
|
+
if (encoded.length > 32768) throw new Error("payment response too large");
|
|
100
|
+
return {
|
|
101
|
+
"PAYMENT-RESPONSE": encoded,
|
|
102
|
+
"X-PAYMENT-RESPONSE": encoded,
|
|
103
|
+
"Access-Control-Expose-Headers": "PAYMENT-RESPONSE, X-PAYMENT-RESPONSE",
|
|
104
|
+
"Cache-Control": "no-store"
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
var LIST_HEADERS = ["Access-Control-Expose-Headers", "Cache-Control"];
|
|
108
|
+
function mergeHeaderList(existing, additions) {
|
|
109
|
+
const name = (entry) => entry.split("=")[0].trim().toLowerCase();
|
|
110
|
+
const kept = (existing || "").split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
111
|
+
const seen = new Set(kept.map(name));
|
|
112
|
+
for (const entry of additions.split(",").map((value) => value.trim()).filter(Boolean)) {
|
|
113
|
+
if (!seen.has(name(entry))) {
|
|
114
|
+
kept.push(entry);
|
|
115
|
+
seen.add(name(entry));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return kept.join(", ");
|
|
119
|
+
}
|
|
120
|
+
function mergePaymentResponseHeaders(result, current) {
|
|
121
|
+
const headers = paymentResponseHeaders(result);
|
|
122
|
+
for (const name of LIST_HEADERS) headers[name] = mergeHeaderList(current(name), headers[name]);
|
|
123
|
+
return headers;
|
|
124
|
+
}
|
|
125
|
+
function receiptFromResponse(response) {
|
|
126
|
+
const values = [response.headers.get("PAYMENT-RESPONSE"), response.headers.get("X-PAYMENT-RESPONSE")].filter((v) => v !== null);
|
|
127
|
+
if (!values.length) return null;
|
|
128
|
+
if (new Set(values).size !== 1 || values[0].includes(",") || values[0].length > 32768) throw new Error("ambiguous or oversized payment response header");
|
|
129
|
+
return parseFacilitatorReceipt(JSON.parse(decoder.decode(decode64(values[0]))).receipt);
|
|
130
|
+
}
|
|
131
|
+
function receiptFromErrorBody(body) {
|
|
132
|
+
try {
|
|
133
|
+
return parseFacilitatorReceipt(JSON.parse(body).receipt);
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function fetchWithReceipt(fetchClient, url, options) {
|
|
139
|
+
const { context, persist, trustedKeys, issuer = RECEIPT_ISSUER } = options;
|
|
140
|
+
const transport = options.fetchImpl || globalThis.fetch;
|
|
141
|
+
const method = (options.method || "GET").toUpperCase();
|
|
142
|
+
const request = new Request(url, { ...options.init, method });
|
|
143
|
+
const bytes = new Uint8Array(await request.arrayBuffer());
|
|
144
|
+
const bodyHash = digest(bytes);
|
|
145
|
+
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");
|
|
146
|
+
context.method = method;
|
|
147
|
+
context.url = request.url;
|
|
148
|
+
context.bodySha256 = bodyHash;
|
|
149
|
+
const init = { ...options.init, method, headers: new Headers(request.headers), body: ["GET", "HEAD"].includes(method) ? void 0 : bytes };
|
|
150
|
+
let receipt = parseFacilitatorReceipt(context.receipt);
|
|
151
|
+
let transportError;
|
|
152
|
+
const persistCopy = async () => {
|
|
153
|
+
await persist(JSON.parse(JSON.stringify(context)));
|
|
154
|
+
};
|
|
155
|
+
const send = async (input, sendOptions) => {
|
|
156
|
+
const headers = new Headers(sendOptions?.headers);
|
|
157
|
+
const paid = headers.get("X-PAYMENT") || headers.get("PAYMENT-SIGNATURE");
|
|
158
|
+
if (paid) {
|
|
159
|
+
const paymentHeaders = {};
|
|
160
|
+
for (const name of ["X-PAYMENT", "PAYMENT-SIGNATURE"]) {
|
|
161
|
+
const value = headers.get(name);
|
|
162
|
+
if (value) paymentHeaders[name] = value;
|
|
163
|
+
}
|
|
164
|
+
if (context.paymentHeaders && JSON.stringify(context.paymentHeaders) !== JSON.stringify(paymentHeaders)) throw new Error("refusing a new authorization for an existing purchase");
|
|
165
|
+
context.paymentHeaders = paymentHeaders;
|
|
166
|
+
headers.set("X-UVD-Purchase", purchaseContextHeader(context));
|
|
167
|
+
await persistCopy();
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
return await transport(input, { ...sendOptions, headers });
|
|
171
|
+
} catch (error) {
|
|
172
|
+
transportError = error;
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
try {
|
|
177
|
+
let response;
|
|
178
|
+
if (context.paymentHeaders) {
|
|
179
|
+
const headers = new Headers(init.headers);
|
|
180
|
+
for (const [name, value] of Object.entries(context.paymentHeaders)) headers.set(name, value);
|
|
181
|
+
response = await send(request.url, { ...init, headers });
|
|
182
|
+
} else {
|
|
183
|
+
response = await fetchClient(request.url, { ...options, method, init, fetchImpl: send });
|
|
184
|
+
}
|
|
185
|
+
const incoming = receiptFromResponse(response);
|
|
186
|
+
if (incoming) {
|
|
187
|
+
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");
|
|
188
|
+
if (receipt?.operation === "settle" && (incoming.receiptId !== receipt.receiptId || incoming.revision < receipt.revision)) throw new Error("receipt revision conflict");
|
|
189
|
+
if (trustedKeys && !verifyFacilitatorReceipt(incoming, trustedKeys, issuer)) throw new Error("receipt signature verification failed");
|
|
190
|
+
receipt = incoming;
|
|
191
|
+
context.receipt = incoming;
|
|
192
|
+
await persistCopy();
|
|
193
|
+
}
|
|
194
|
+
const proofVerified = !!(receipt && trustedKeys && verifyFacilitatorReceipt(receipt, trustedKeys, issuer));
|
|
195
|
+
if (receipt && trustedKeys && !proofVerified) throw new Error("receipt signature verification failed");
|
|
196
|
+
return { response, receipt, paymentState: receipt ? receipt.status : context.paymentHeaders ? "unknown" : "not_required", context, proofVerified };
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (!transportError) throw error;
|
|
199
|
+
return { response: null, receipt, paymentState: "unknown", context, proofVerified: false, error };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function getFacilitatorReceipt(receiptId, context, options = {}) {
|
|
203
|
+
if (!/^[0-9a-f-]{36}$/.test(receiptId)) throw new Error("invalid receipt ID");
|
|
204
|
+
const issuer = options.issuer || RECEIPT_ISSUER;
|
|
205
|
+
const response = await (options.fetchImpl || globalThis.fetch)(`${issuer}/receipts/${receiptId}`, { headers: { Authorization: `Bearer ${context.accessToken}` } });
|
|
206
|
+
if (!response.ok) throw new Error(`receipt lookup failed: ${response.status}`);
|
|
207
|
+
const receipt = parseFacilitatorReceipt((await response.json()).receipt);
|
|
208
|
+
if (!receipt || receipt.issuer !== issuer || receipt.purchaseId !== context.purchaseId) throw new Error("receipt lookup mismatch");
|
|
209
|
+
return receipt;
|
|
210
|
+
}
|
|
9
211
|
|
|
10
212
|
// src/utils/base64.ts
|
|
11
213
|
function decodeBase64Utf8(encoded) {
|
|
@@ -807,7 +1009,8 @@ var SUPPORTED_CHAINS = {
|
|
|
807
1009
|
// Pinned by src/arc-testnet.test.ts.
|
|
808
1010
|
//
|
|
809
1011
|
// Mainnet (5042) and testnet (5042002) were verified independently against
|
|
810
|
-
// their official RPCs on 2026-09-16. EURC domains checked on 2026-09-17;
|
|
1012
|
+
// their official RPCs on 2026-09-16. EURC domains checked on 2026-09-17; funded EURC payments settled on Arc mainnet on
|
|
1013
|
+
// 2026-09-22 (x402 v1/v2, docs/networks/arc.md); testnet pending.
|
|
811
1014
|
arc: {
|
|
812
1015
|
chainId: 5042,
|
|
813
1016
|
chainIdHex: "0x13b2",
|
|
@@ -1522,6 +1725,9 @@ var REPLAYABLE_LEASE_REASONS = [
|
|
|
1522
1725
|
];
|
|
1523
1726
|
var AMBIGUOUS_LEASE_REASONS = ["forward_failed"];
|
|
1524
1727
|
var SETTLEMENT_UNCONFIRMED = "settlement_unconfirmed";
|
|
1728
|
+
var AUTHORIZATION_ALREADY_SETTLED = "authorization_already_settled";
|
|
1729
|
+
var AUTHORIZATION_IN_FLIGHT = "authorization_in_flight";
|
|
1730
|
+
var RECEIPT_REQUEST_CONFLICT = "receipt_request_conflict";
|
|
1525
1731
|
var MAX_RETRY_AFTER_SECONDS = 15;
|
|
1526
1732
|
var DEFAULT_RETRY_AFTER_SECONDS = 5;
|
|
1527
1733
|
var DEFAULT_FACILITATOR_RETRIES = 2;
|
|
@@ -1582,6 +1788,12 @@ function isDeclaredUnretryable(parsed) {
|
|
|
1582
1788
|
function isSettlementUnconfirmed(failure) {
|
|
1583
1789
|
return failure.errorCode === SETTLEMENT_UNCONFIRMED;
|
|
1584
1790
|
}
|
|
1791
|
+
function isAuthorizationAlreadyUsed(failure) {
|
|
1792
|
+
return failure.errorCode === AUTHORIZATION_ALREADY_SETTLED || failure.errorCode === RECEIPT_REQUEST_CONFLICT;
|
|
1793
|
+
}
|
|
1794
|
+
function isAuthorizationInFlight(failure) {
|
|
1795
|
+
return failure.errorCode === AUTHORIZATION_IN_FLIGHT;
|
|
1796
|
+
}
|
|
1585
1797
|
async function readFacilitatorError(response) {
|
|
1586
1798
|
let body = "";
|
|
1587
1799
|
try {
|
|
@@ -1593,7 +1805,8 @@ async function readFacilitatorError(response) {
|
|
|
1593
1805
|
const parsed = parseFacilitatorErrorBody(body);
|
|
1594
1806
|
const reason = parsed.reason;
|
|
1595
1807
|
const transportRetryable = status === 429 || status === 502 || status === 503 || status === 504;
|
|
1596
|
-
const
|
|
1808
|
+
const inFlight = status === 409 && parsed.errorCode === AUTHORIZATION_IN_FLIGHT;
|
|
1809
|
+
const retryable = inFlight || transportRetryable && !isDeclaredUnretryable(parsed);
|
|
1597
1810
|
const retryAfterSeconds = retryable ? parseRetryAfterSeconds(response) ?? DEFAULT_RETRY_AFTER_SECONDS : void 0;
|
|
1598
1811
|
const safeToReplay = retryable && (status === 429 || status === 503 && isReplayableLeaseReason(reason));
|
|
1599
1812
|
return {
|
|
@@ -1851,7 +2064,7 @@ function buildSettleRequestForVersion(paymentHeader, requirements, version) {
|
|
|
1851
2064
|
return buildSettleRequest(paymentHeader, requirements);
|
|
1852
2065
|
}
|
|
1853
2066
|
var X402_CORS_HEADERS = {
|
|
1854
|
-
"Access-Control-Allow-Headers": "Content-Type, X-PAYMENT, PAYMENT-SIGNATURE, Authorization",
|
|
2067
|
+
"Access-Control-Allow-Headers": "Content-Type, X-PAYMENT, PAYMENT-SIGNATURE, X-UVD-Purchase, Authorization",
|
|
1855
2068
|
"Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE, PAYMENT-RESPONSE, PAYMENT-REQUIRED",
|
|
1856
2069
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS"
|
|
1857
2070
|
};
|
|
@@ -1860,7 +2073,8 @@ var X402_HEADER_NAMES = [
|
|
|
1860
2073
|
"PAYMENT-SIGNATURE",
|
|
1861
2074
|
"X-PAYMENT-RESPONSE",
|
|
1862
2075
|
"PAYMENT-RESPONSE",
|
|
1863
|
-
"PAYMENT-REQUIRED"
|
|
2076
|
+
"PAYMENT-REQUIRED",
|
|
2077
|
+
"X-UVD-Purchase"
|
|
1864
2078
|
];
|
|
1865
2079
|
function getCorsHeaders(origin = "*") {
|
|
1866
2080
|
return {
|
|
@@ -1868,6 +2082,78 @@ function getCorsHeaders(origin = "*") {
|
|
|
1868
2082
|
...X402_CORS_HEADERS
|
|
1869
2083
|
};
|
|
1870
2084
|
}
|
|
2085
|
+
var IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
|
|
2086
|
+
var IDEMPOTENT_REPLAYED_HEADER = "Idempotent-Replayed";
|
|
2087
|
+
function createIdempotencyKey() {
|
|
2088
|
+
return `x402-${bytesToHex(randomBytes(32))}`;
|
|
2089
|
+
}
|
|
2090
|
+
function checkIdempotencyKey(key) {
|
|
2091
|
+
if (typeof key !== "string" || !/^[\x21-\x7e]{1,255}$/.test(key) || key.startsWith("receipt:")) {
|
|
2092
|
+
throw new Error('idempotencyKey must be 1-255 visible ASCII characters and must not start with "receipt:"');
|
|
2093
|
+
}
|
|
2094
|
+
return key;
|
|
2095
|
+
}
|
|
2096
|
+
function isReplayed(response) {
|
|
2097
|
+
try {
|
|
2098
|
+
return response?.headers?.get?.(IDEMPOTENT_REPLAYED_HEADER)?.trim().toLowerCase() === "true";
|
|
2099
|
+
} catch {
|
|
2100
|
+
return false;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
function refuseUnboundReplay(result) {
|
|
2104
|
+
if (!result.replayed) return result;
|
|
2105
|
+
const pending = result.receipt?.status === "pending" || result.receipt?.status === "unknown" || result.errorCode === "settlement_in_progress";
|
|
2106
|
+
if (!result.success && !pending) return result;
|
|
2107
|
+
const {
|
|
2108
|
+
transactionHash: _tx,
|
|
2109
|
+
proofOfPayment: _proof,
|
|
2110
|
+
paymentId: _id,
|
|
2111
|
+
transaction: _t,
|
|
2112
|
+
retryAfterSeconds,
|
|
2113
|
+
...rest
|
|
2114
|
+
} = result;
|
|
2115
|
+
if (result.success) {
|
|
2116
|
+
return {
|
|
2117
|
+
...rest,
|
|
2118
|
+
success: false,
|
|
2119
|
+
error: AUTHORIZATION_ALREADY_SETTLED,
|
|
2120
|
+
errorCode: AUTHORIZATION_ALREADY_SETTLED,
|
|
2121
|
+
retryable: false,
|
|
2122
|
+
safeToReplay: false
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
return {
|
|
2126
|
+
...rest,
|
|
2127
|
+
success: false,
|
|
2128
|
+
error: AUTHORIZATION_IN_FLIGHT,
|
|
2129
|
+
errorCode: AUTHORIZATION_IN_FLIGHT,
|
|
2130
|
+
retryable: true,
|
|
2131
|
+
safeToReplay: false,
|
|
2132
|
+
retryAfterSeconds: retryAfterSeconds ?? DEFAULT_RETRY_AFTER_SECONDS
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
function facilitatorHeaders(idempotencyKey, receiptContext) {
|
|
2136
|
+
return {
|
|
2137
|
+
"Content-Type": "application/json",
|
|
2138
|
+
[IDEMPOTENCY_KEY_HEADER]: idempotencyKey,
|
|
2139
|
+
...receiptContext ? { "X-UVD-Purchase": receiptContext } : {}
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
function admittedVerdict(result) {
|
|
2143
|
+
if (result.isValid !== false) return {};
|
|
2144
|
+
if (result.invalidReason === AUTHORIZATION_IN_FLIGHT) {
|
|
2145
|
+
return {
|
|
2146
|
+
errorCode: AUTHORIZATION_IN_FLIGHT,
|
|
2147
|
+
retryable: true,
|
|
2148
|
+
safeToReplay: false,
|
|
2149
|
+
retryAfterSeconds: DEFAULT_RETRY_AFTER_SECONDS
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
if (result.invalidReason === AUTHORIZATION_ALREADY_SETTLED) {
|
|
2153
|
+
return { errorCode: AUTHORIZATION_ALREADY_SETTLED, retryable: false, safeToReplay: false };
|
|
2154
|
+
}
|
|
2155
|
+
return {};
|
|
2156
|
+
}
|
|
1871
2157
|
var FacilitatorClient = class {
|
|
1872
2158
|
baseUrl;
|
|
1873
2159
|
timeout;
|
|
@@ -1900,11 +2186,19 @@ var FacilitatorClient = class {
|
|
|
1900
2186
|
*
|
|
1901
2187
|
* Call this before providing the paid resource to validate the payment.
|
|
1902
2188
|
*
|
|
2189
|
+
* An authorization the facilitator already admitted for another request
|
|
2190
|
+
* comes back invalid with `errorCode` {@link AUTHORIZATION_ALREADY_SETTLED}
|
|
2191
|
+
* (do not serve, do not ask for a new signature) or
|
|
2192
|
+
* {@link AUTHORIZATION_IN_FLIGHT} (`retryable`: the same request learns the
|
|
2193
|
+
* verdict later). Neither is a rejected payment: never answer them with 402.
|
|
2194
|
+
*
|
|
1903
2195
|
* @param paymentHeader - Parsed x402 payment header
|
|
1904
2196
|
* @param requirements - Payment requirements
|
|
2197
|
+
* @param options - Purchase context and the payment's `idempotencyKey`
|
|
1905
2198
|
* @returns Verification result
|
|
1906
2199
|
*/
|
|
1907
|
-
async verify(paymentHeader, requirements) {
|
|
2200
|
+
async verify(paymentHeader, requirements, options = {}) {
|
|
2201
|
+
const idempotencyKey = checkIdempotencyKey(options.idempotencyKey ?? createIdempotencyKey());
|
|
1908
2202
|
const body = buildVerifyRequestForVersion(
|
|
1909
2203
|
paymentHeader,
|
|
1910
2204
|
requirements,
|
|
@@ -1915,7 +2209,7 @@ var FacilitatorClient = class {
|
|
|
1915
2209
|
`${this.baseUrl}/verify`,
|
|
1916
2210
|
{
|
|
1917
2211
|
method: "POST",
|
|
1918
|
-
headers:
|
|
2212
|
+
headers: facilitatorHeaders(idempotencyKey, options.receiptContext),
|
|
1919
2213
|
body: JSON.stringify(body)
|
|
1920
2214
|
},
|
|
1921
2215
|
{ timeoutMs: this.timeout, retries: this.retries }
|
|
@@ -1924,17 +2218,21 @@ var FacilitatorClient = class {
|
|
|
1924
2218
|
return {
|
|
1925
2219
|
isValid: false,
|
|
1926
2220
|
invalidReason: error.error,
|
|
1927
|
-
...failureFields(error)
|
|
2221
|
+
...failureFields(error),
|
|
2222
|
+
receipt: receiptFromErrorBody(error.body),
|
|
2223
|
+
idempotencyKey
|
|
1928
2224
|
};
|
|
1929
2225
|
}
|
|
1930
|
-
|
|
2226
|
+
const result = await response.json();
|
|
2227
|
+
return { ...result, ...admittedVerdict(result), receipt: parseFacilitatorReceipt(result.receipt), idempotencyKey };
|
|
1931
2228
|
} catch (error) {
|
|
1932
2229
|
return {
|
|
1933
2230
|
isValid: false,
|
|
1934
2231
|
invalidReason: error instanceof Error ? error.message : "Unknown error",
|
|
1935
2232
|
retryable: true,
|
|
1936
2233
|
safeToReplay: false,
|
|
1937
|
-
retryAfterSeconds: DEFAULT_RETRY_AFTER_SECONDS
|
|
2234
|
+
retryAfterSeconds: DEFAULT_RETRY_AFTER_SECONDS,
|
|
2235
|
+
idempotencyKey
|
|
1938
2236
|
};
|
|
1939
2237
|
}
|
|
1940
2238
|
}
|
|
@@ -1943,11 +2241,23 @@ var FacilitatorClient = class {
|
|
|
1943
2241
|
*
|
|
1944
2242
|
* Call this after providing the paid resource to execute the on-chain transfer.
|
|
1945
2243
|
*
|
|
2244
|
+
* Send the `idempotencyKey` the payment was verified with, and the same one
|
|
2245
|
+
* on any retry: it is the binding that earns the original answer back after
|
|
2246
|
+
* a lost response (`replayed: true`). Without it, or with another one, an
|
|
2247
|
+
* authorization already admitted comes back `409`
|
|
2248
|
+
* {@link AUTHORIZATION_ALREADY_SETTLED}, {@link AUTHORIZATION_IN_FLIGHT} or
|
|
2249
|
+
* {@link RECEIPT_REQUEST_CONFLICT} -- the X-PAYMENT was used; do not serve it
|
|
2250
|
+
* again. With no key and no context the call uses a fresh key, so a replay
|
|
2251
|
+
* it receives is refused the same way.
|
|
2252
|
+
*
|
|
1946
2253
|
* @param paymentHeader - Parsed x402 payment header
|
|
1947
2254
|
* @param requirements - Payment requirements
|
|
2255
|
+
* @param options - Purchase context and the payment's `idempotencyKey`
|
|
1948
2256
|
* @returns Settlement result with transaction hash
|
|
1949
2257
|
*/
|
|
1950
|
-
async settle(paymentHeader, requirements) {
|
|
2258
|
+
async settle(paymentHeader, requirements, options = {}) {
|
|
2259
|
+
const unbound = options.idempotencyKey === void 0 && !options.receiptContext;
|
|
2260
|
+
const idempotencyKey = checkIdempotencyKey(options.idempotencyKey ?? createIdempotencyKey());
|
|
1951
2261
|
const body = buildSettleRequestForVersion(
|
|
1952
2262
|
paymentHeader,
|
|
1953
2263
|
requirements,
|
|
@@ -1959,7 +2269,8 @@ var FacilitatorClient = class {
|
|
|
1959
2269
|
`${this.baseUrl}/settle`,
|
|
1960
2270
|
{
|
|
1961
2271
|
method: "POST",
|
|
1962
|
-
|
|
2272
|
+
// The same init, key included, goes out on every automatic retry.
|
|
2273
|
+
headers: facilitatorHeaders(idempotencyKey, options.receiptContext),
|
|
1963
2274
|
body: JSON.stringify(body)
|
|
1964
2275
|
},
|
|
1965
2276
|
{ timeoutMs: settleTimeout, retries: this.retries }
|
|
@@ -1968,7 +2279,9 @@ var FacilitatorClient = class {
|
|
|
1968
2279
|
return {
|
|
1969
2280
|
success: false,
|
|
1970
2281
|
error: error.error,
|
|
1971
|
-
...failureFields(error)
|
|
2282
|
+
...failureFields(error),
|
|
2283
|
+
receipt: receiptFromErrorBody(error.body),
|
|
2284
|
+
idempotencyKey
|
|
1972
2285
|
};
|
|
1973
2286
|
}
|
|
1974
2287
|
const result = await response.json();
|
|
@@ -1978,7 +2291,7 @@ var FacilitatorClient = class {
|
|
|
1978
2291
|
"[x402] settle reported success but carried no transaction hash under transaction/transactionHash/transaction_hash \u2014 treat delivery as unconfirmed"
|
|
1979
2292
|
);
|
|
1980
2293
|
}
|
|
1981
|
-
|
|
2294
|
+
const settled = {
|
|
1982
2295
|
// Read the facilitator's verdict instead of asserting one.
|
|
1983
2296
|
//
|
|
1984
2297
|
// This was the literal `true`, so a settle was reported successful
|
|
@@ -2006,15 +2319,24 @@ var FacilitatorClient = class {
|
|
|
2006
2319
|
// facilitator returns it, and this method threw it away — so a seller
|
|
2007
2320
|
// using the SDK end to end could only ever produce provisional anchors.
|
|
2008
2321
|
proofOfPayment: result.proofOfPayment ?? result.proof_of_payment,
|
|
2009
|
-
payer: result.payer
|
|
2322
|
+
payer: result.payer,
|
|
2323
|
+
receipt: parseFacilitatorReceipt(result.receipt),
|
|
2324
|
+
paymentId: result.paymentId,
|
|
2325
|
+
...result.receipt?.status === "unknown" || result.receipt?.status === "pending" ? { retryable: true, safeToReplay: false } : {},
|
|
2326
|
+
// `202 settlement_in_progress` and friends: a 2xx that names its state.
|
|
2327
|
+
...!result.success && typeof result.error === "string" && result.error ? { errorCode: result.error } : {},
|
|
2328
|
+
...isReplayed(response) ? { replayed: true } : {},
|
|
2329
|
+
idempotencyKey
|
|
2010
2330
|
};
|
|
2331
|
+
return unbound ? refuseUnboundReplay(settled) : settled;
|
|
2011
2332
|
} catch (error) {
|
|
2012
2333
|
return {
|
|
2013
2334
|
success: false,
|
|
2014
2335
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
2015
2336
|
retryable: true,
|
|
2016
2337
|
safeToReplay: false,
|
|
2017
|
-
retryAfterSeconds: DEFAULT_RETRY_AFTER_SECONDS
|
|
2338
|
+
retryAfterSeconds: DEFAULT_RETRY_AFTER_SECONDS,
|
|
2339
|
+
idempotencyKey
|
|
2018
2340
|
};
|
|
2019
2341
|
}
|
|
2020
2342
|
}
|
|
@@ -2024,27 +2346,41 @@ var FacilitatorClient = class {
|
|
|
2024
2346
|
* Convenience method that verifies first, then settles if valid.
|
|
2025
2347
|
* Use this for simple payment flows where you don't need custom logic between verify and settle.
|
|
2026
2348
|
*
|
|
2349
|
+
* Both steps carry ONE `Idempotency-Key` (`options.idempotencyKey`, or a
|
|
2350
|
+
* fresh one reported back as `idempotencyKey`). With a fresh key and no
|
|
2351
|
+
* purchase context, a replay of an earlier settle is not this purchase and
|
|
2352
|
+
* comes back `settled: false` with `errorCode`
|
|
2353
|
+
* {@link AUTHORIZATION_ALREADY_SETTLED}.
|
|
2354
|
+
*
|
|
2027
2355
|
* @param paymentHeader - Parsed x402 payment header
|
|
2028
2356
|
* @param requirements - Payment requirements
|
|
2357
|
+
* @param options - Purchase context and the payment's `idempotencyKey`
|
|
2029
2358
|
* @returns Combined result with verify and settle status
|
|
2030
2359
|
*/
|
|
2031
|
-
async verifyAndSettle(paymentHeader, requirements) {
|
|
2032
|
-
const
|
|
2360
|
+
async verifyAndSettle(paymentHeader, requirements, options = {}) {
|
|
2361
|
+
const unbound = options.idempotencyKey === void 0 && !options.receiptContext;
|
|
2362
|
+
const binding = { ...options, idempotencyKey: checkIdempotencyKey(options.idempotencyKey ?? createIdempotencyKey()) };
|
|
2363
|
+
const { idempotencyKey } = binding;
|
|
2364
|
+
const verifyResult = await this.verify(paymentHeader, requirements, binding);
|
|
2033
2365
|
if (!verifyResult.isValid) {
|
|
2034
2366
|
return {
|
|
2035
2367
|
verified: false,
|
|
2036
2368
|
settled: false,
|
|
2037
2369
|
error: verifyResult.invalidReason,
|
|
2038
|
-
...carryFailureFields(verifyResult)
|
|
2370
|
+
...carryFailureFields(verifyResult),
|
|
2371
|
+
idempotencyKey
|
|
2039
2372
|
};
|
|
2040
2373
|
}
|
|
2041
|
-
const
|
|
2374
|
+
const answered = await this.settle(paymentHeader, requirements, binding);
|
|
2375
|
+
const settleResult = unbound ? refuseUnboundReplay(answered) : answered;
|
|
2042
2376
|
return {
|
|
2043
2377
|
verified: true,
|
|
2044
2378
|
settled: settleResult.success,
|
|
2045
2379
|
transactionHash: settleResult.transactionHash,
|
|
2046
2380
|
error: settleResult.error,
|
|
2047
|
-
...carryFailureFields(settleResult)
|
|
2381
|
+
...carryFailureFields(settleResult),
|
|
2382
|
+
...settleResult.replayed ? { replayed: true } : {},
|
|
2383
|
+
idempotencyKey
|
|
2048
2384
|
};
|
|
2049
2385
|
}
|
|
2050
2386
|
/**
|
|
@@ -2432,20 +2768,51 @@ async function resolvePaymentRequirement(payment, requirements, resolver) {
|
|
|
2432
2768
|
}
|
|
2433
2769
|
return { requirement: matches[0] };
|
|
2434
2770
|
}
|
|
2435
|
-
function createVerifiedPaymentState(client, payment, requirements, verifyResult) {
|
|
2771
|
+
function createVerifiedPaymentState(client, payment, requirements, verifyResult, binding, onSettled) {
|
|
2436
2772
|
let settlePromise = null;
|
|
2437
2773
|
return {
|
|
2438
2774
|
payment,
|
|
2439
2775
|
requirements,
|
|
2440
2776
|
verifyResult,
|
|
2777
|
+
idempotencyKey: binding.idempotencyKey,
|
|
2441
2778
|
settle: () => {
|
|
2442
2779
|
if (!settlePromise) {
|
|
2443
|
-
settlePromise = client.settle(payment, requirements)
|
|
2780
|
+
settlePromise = client.settle(payment, requirements, binding).then((answer) => {
|
|
2781
|
+
const result = binding.receiptContext ? answer : refuseUnboundReplay(answer);
|
|
2782
|
+
try {
|
|
2783
|
+
onSettled?.(result);
|
|
2784
|
+
} catch (error) {
|
|
2785
|
+
console.warn("[x402] settled, but the payment response headers could not be attached:", error);
|
|
2786
|
+
}
|
|
2787
|
+
return result;
|
|
2788
|
+
});
|
|
2444
2789
|
}
|
|
2445
2790
|
return settlePromise;
|
|
2446
2791
|
}
|
|
2447
2792
|
};
|
|
2448
2793
|
}
|
|
2794
|
+
function propagatedResult(result) {
|
|
2795
|
+
const { idempotencyKey: _key, ...propagated } = result;
|
|
2796
|
+
return propagated;
|
|
2797
|
+
}
|
|
2798
|
+
function buildPaymentConflictResponse(failure) {
|
|
2799
|
+
if (isAuthorizationInFlight(failure)) {
|
|
2800
|
+
const { headers, body } = buildUnavailableResponse("Payment authorization in flight", failure);
|
|
2801
|
+
return { status: 503, headers, body: { ...body, reason: AUTHORIZATION_IN_FLIGHT } };
|
|
2802
|
+
}
|
|
2803
|
+
const used = isAuthorizationAlreadyUsed(failure);
|
|
2804
|
+
if (!used && failure.status !== 409) return null;
|
|
2805
|
+
return {
|
|
2806
|
+
status: 409,
|
|
2807
|
+
headers: {},
|
|
2808
|
+
body: {
|
|
2809
|
+
error: used ? "Payment authorization already used" : "Payment request conflict",
|
|
2810
|
+
reason: failure.errorCode ?? failure.invalidReason ?? failure.error,
|
|
2811
|
+
retryable: false,
|
|
2812
|
+
safeToReplay: false
|
|
2813
|
+
}
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2449
2816
|
function buildUnavailableResponse(message, failure) {
|
|
2450
2817
|
const seconds = Math.max(1, Math.ceil(failure.retryAfterSeconds ?? DEFAULT_RETRY_AFTER_SECONDS));
|
|
2451
2818
|
return {
|
|
@@ -2462,14 +2829,25 @@ function buildUnavailableResponse(message, failure) {
|
|
|
2462
2829
|
};
|
|
2463
2830
|
}
|
|
2464
2831
|
function respondUnavailable(res, message, failure) {
|
|
2465
|
-
|
|
2832
|
+
respondExpress(res, buildUnavailableResponse(message, failure));
|
|
2833
|
+
}
|
|
2834
|
+
function respondExpress(res, { status, headers, body }) {
|
|
2466
2835
|
const staged = res.status(status);
|
|
2467
|
-
if (typeof staged.set === "function") {
|
|
2836
|
+
if (Object.keys(headers).length > 0 && typeof staged.set === "function") {
|
|
2468
2837
|
staged.set(headers).json(body);
|
|
2469
2838
|
return;
|
|
2470
2839
|
}
|
|
2471
2840
|
staged.json(body);
|
|
2472
2841
|
}
|
|
2842
|
+
function setExpressPaymentHeaders(res, result) {
|
|
2843
|
+
if (!result.receipt || !res.set || res.headersSent) return;
|
|
2844
|
+
const current = (name) => {
|
|
2845
|
+
const value = res.getHeader ? res.getHeader(name) : res.get?.(name);
|
|
2846
|
+
if (value === void 0 || value === null) return void 0;
|
|
2847
|
+
return Array.isArray(value) ? value.join(", ") : String(value);
|
|
2848
|
+
};
|
|
2849
|
+
res.set(mergePaymentResponseHeaders(propagatedResult(result), current));
|
|
2850
|
+
}
|
|
2473
2851
|
function createPaymentMiddleware(getRequirements, options = {}) {
|
|
2474
2852
|
const client = new FacilitatorClient({
|
|
2475
2853
|
baseUrl: options.facilitatorUrl || options.baseUrl,
|
|
@@ -2487,8 +2865,28 @@ function createPaymentMiddleware(getRequirements, options = {}) {
|
|
|
2487
2865
|
}
|
|
2488
2866
|
const reqOptions = getRequirements(req);
|
|
2489
2867
|
const requirements = buildPaymentRequirements(reqOptions);
|
|
2490
|
-
|
|
2868
|
+
let receiptContext;
|
|
2869
|
+
try {
|
|
2870
|
+
const entry = Object.entries(req.headers).find(([name]) => name.toLowerCase() === "x-uvd-purchase")?.[1];
|
|
2871
|
+
if (entry !== void 0) {
|
|
2872
|
+
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");
|
|
2873
|
+
const actualUrl = new URL(req.originalUrl || req.url || reqOptions.resource, reqOptions.resource).toString();
|
|
2874
|
+
receiptContext = validatePurchaseContext(entry, req.method, actualUrl, req.rawBody || new Uint8Array());
|
|
2875
|
+
requirements.resource = actualUrl;
|
|
2876
|
+
}
|
|
2877
|
+
} catch {
|
|
2878
|
+
res.status(400).json({ error: "receipt_context_mismatch" });
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
const binding = { idempotencyKey: createIdempotencyKey(), receiptContext };
|
|
2882
|
+
const verifyResult = await client.verify(payment, requirements, binding);
|
|
2883
|
+
setExpressPaymentHeaders(res, verifyResult);
|
|
2491
2884
|
if (!verifyResult.isValid) {
|
|
2885
|
+
const conflict = buildPaymentConflictResponse(verifyResult);
|
|
2886
|
+
if (conflict) {
|
|
2887
|
+
respondExpress(res, conflict);
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2492
2890
|
if (verifyResult.retryable) {
|
|
2493
2891
|
respondUnavailable(res, "Payment verification unavailable", verifyResult);
|
|
2494
2892
|
return;
|
|
@@ -2499,10 +2897,22 @@ function createPaymentMiddleware(getRequirements, options = {}) {
|
|
|
2499
2897
|
});
|
|
2500
2898
|
return;
|
|
2501
2899
|
}
|
|
2502
|
-
req.x402 = createVerifiedPaymentState(
|
|
2900
|
+
req.x402 = createVerifiedPaymentState(
|
|
2901
|
+
client,
|
|
2902
|
+
payment,
|
|
2903
|
+
requirements,
|
|
2904
|
+
verifyResult,
|
|
2905
|
+
binding,
|
|
2906
|
+
(result) => setExpressPaymentHeaders(res, result)
|
|
2907
|
+
);
|
|
2503
2908
|
if (settlementStrategy === "before-handler") {
|
|
2504
2909
|
const settleResult = await req.x402.settle();
|
|
2505
2910
|
if (!settleResult.success) {
|
|
2911
|
+
const conflict = buildPaymentConflictResponse(settleResult);
|
|
2912
|
+
if (conflict) {
|
|
2913
|
+
respondExpress(res, conflict);
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2506
2916
|
if (settleResult.retryable) {
|
|
2507
2917
|
respondUnavailable(res, "Payment settlement unavailable", settleResult);
|
|
2508
2918
|
return;
|
|
@@ -2525,12 +2935,21 @@ function settlementFailureBody(failure, fallbackReason) {
|
|
|
2525
2935
|
};
|
|
2526
2936
|
}
|
|
2527
2937
|
function honoUnavailable(c, message, failure) {
|
|
2528
|
-
|
|
2938
|
+
return respondHono(c, buildUnavailableResponse(message, failure));
|
|
2939
|
+
}
|
|
2940
|
+
function respondHono(c, { status, headers, body }) {
|
|
2529
2941
|
for (const [name, value] of Object.entries(headers)) {
|
|
2530
2942
|
c.header?.(name, value);
|
|
2531
2943
|
}
|
|
2532
2944
|
return c.json(body, status);
|
|
2533
2945
|
}
|
|
2946
|
+
function setHonoPaymentHeaders(c, result) {
|
|
2947
|
+
if (!result.receipt || !c.header || c.finalized) return;
|
|
2948
|
+
const current = (name) => c.res?.headers.get(name) ?? void 0;
|
|
2949
|
+
for (const [name, value] of Object.entries(mergePaymentResponseHeaders(propagatedResult(result), current))) {
|
|
2950
|
+
c.header(name, value);
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2534
2953
|
function createHonoMiddleware(options) {
|
|
2535
2954
|
const client = new FacilitatorClient({
|
|
2536
2955
|
baseUrl: options.facilitatorUrl || options.baseUrl,
|
|
@@ -2542,7 +2961,7 @@ function createHonoMiddleware(options) {
|
|
|
2542
2961
|
throw new Error("At least one accept entry is required");
|
|
2543
2962
|
}
|
|
2544
2963
|
return async (c, next) => {
|
|
2545
|
-
const paymentHeader = c.req.header("X-PAYMENT") || c.req.header("x-payment");
|
|
2964
|
+
const paymentHeader = c.req.header("PAYMENT-SIGNATURE") || c.req.header("X-PAYMENT") || c.req.header("x-payment");
|
|
2546
2965
|
const advertisedVersion = resolveAdvertisedVersion(options.accepts, options.x402Version);
|
|
2547
2966
|
const advertisedRequirements = options.accepts.map(
|
|
2548
2967
|
(accept) => buildRequirementFromAcceptance(accept, c.req.url, advertisedVersion)
|
|
@@ -2573,8 +2992,24 @@ function createHonoMiddleware(options) {
|
|
|
2573
2992
|
reason
|
|
2574
2993
|
}, 402);
|
|
2575
2994
|
}
|
|
2576
|
-
|
|
2995
|
+
let receiptContext;
|
|
2996
|
+
try {
|
|
2997
|
+
const header = c.req.header("X-UVD-Purchase");
|
|
2998
|
+
if (header) {
|
|
2999
|
+
const method = c.req.method || c.req.raw?.method;
|
|
3000
|
+
if (!method || !["GET", "HEAD"].includes(method) && !c.req.raw) throw new Error("raw request required");
|
|
3001
|
+
const bytes = c.req.raw ? new Uint8Array(await c.req.raw.clone().arrayBuffer()) : new Uint8Array();
|
|
3002
|
+
receiptContext = validatePurchaseContext(header, method, c.req.url, bytes);
|
|
3003
|
+
}
|
|
3004
|
+
} catch {
|
|
3005
|
+
return c.json({ error: "receipt_context_mismatch" }, 400);
|
|
3006
|
+
}
|
|
3007
|
+
const binding = { idempotencyKey: createIdempotencyKey(), receiptContext };
|
|
3008
|
+
const verifyResult = await client.verify(parsed, requirement, binding);
|
|
3009
|
+
setHonoPaymentHeaders(c, verifyResult);
|
|
2577
3010
|
if (!verifyResult.isValid) {
|
|
3011
|
+
const conflict = buildPaymentConflictResponse(verifyResult);
|
|
3012
|
+
if (conflict) return respondHono(c, conflict);
|
|
2578
3013
|
if (verifyResult.retryable) {
|
|
2579
3014
|
return honoUnavailable(c, "Payment verification unavailable", verifyResult);
|
|
2580
3015
|
}
|
|
@@ -2583,11 +3018,20 @@ function createHonoMiddleware(options) {
|
|
|
2583
3018
|
reason: verifyResult.invalidReason
|
|
2584
3019
|
}, 402);
|
|
2585
3020
|
}
|
|
2586
|
-
const verifiedPayment = createVerifiedPaymentState(
|
|
3021
|
+
const verifiedPayment = createVerifiedPaymentState(
|
|
3022
|
+
client,
|
|
3023
|
+
parsed,
|
|
3024
|
+
requirement,
|
|
3025
|
+
verifyResult,
|
|
3026
|
+
binding,
|
|
3027
|
+
(result) => setHonoPaymentHeaders(c, result)
|
|
3028
|
+
);
|
|
2587
3029
|
c.set?.("x402", verifiedPayment);
|
|
2588
3030
|
if (settlementStrategy === "before-handler") {
|
|
2589
3031
|
const settleResult = await verifiedPayment.settle();
|
|
2590
3032
|
if (!settleResult.success) {
|
|
3033
|
+
const conflict = buildPaymentConflictResponse(settleResult);
|
|
3034
|
+
if (conflict) return respondHono(c, conflict);
|
|
2591
3035
|
if (settleResult.retryable) {
|
|
2592
3036
|
return honoUnavailable(c, "Payment settlement unavailable", settleResult);
|
|
2593
3037
|
}
|
|
@@ -5478,6 +5922,6 @@ var AdvancedEscrowClient = class {
|
|
|
5478
5922
|
}
|
|
5479
5923
|
};
|
|
5480
5924
|
|
|
5481
|
-
export { AMBIGUOUS_LEASE_REASONS, AdvancedEscrowClient, BASE_MAINNET_CONTRACTS, BazaarClient, DEFAULT_FACILITATOR_RETRIES, DEFAULT_RETRY_AFTER_SECONDS, DEPOSIT_LIMIT_USDC, ERC8004_CONTRACTS, ERC8004_EXTENSION_ID, ESCROW_CONTRACTS, ESCROW_TIMEOUT_MS, Erc8004Client, Erc8004LookupError, EscrowClient, FacilitatorClient, HEALTH_FILTERS, MAX_RETRY_AFTER_SECONDS, MAX_SEARCH_LEN, OPERATOR_ABI, OPERATOR_ABI_CREATE3, PAYMENT_INFO_TYPEHASH, RELAYED_FEEDBACK_NETWORKS, REPLAYABLE_LEASE_REASONS, RegistrationPendingError, SETTLEMENT_UNCONFIRMED, SOLANA_FEEDBACK_NETWORKS, TIER_FILTERS, TIER_TIMINGS, USDC_DOMAIN_NAME, WRITER_LEASE_REASONS, X402_CORS_HEADERS, X402_HEADER_NAMES, ZERO_ADDRESS, buildErc8004PaymentRequirements, buildPaymentRequirements, buildSettleRequest, buildSettleRequestForVersion, buildSettleRequestV2, buildUnavailableResponse, buildVerifyRequest, buildVerifyRequestForVersion, buildVerifyRequestV2, canRefundEscrow, canReleaseEscrow, carryFailureFields, create402Response, createHonoMiddleware, createPaymentMiddleware, epochToDate, escrowTimeRemaining, extractPaymentFromHeaders, facilitatorFetch, getCorsHeaders, getEscrowContractsByChainId, getEscrowSupportedChainIds, isAlive, isAmbiguousLeaseReason, isEscrowExpired, isEscrowSupportedOnChain, isRegisterJobTerminal, isReplayableLeaseReason, isSettlementUnconfirmed, parseFacilitatorErrorBody, parsePaymentHeader, parseRetryAfterSeconds, readFacilitatorError, resolveEnvelopeVersion, supportsRelayedFeedback, supportsSolanaFeedback, toPaymentRequirementsV2, toResourceInfoV2, wireNetwork };
|
|
5925
|
+
export { AMBIGUOUS_LEASE_REASONS, AUTHORIZATION_ALREADY_SETTLED, AUTHORIZATION_IN_FLIGHT, AdvancedEscrowClient, BASE_MAINNET_CONTRACTS, BazaarClient, DEFAULT_FACILITATOR_RETRIES, DEFAULT_RETRY_AFTER_SECONDS, DEPOSIT_LIMIT_USDC, ERC8004_CONTRACTS, ERC8004_EXTENSION_ID, ESCROW_CONTRACTS, ESCROW_TIMEOUT_MS, Erc8004Client, Erc8004LookupError, EscrowClient, FacilitatorClient, HEALTH_FILTERS, IDEMPOTENCY_KEY_HEADER, IDEMPOTENT_REPLAYED_HEADER, MAX_RETRY_AFTER_SECONDS, MAX_SEARCH_LEN, OPERATOR_ABI, OPERATOR_ABI_CREATE3, PAYMENT_INFO_TYPEHASH, RECEIPT_ISSUER, RECEIPT_REQUEST_CONFLICT, RELAYED_FEEDBACK_NETWORKS, REPLAYABLE_LEASE_REASONS, RegistrationPendingError, SETTLEMENT_UNCONFIRMED, SOLANA_FEEDBACK_NETWORKS, TIER_FILTERS, TIER_TIMINGS, USDC_DOMAIN_NAME, WRITER_LEASE_REASONS, X402_CORS_HEADERS, X402_HEADER_NAMES, ZERO_ADDRESS, buildErc8004PaymentRequirements, buildPaymentConflictResponse, buildPaymentRequirements, buildSettleRequest, buildSettleRequestForVersion, buildSettleRequestV2, buildUnavailableResponse, buildVerifyRequest, buildVerifyRequestForVersion, buildVerifyRequestV2, canRefundEscrow, canReleaseEscrow, canonicalReceiptJson, carryFailureFields, create402Response, createHonoMiddleware, createIdempotencyKey, createPaymentMiddleware, createPurchaseContext, epochToDate, escrowTimeRemaining, extractPaymentFromHeaders, facilitatorFetch, fetchWithReceipt, getCorsHeaders, getEscrowContractsByChainId, getEscrowSupportedChainIds, getFacilitatorReceipt, isAlive, isAmbiguousLeaseReason, isAuthorizationAlreadyUsed, isAuthorizationInFlight, isEscrowExpired, isEscrowSupportedOnChain, isRegisterJobTerminal, isReplayableLeaseReason, isSettlementUnconfirmed, mergeHeaderList, mergePaymentResponseHeaders, parseFacilitatorErrorBody, parseFacilitatorReceipt, parsePaymentHeader, parseRetryAfterSeconds, paymentResponseHeaders, purchaseContextHeader, readFacilitatorError, receiptCommitment, receiptFromErrorBody, receiptFromResponse, resolveEnvelopeVersion, supportsRelayedFeedback, supportsSolanaFeedback, toPaymentRequirementsV2, toResourceInfoV2, validatePurchaseContext, verifyFacilitatorReceipt, wireNetwork };
|
|
5482
5926
|
//# sourceMappingURL=index.mjs.map
|
|
5483
5927
|
//# sourceMappingURL=index.mjs.map
|