@bankofai/x402-core 1.0.0-beta.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 +294 -0
- package/dist/cjs/client/index.d.ts +149 -0
- package/dist/cjs/client/index.js +909 -0
- package/dist/cjs/client/index.js.map +1 -0
- package/dist/cjs/facilitator/index.d.ts +206 -0
- package/dist/cjs/facilitator/index.js +431 -0
- package/dist/cjs/facilitator/index.js.map +1 -0
- package/dist/cjs/http/index.d.ts +50 -0
- package/dist/cjs/http/index.js +1452 -0
- package/dist/cjs/http/index.js.map +1 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.js +31 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/schemas/index.d.ts +891 -0
- package/dist/cjs/schemas/index.js +216 -0
- package/dist/cjs/schemas/index.js.map +1 -0
- package/dist/cjs/server/index.d.ts +79 -0
- package/dist/cjs/server/index.js +2568 -0
- package/dist/cjs/server/index.js.map +1 -0
- package/dist/cjs/types/index.d.ts +1 -0
- package/dist/cjs/types/index.js +97 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/cjs/types/v1/index.d.ts +1 -0
- package/dist/cjs/types/v1/index.js +19 -0
- package/dist/cjs/types/v1/index.js.map +1 -0
- package/dist/cjs/utils/index.d.ts +77 -0
- package/dist/cjs/utils/index.js +179 -0
- package/dist/cjs/utils/index.js.map +1 -0
- package/dist/cjs/x402Client-BgegfQgE.d.ts +1807 -0
- package/dist/cjs/x402Client-TQHctrG7.d.ts +1807 -0
- package/dist/esm/chunk-ABS7D6VX.mjs +145 -0
- package/dist/esm/chunk-ABS7D6VX.mjs.map +1 -0
- package/dist/esm/chunk-AGOUMC4P.mjs +68 -0
- package/dist/esm/chunk-AGOUMC4P.mjs.map +1 -0
- package/dist/esm/chunk-BJTO5JO5.mjs +11 -0
- package/dist/esm/chunk-BJTO5JO5.mjs.map +1 -0
- package/dist/esm/chunk-FPXAE3OS.mjs +161 -0
- package/dist/esm/chunk-FPXAE3OS.mjs.map +1 -0
- package/dist/esm/chunk-IL77TMJL.mjs +1275 -0
- package/dist/esm/chunk-IL77TMJL.mjs.map +1 -0
- package/dist/esm/chunk-VE37GDG2.mjs +7 -0
- package/dist/esm/chunk-VE37GDG2.mjs.map +1 -0
- package/dist/esm/client/index.d.mts +149 -0
- package/dist/esm/client/index.mjs +504 -0
- package/dist/esm/client/index.mjs.map +1 -0
- package/dist/esm/facilitator/index.d.mts +206 -0
- package/dist/esm/facilitator/index.mjs +399 -0
- package/dist/esm/facilitator/index.mjs.map +1 -0
- package/dist/esm/http/index.d.mts +50 -0
- package/dist/esm/http/index.mjs +35 -0
- package/dist/esm/http/index.mjs.map +1 -0
- package/dist/esm/index.d.mts +3 -0
- package/dist/esm/index.mjs +8 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/schemas/index.d.mts +891 -0
- package/dist/esm/schemas/index.mjs +70 -0
- package/dist/esm/schemas/index.mjs.map +1 -0
- package/dist/esm/server/index.d.mts +79 -0
- package/dist/esm/server/index.mjs +1318 -0
- package/dist/esm/server/index.mjs.map +1 -0
- package/dist/esm/types/index.d.mts +1 -0
- package/dist/esm/types/index.mjs +14 -0
- package/dist/esm/types/index.mjs.map +1 -0
- package/dist/esm/types/v1/index.d.mts +1 -0
- package/dist/esm/types/v1/index.mjs +1 -0
- package/dist/esm/types/v1/index.mjs.map +1 -0
- package/dist/esm/utils/index.d.mts +77 -0
- package/dist/esm/utils/index.mjs +28 -0
- package/dist/esm/utils/index.mjs.map +1 -0
- package/dist/esm/x402Client-BgegfQgE.d.mts +1807 -0
- package/dist/esm/x402Client-TQHctrG7.d.mts +1807 -0
- package/package.json +142 -0
|
@@ -0,0 +1,2568 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server/index.ts
|
|
21
|
+
var server_exports = {};
|
|
22
|
+
__export(server_exports, {
|
|
23
|
+
FacilitatorResponseError: () => FacilitatorResponseError,
|
|
24
|
+
HTTPFacilitatorClient: () => HTTPFacilitatorClient,
|
|
25
|
+
RouteConfigurationError: () => RouteConfigurationError,
|
|
26
|
+
SETTLEMENT_OVERRIDES_HEADER: () => SETTLEMENT_OVERRIDES_HEADER,
|
|
27
|
+
assertAcceptsAdditiveExtraAfterSchemeEnrich: () => assertAcceptsAdditiveExtraAfterSchemeEnrich,
|
|
28
|
+
assertAcceptsAllowlistedAfterExtensionEnrich: () => assertAcceptsAllowlistedAfterExtensionEnrich,
|
|
29
|
+
assertAdditivePayloadEnrichment: () => assertAdditivePayloadEnrichment,
|
|
30
|
+
assertAdditiveSettlementExtra: () => assertAdditiveSettlementExtra,
|
|
31
|
+
assertSettleResponseCoreUnchanged: () => assertSettleResponseCoreUnchanged,
|
|
32
|
+
checkIfBazaarNeeded: () => checkIfBazaarNeeded,
|
|
33
|
+
getFacilitatorResponseError: () => getFacilitatorResponseError,
|
|
34
|
+
isVacantStringField: () => isVacantStringField,
|
|
35
|
+
snapshotPaymentRequirementsList: () => snapshotPaymentRequirementsList,
|
|
36
|
+
snapshotSettleResponseCore: () => snapshotSettleResponseCore,
|
|
37
|
+
x402HTTPResourceServer: () => x402HTTPResourceServer,
|
|
38
|
+
x402ResourceServer: () => x402ResourceServer
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(server_exports);
|
|
41
|
+
|
|
42
|
+
// src/types/facilitator.ts
|
|
43
|
+
var VerifyError = class extends Error {
|
|
44
|
+
/**
|
|
45
|
+
* Creates a VerifyError from a failed verification response.
|
|
46
|
+
*
|
|
47
|
+
* @param statusCode - HTTP status code from the facilitator
|
|
48
|
+
* @param response - The verify response containing failure details
|
|
49
|
+
*/
|
|
50
|
+
constructor(statusCode, response) {
|
|
51
|
+
const reason = response.invalidReason || "unknown reason";
|
|
52
|
+
const message = response.invalidMessage;
|
|
53
|
+
super(message ? `${reason}: ${message}` : reason);
|
|
54
|
+
this.name = "VerifyError";
|
|
55
|
+
this.statusCode = statusCode;
|
|
56
|
+
this.invalidReason = response.invalidReason;
|
|
57
|
+
this.invalidMessage = response.invalidMessage;
|
|
58
|
+
this.payer = response.payer;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var SettleError = class extends Error {
|
|
62
|
+
/**
|
|
63
|
+
* Creates a SettleError from a failed settlement response.
|
|
64
|
+
*
|
|
65
|
+
* @param statusCode - HTTP status code from the facilitator
|
|
66
|
+
* @param response - The settle response containing error details
|
|
67
|
+
*/
|
|
68
|
+
constructor(statusCode, response) {
|
|
69
|
+
const reason = response.errorReason || "unknown reason";
|
|
70
|
+
const message = response.errorMessage;
|
|
71
|
+
super(message ? `${reason}: ${message}` : reason);
|
|
72
|
+
this.name = "SettleError";
|
|
73
|
+
this.statusCode = statusCode;
|
|
74
|
+
this.errorReason = response.errorReason;
|
|
75
|
+
this.errorMessage = response.errorMessage;
|
|
76
|
+
this.payer = response.payer;
|
|
77
|
+
this.transaction = response.transaction;
|
|
78
|
+
this.network = response.network;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var FacilitatorResponseError = class extends Error {
|
|
82
|
+
/**
|
|
83
|
+
* Creates a FacilitatorResponseError for malformed facilitator responses.
|
|
84
|
+
*
|
|
85
|
+
* @param message - The boundary error message
|
|
86
|
+
*/
|
|
87
|
+
constructor(message) {
|
|
88
|
+
super(message);
|
|
89
|
+
this.name = "FacilitatorResponseError";
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
function getFacilitatorResponseError(error) {
|
|
93
|
+
let current = error;
|
|
94
|
+
while (current instanceof Error) {
|
|
95
|
+
if (current instanceof FacilitatorResponseError) {
|
|
96
|
+
return current;
|
|
97
|
+
}
|
|
98
|
+
current = current.cause;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/utils/index.ts
|
|
104
|
+
var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
105
|
+
var networkPatternToRegExp = (pattern) => {
|
|
106
|
+
const source = escapeRegExp(pattern).replace(/\\\*/g, ".*");
|
|
107
|
+
return new RegExp(`^${source}$`);
|
|
108
|
+
};
|
|
109
|
+
var networkMatchesPattern = (pattern, network) => {
|
|
110
|
+
return networkPatternToRegExp(pattern).test(network);
|
|
111
|
+
};
|
|
112
|
+
var findSchemesByNetwork = (map, network) => {
|
|
113
|
+
let implementationsByScheme = map.get(network);
|
|
114
|
+
if (!implementationsByScheme) {
|
|
115
|
+
for (const [registeredNetworkPattern, implementations] of map.entries()) {
|
|
116
|
+
if (networkMatchesPattern(registeredNetworkPattern, network)) {
|
|
117
|
+
implementationsByScheme = implementations;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return implementationsByScheme;
|
|
123
|
+
};
|
|
124
|
+
var findByNetworkAndScheme = (map, scheme, network) => {
|
|
125
|
+
return findSchemesByNetwork(map, network)?.get(scheme);
|
|
126
|
+
};
|
|
127
|
+
var Base64EncodedRegex = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
128
|
+
function safeBase64Encode(data) {
|
|
129
|
+
if (typeof globalThis !== "undefined" && typeof globalThis.btoa === "function") {
|
|
130
|
+
const bytes = new TextEncoder().encode(data);
|
|
131
|
+
const binaryString = Array.from(bytes, (byte) => String.fromCharCode(byte)).join("");
|
|
132
|
+
return globalThis.btoa(binaryString);
|
|
133
|
+
}
|
|
134
|
+
return Buffer.from(data, "utf8").toString("base64");
|
|
135
|
+
}
|
|
136
|
+
function safeBase64Decode(data) {
|
|
137
|
+
if (typeof globalThis !== "undefined" && typeof globalThis.atob === "function") {
|
|
138
|
+
const binaryString = globalThis.atob(data);
|
|
139
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
140
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
141
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
142
|
+
}
|
|
143
|
+
const decoder = new TextDecoder("utf-8");
|
|
144
|
+
return decoder.decode(bytes);
|
|
145
|
+
}
|
|
146
|
+
return Buffer.from(data, "base64").toString("utf-8");
|
|
147
|
+
}
|
|
148
|
+
function deepEqual(obj1, obj2) {
|
|
149
|
+
const normalize = (obj) => {
|
|
150
|
+
if (obj === null || obj === void 0) return JSON.stringify(obj);
|
|
151
|
+
if (typeof obj !== "object") return JSON.stringify(obj);
|
|
152
|
+
if (Array.isArray(obj)) {
|
|
153
|
+
return JSON.stringify(
|
|
154
|
+
obj.map(
|
|
155
|
+
(item) => typeof item === "object" && item !== null ? JSON.parse(normalize(item)) : item
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const sorted = {};
|
|
160
|
+
Object.keys(obj).sort().forEach((key) => {
|
|
161
|
+
const value = obj[key];
|
|
162
|
+
sorted[key] = typeof value === "object" && value !== null ? JSON.parse(normalize(value)) : value;
|
|
163
|
+
});
|
|
164
|
+
return JSON.stringify(sorted);
|
|
165
|
+
};
|
|
166
|
+
try {
|
|
167
|
+
return normalize(obj1) === normalize(obj2);
|
|
168
|
+
} catch {
|
|
169
|
+
return JSON.stringify(obj1) === JSON.stringify(obj2);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/server/hookPolicy.ts
|
|
174
|
+
function isVacantStringField(value) {
|
|
175
|
+
return value.trim() === "";
|
|
176
|
+
}
|
|
177
|
+
function snapshotPaymentRequirementsList(requirements) {
|
|
178
|
+
return requirements.map((req) => ({
|
|
179
|
+
...req,
|
|
180
|
+
extra: structuredClone(req.extra)
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
function assertAcceptsAllowlistedAfterExtensionEnrich(baseline, current, extensionKey) {
|
|
184
|
+
if (baseline.length !== current.length) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`[x402] extension "${extensionKey}" violated accepts mutation policy: accepts length changed (${baseline.length} \u2192 ${current.length})`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
for (let i = 0; i < baseline.length; i++) {
|
|
190
|
+
const b = baseline[i];
|
|
191
|
+
const c = current[i];
|
|
192
|
+
if (b.scheme !== c.scheme || b.network !== c.network) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`[x402] extension "${extensionKey}" violated accepts mutation policy: scheme/network are immutable (index ${i})`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (b.maxTimeoutSeconds !== c.maxTimeoutSeconds) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`[x402] extension "${extensionKey}" violated accepts mutation policy: maxTimeoutSeconds is immutable (index ${i})`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
for (const field of ["payTo", "amount", "asset"]) {
|
|
203
|
+
const bv = b[field];
|
|
204
|
+
const cv = c[field];
|
|
205
|
+
if (!isVacantStringField(bv) && cv !== bv) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`[x402] extension "${extensionKey}" violated accepts mutation policy: "${field}" may only be set when the resource left it vacant (""); non-vacant values are immutable (index ${i})`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (const key of Object.keys(b.extra)) {
|
|
212
|
+
if (!Object.prototype.hasOwnProperty.call(c.extra, key)) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`[x402] extension "${extensionKey}" violated accepts mutation policy: extra["${key}"] was removed (index ${i})`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (!deepEqual(c.extra[key], b.extra[key])) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`[x402] extension "${extensionKey}" violated accepts mutation policy: extra["${key}"] may not be changed (index ${i})`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function assertAcceptsAdditiveExtraAfterSchemeEnrich(baseline, current, scheme, network) {
|
|
226
|
+
if (baseline.length !== current.length) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`[x402] scheme "${scheme}" violated accepts mutation policy: accepts length changed (${baseline.length} \u2192 ${current.length})`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
for (let i = 0; i < baseline.length; i++) {
|
|
232
|
+
const b = baseline[i];
|
|
233
|
+
const c = current[i];
|
|
234
|
+
const isMatchingAccept = b.scheme === scheme && b.network === network;
|
|
235
|
+
if (b.scheme !== c.scheme || b.network !== c.network) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`[x402] scheme "${scheme}" violated accepts mutation policy: scheme/network are immutable (index ${i})`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (b.maxTimeoutSeconds !== c.maxTimeoutSeconds || b.payTo !== c.payTo || b.amount !== c.amount || b.asset !== c.asset) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`[x402] scheme "${scheme}" violated accepts mutation policy: payment terms are immutable (index ${i})`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
for (const key of Object.keys(b.extra)) {
|
|
246
|
+
if (!Object.prototype.hasOwnProperty.call(c.extra, key)) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`[x402] scheme "${scheme}" violated accepts mutation policy: extra["${key}"] was removed (index ${i})`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (!deepEqual(c.extra[key], b.extra[key])) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`[x402] scheme "${scheme}" violated accepts mutation policy: extra["${key}"] may not be changed (index ${i})`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!isMatchingAccept && Object.keys(c.extra).length !== Object.keys(b.extra).length) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`[x402] scheme "${scheme}" violated accepts mutation policy: only matching accepts may receive new extra fields (index ${i})`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function snapshotSettleResponseCore(result) {
|
|
265
|
+
return {
|
|
266
|
+
success: result.success,
|
|
267
|
+
transaction: result.transaction,
|
|
268
|
+
network: result.network,
|
|
269
|
+
amount: result.amount,
|
|
270
|
+
payer: result.payer,
|
|
271
|
+
errorReason: result.errorReason,
|
|
272
|
+
errorMessage: result.errorMessage
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function assertSettleResponseCoreUnchanged(before, after, extensionKey) {
|
|
276
|
+
const keys = [
|
|
277
|
+
"success",
|
|
278
|
+
"transaction",
|
|
279
|
+
"network",
|
|
280
|
+
"amount",
|
|
281
|
+
"payer",
|
|
282
|
+
"errorReason",
|
|
283
|
+
"errorMessage"
|
|
284
|
+
];
|
|
285
|
+
for (const k of keys) {
|
|
286
|
+
if (!deepEqual(after[k], before[k])) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`[x402] extension "${extensionKey}" violated settlement mutation policy: field "${String(k)}" is immutable after facilitator settle`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function assertAdditivePayloadEnrichment(payload, enrichment, callerLabel) {
|
|
294
|
+
for (const key of Object.keys(enrichment)) {
|
|
295
|
+
if (!Object.prototype.hasOwnProperty.call(payload, key)) continue;
|
|
296
|
+
throw new Error(
|
|
297
|
+
`[x402] ${callerLabel} violated settlement payload enrichment policy: "${key}" already exists on the client payload`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function isPlainRecord(value) {
|
|
302
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
303
|
+
}
|
|
304
|
+
function assertAdditiveSettlementExtra(extra, enrichment, callerLabel) {
|
|
305
|
+
assertAdditiveRecord(extra, enrichment, callerLabel, "extra");
|
|
306
|
+
}
|
|
307
|
+
function mergeAdditiveSettlementExtra(extra, enrichment) {
|
|
308
|
+
return mergeAdditiveRecord(extra, enrichment);
|
|
309
|
+
}
|
|
310
|
+
function assertAdditiveRecord(target, enrichment, callerLabel, path) {
|
|
311
|
+
for (const [key, enrichmentValue] of Object.entries(enrichment)) {
|
|
312
|
+
const nextPath = `${path}["${key}"]`;
|
|
313
|
+
if (!Object.prototype.hasOwnProperty.call(target, key)) continue;
|
|
314
|
+
const targetValue = target[key];
|
|
315
|
+
if (isPlainRecord(targetValue) && isPlainRecord(enrichmentValue)) {
|
|
316
|
+
assertAdditiveRecord(targetValue, enrichmentValue, callerLabel, nextPath);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
throw new Error(
|
|
320
|
+
`[x402] ${callerLabel} violated settlement response enrichment policy: ${nextPath} already exists on the settlement result`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function mergeAdditiveRecord(target, enrichment) {
|
|
325
|
+
const merged = { ...target };
|
|
326
|
+
for (const [key, enrichmentValue] of Object.entries(enrichment)) {
|
|
327
|
+
const targetValue = merged[key];
|
|
328
|
+
if (isPlainRecord(targetValue) && isPlainRecord(enrichmentValue)) {
|
|
329
|
+
merged[key] = mergeAdditiveRecord(targetValue, enrichmentValue);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
merged[key] = enrichmentValue;
|
|
333
|
+
}
|
|
334
|
+
return merged;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/schemas/index.ts
|
|
338
|
+
var import_zod = require("zod");
|
|
339
|
+
var import_zod2 = require("zod");
|
|
340
|
+
var NonEmptyString = import_zod.z.string().min(1);
|
|
341
|
+
var Any = import_zod.z.record(import_zod.z.unknown());
|
|
342
|
+
var OptionalAny = import_zod.z.record(import_zod.z.unknown()).optional().nullable();
|
|
343
|
+
var NetworkSchemaV1 = NonEmptyString;
|
|
344
|
+
var NetworkSchemaV2 = import_zod.z.string().min(3).refine((val) => val.includes(":"), {
|
|
345
|
+
message: "Network must be in CAIP-2 format (e.g., 'eip155:84532')"
|
|
346
|
+
});
|
|
347
|
+
var NetworkSchema = import_zod.z.union([NetworkSchemaV1, NetworkSchemaV2]);
|
|
348
|
+
var PRINTABLE_ASCII_REGEX = /^[\x20-\x7e]+$/;
|
|
349
|
+
var ResourceInfoSchema = import_zod.z.object({
|
|
350
|
+
url: NonEmptyString,
|
|
351
|
+
description: import_zod.z.string().optional(),
|
|
352
|
+
mimeType: import_zod.z.string().optional(),
|
|
353
|
+
serviceName: import_zod.z.string().min(1).max(32).regex(PRINTABLE_ASCII_REGEX).optional(),
|
|
354
|
+
tags: import_zod.z.array(import_zod.z.string().min(1).max(32).regex(PRINTABLE_ASCII_REGEX)).max(5).optional(),
|
|
355
|
+
iconUrl: import_zod.z.string().max(2048).optional()
|
|
356
|
+
});
|
|
357
|
+
var PaymentRequirementsV1Schema = import_zod.z.object({
|
|
358
|
+
scheme: NonEmptyString,
|
|
359
|
+
network: NetworkSchemaV1,
|
|
360
|
+
maxAmountRequired: NonEmptyString,
|
|
361
|
+
resource: NonEmptyString,
|
|
362
|
+
// URL string in V1
|
|
363
|
+
description: import_zod.z.string(),
|
|
364
|
+
mimeType: import_zod.z.string().optional(),
|
|
365
|
+
outputSchema: Any.optional().nullable(),
|
|
366
|
+
payTo: NonEmptyString,
|
|
367
|
+
maxTimeoutSeconds: import_zod.z.number().positive(),
|
|
368
|
+
asset: NonEmptyString,
|
|
369
|
+
extra: OptionalAny
|
|
370
|
+
});
|
|
371
|
+
var PaymentRequiredV1Schema = import_zod.z.object({
|
|
372
|
+
x402Version: import_zod.z.literal(1),
|
|
373
|
+
error: import_zod.z.string().optional(),
|
|
374
|
+
accepts: import_zod.z.array(PaymentRequirementsV1Schema).min(1)
|
|
375
|
+
});
|
|
376
|
+
var PaymentPayloadV1Schema = import_zod.z.object({
|
|
377
|
+
x402Version: import_zod.z.literal(1),
|
|
378
|
+
scheme: NonEmptyString,
|
|
379
|
+
network: NetworkSchemaV1,
|
|
380
|
+
payload: Any
|
|
381
|
+
});
|
|
382
|
+
var PaymentRequirementsV2Schema = import_zod.z.object({
|
|
383
|
+
scheme: NonEmptyString,
|
|
384
|
+
network: NetworkSchemaV2,
|
|
385
|
+
amount: NonEmptyString,
|
|
386
|
+
asset: NonEmptyString,
|
|
387
|
+
payTo: NonEmptyString,
|
|
388
|
+
maxTimeoutSeconds: import_zod.z.number().positive(),
|
|
389
|
+
extra: OptionalAny
|
|
390
|
+
});
|
|
391
|
+
var PaymentRequiredV2Schema = import_zod.z.object({
|
|
392
|
+
x402Version: import_zod.z.literal(2),
|
|
393
|
+
error: import_zod.z.string().optional(),
|
|
394
|
+
resource: ResourceInfoSchema,
|
|
395
|
+
accepts: import_zod.z.array(PaymentRequirementsV2Schema).min(1),
|
|
396
|
+
extensions: OptionalAny
|
|
397
|
+
});
|
|
398
|
+
var PaymentPayloadV2Schema = import_zod.z.object({
|
|
399
|
+
x402Version: import_zod.z.literal(2),
|
|
400
|
+
resource: ResourceInfoSchema.optional(),
|
|
401
|
+
accepted: PaymentRequirementsV2Schema,
|
|
402
|
+
payload: Any,
|
|
403
|
+
extensions: OptionalAny
|
|
404
|
+
});
|
|
405
|
+
var PaymentRequirementsSchema = import_zod.z.union([
|
|
406
|
+
PaymentRequirementsV1Schema,
|
|
407
|
+
PaymentRequirementsV2Schema
|
|
408
|
+
]);
|
|
409
|
+
var PaymentRequiredSchema = import_zod.z.discriminatedUnion("x402Version", [
|
|
410
|
+
PaymentRequiredV1Schema,
|
|
411
|
+
PaymentRequiredV2Schema
|
|
412
|
+
]);
|
|
413
|
+
var PaymentPayloadSchema = import_zod.z.discriminatedUnion("x402Version", [
|
|
414
|
+
PaymentPayloadV1Schema,
|
|
415
|
+
PaymentPayloadV2Schema
|
|
416
|
+
]);
|
|
417
|
+
|
|
418
|
+
// src/http/httpFacilitatorClient.ts
|
|
419
|
+
var DEFAULT_FACILITATOR_URL = "https://x402.org/facilitator";
|
|
420
|
+
var GET_SUPPORTED_RETRIES = 3;
|
|
421
|
+
var GET_SUPPORTED_RETRY_DELAY_MS = 1e3;
|
|
422
|
+
var MAX_RETRY_DELAY_MS = 3e4;
|
|
423
|
+
function computeRetryDelay(retryAfter, attempt) {
|
|
424
|
+
let delay = null;
|
|
425
|
+
if (retryAfter !== null) {
|
|
426
|
+
const seconds = Number(retryAfter);
|
|
427
|
+
if (!isNaN(seconds)) {
|
|
428
|
+
delay = seconds * 1e3;
|
|
429
|
+
} else {
|
|
430
|
+
const retryDate = Date.parse(retryAfter);
|
|
431
|
+
if (!isNaN(retryDate)) {
|
|
432
|
+
delay = retryDate - Date.now();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (delay === null || delay <= 0) {
|
|
437
|
+
delay = GET_SUPPORTED_RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
438
|
+
}
|
|
439
|
+
return Math.min(delay, MAX_RETRY_DELAY_MS);
|
|
440
|
+
}
|
|
441
|
+
var verifyResponseSchema = import_zod2.z.object({
|
|
442
|
+
isValid: import_zod2.z.boolean(),
|
|
443
|
+
invalidReason: import_zod2.z.string().nullish().transform((v) => v ?? void 0),
|
|
444
|
+
invalidMessage: import_zod2.z.string().nullish().transform((v) => v ?? void 0),
|
|
445
|
+
payer: import_zod2.z.string().nullish().transform((v) => v ?? void 0),
|
|
446
|
+
extensions: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).nullish().transform((v) => v ?? void 0),
|
|
447
|
+
extra: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).nullish().transform((v) => v ?? void 0)
|
|
448
|
+
});
|
|
449
|
+
var settleResponseSchema = import_zod2.z.object({
|
|
450
|
+
success: import_zod2.z.boolean(),
|
|
451
|
+
errorReason: import_zod2.z.string().nullish().transform((v) => v ?? void 0),
|
|
452
|
+
errorMessage: import_zod2.z.string().nullish().transform((v) => v ?? void 0),
|
|
453
|
+
payer: import_zod2.z.string().nullish().transform((v) => v ?? void 0),
|
|
454
|
+
transaction: import_zod2.z.string(),
|
|
455
|
+
network: import_zod2.z.custom((value) => typeof value === "string"),
|
|
456
|
+
amount: import_zod2.z.string().nullish().transform((v) => v ?? void 0),
|
|
457
|
+
extensions: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).nullish().transform((v) => v ?? void 0),
|
|
458
|
+
extra: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).nullish().transform((v) => v ?? void 0)
|
|
459
|
+
});
|
|
460
|
+
var supportedKindSchema = import_zod2.z.object({
|
|
461
|
+
x402Version: import_zod2.z.number(),
|
|
462
|
+
scheme: import_zod2.z.string(),
|
|
463
|
+
network: import_zod2.z.custom(
|
|
464
|
+
(value) => typeof value === "string"
|
|
465
|
+
),
|
|
466
|
+
extra: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).nullish().transform((v) => v ?? void 0)
|
|
467
|
+
});
|
|
468
|
+
var supportedResponseSchema = import_zod2.z.object({
|
|
469
|
+
kinds: import_zod2.z.array(supportedKindSchema),
|
|
470
|
+
extensions: import_zod2.z.array(import_zod2.z.string()).default([]),
|
|
471
|
+
signers: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.array(import_zod2.z.string())).default({})
|
|
472
|
+
});
|
|
473
|
+
function responseExcerpt(text, limit = 200) {
|
|
474
|
+
const compact = text.trim().replace(/\s+/g, " ");
|
|
475
|
+
if (!compact) {
|
|
476
|
+
return "<empty response>";
|
|
477
|
+
}
|
|
478
|
+
if (compact.length <= limit) {
|
|
479
|
+
return compact;
|
|
480
|
+
}
|
|
481
|
+
return `${compact.slice(0, limit - 3)}...`;
|
|
482
|
+
}
|
|
483
|
+
var EXTENSION_RESPONSE_LOG_FIELD_ALLOWLIST = ["status", "rejectedReason", "reason", "code"];
|
|
484
|
+
function logExtensionResponsesHeader(response) {
|
|
485
|
+
const header = response.headers.get("EXTENSION-RESPONSES");
|
|
486
|
+
if (!header) return;
|
|
487
|
+
try {
|
|
488
|
+
const decoded = JSON.parse(safeBase64Decode(header));
|
|
489
|
+
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) return;
|
|
490
|
+
const sanitized = {};
|
|
491
|
+
for (const [extensionKey, payload] of Object.entries(decoded)) {
|
|
492
|
+
const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
493
|
+
const filtered = {};
|
|
494
|
+
for (const key of EXTENSION_RESPONSE_LOG_FIELD_ALLOWLIST) {
|
|
495
|
+
if (source[key] !== void 0) {
|
|
496
|
+
filtered[key] = source[key];
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
sanitized[extensionKey] = filtered;
|
|
500
|
+
}
|
|
501
|
+
console.log(`[x402] extension responses: ${JSON.stringify(sanitized)}`);
|
|
502
|
+
} catch {
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function parseSuccessResponse(response, schema, operation) {
|
|
506
|
+
const text = await response.text();
|
|
507
|
+
let data;
|
|
508
|
+
try {
|
|
509
|
+
data = JSON.parse(text);
|
|
510
|
+
} catch {
|
|
511
|
+
throw new FacilitatorResponseError(
|
|
512
|
+
`Facilitator ${operation} returned invalid JSON: ${responseExcerpt(text)}`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
const parsed = schema.safeParse(data);
|
|
516
|
+
if (!parsed.success) {
|
|
517
|
+
throw new FacilitatorResponseError(
|
|
518
|
+
`Facilitator ${operation} returned invalid data: ${responseExcerpt(text)}`
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return parsed.data;
|
|
522
|
+
}
|
|
523
|
+
var HTTPFacilitatorClient = class {
|
|
524
|
+
/**
|
|
525
|
+
* Creates a new HTTPFacilitatorClient instance.
|
|
526
|
+
*
|
|
527
|
+
* @param config - Configuration options for the facilitator client
|
|
528
|
+
*/
|
|
529
|
+
constructor(config) {
|
|
530
|
+
this.url = (config?.url || DEFAULT_FACILITATOR_URL).replace(/\/+$/, "");
|
|
531
|
+
this._createAuthHeaders = config?.createAuthHeaders;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Verify a payment with the facilitator
|
|
535
|
+
*
|
|
536
|
+
* @param paymentPayload - The payment to verify
|
|
537
|
+
* @param paymentRequirements - The requirements to verify against
|
|
538
|
+
* @returns Verification response
|
|
539
|
+
*/
|
|
540
|
+
async verify(paymentPayload, paymentRequirements) {
|
|
541
|
+
let headers = {
|
|
542
|
+
"Content-Type": "application/json"
|
|
543
|
+
};
|
|
544
|
+
if (this._createAuthHeaders) {
|
|
545
|
+
const authHeaders = await this.createAuthHeaders("verify");
|
|
546
|
+
headers = { ...headers, ...authHeaders.headers };
|
|
547
|
+
}
|
|
548
|
+
const response = await fetch(`${this.url}/verify`, {
|
|
549
|
+
method: "POST",
|
|
550
|
+
headers,
|
|
551
|
+
redirect: "follow",
|
|
552
|
+
body: JSON.stringify({
|
|
553
|
+
x402Version: paymentPayload.x402Version,
|
|
554
|
+
paymentPayload: this.toJsonSafe(paymentPayload),
|
|
555
|
+
paymentRequirements: this.toJsonSafe(paymentRequirements)
|
|
556
|
+
})
|
|
557
|
+
});
|
|
558
|
+
if (!response.ok) {
|
|
559
|
+
const text = await response.text();
|
|
560
|
+
let data;
|
|
561
|
+
try {
|
|
562
|
+
data = JSON.parse(text);
|
|
563
|
+
} catch {
|
|
564
|
+
throw new Error(`Facilitator verify failed (${response.status}): ${responseExcerpt(text)}`);
|
|
565
|
+
}
|
|
566
|
+
if (typeof data === "object" && data !== null && "isValid" in data) {
|
|
567
|
+
throw new VerifyError(response.status, data);
|
|
568
|
+
}
|
|
569
|
+
throw new Error(
|
|
570
|
+
`Facilitator verify failed (${response.status}): ${responseExcerpt(JSON.stringify(data))}`
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
const verifyResult = await parseSuccessResponse(response, verifyResponseSchema, "verify");
|
|
574
|
+
logExtensionResponsesHeader(response);
|
|
575
|
+
return verifyResult;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Settle a payment with the facilitator
|
|
579
|
+
*
|
|
580
|
+
* @param paymentPayload - The payment to settle
|
|
581
|
+
* @param paymentRequirements - The requirements for settlement
|
|
582
|
+
* @returns Settlement response
|
|
583
|
+
*/
|
|
584
|
+
async settle(paymentPayload, paymentRequirements) {
|
|
585
|
+
let headers = {
|
|
586
|
+
"Content-Type": "application/json"
|
|
587
|
+
};
|
|
588
|
+
if (this._createAuthHeaders) {
|
|
589
|
+
const authHeaders = await this.createAuthHeaders("settle");
|
|
590
|
+
headers = { ...headers, ...authHeaders.headers };
|
|
591
|
+
}
|
|
592
|
+
const response = await fetch(`${this.url}/settle`, {
|
|
593
|
+
method: "POST",
|
|
594
|
+
headers,
|
|
595
|
+
redirect: "follow",
|
|
596
|
+
body: JSON.stringify({
|
|
597
|
+
x402Version: paymentPayload.x402Version,
|
|
598
|
+
paymentPayload: this.toJsonSafe(paymentPayload),
|
|
599
|
+
paymentRequirements: this.toJsonSafe(paymentRequirements)
|
|
600
|
+
})
|
|
601
|
+
});
|
|
602
|
+
if (!response.ok) {
|
|
603
|
+
const text = await response.text();
|
|
604
|
+
let data;
|
|
605
|
+
try {
|
|
606
|
+
data = JSON.parse(text);
|
|
607
|
+
} catch {
|
|
608
|
+
throw new Error(`Facilitator settle failed (${response.status}): ${responseExcerpt(text)}`);
|
|
609
|
+
}
|
|
610
|
+
if (typeof data === "object" && data !== null && "success" in data) {
|
|
611
|
+
throw new SettleError(response.status, data);
|
|
612
|
+
}
|
|
613
|
+
throw new Error(
|
|
614
|
+
`Facilitator settle failed (${response.status}): ${responseExcerpt(JSON.stringify(data))}`
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
const settleResult = await parseSuccessResponse(response, settleResponseSchema, "settle");
|
|
618
|
+
logExtensionResponsesHeader(response);
|
|
619
|
+
return settleResult;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Get supported payment kinds and extensions from the facilitator.
|
|
623
|
+
* Retries with exponential backoff on 429 rate limit errors.
|
|
624
|
+
*
|
|
625
|
+
* @returns Supported payment kinds and extensions
|
|
626
|
+
*/
|
|
627
|
+
async getSupported() {
|
|
628
|
+
let headers = {
|
|
629
|
+
"Content-Type": "application/json"
|
|
630
|
+
};
|
|
631
|
+
if (this._createAuthHeaders) {
|
|
632
|
+
const authHeaders = await this.createAuthHeaders("supported");
|
|
633
|
+
headers = { ...headers, ...authHeaders.headers };
|
|
634
|
+
}
|
|
635
|
+
let lastError = null;
|
|
636
|
+
for (let attempt = 0; attempt < GET_SUPPORTED_RETRIES; attempt++) {
|
|
637
|
+
const response = await fetch(`${this.url}/supported`, {
|
|
638
|
+
method: "GET",
|
|
639
|
+
headers,
|
|
640
|
+
redirect: "follow"
|
|
641
|
+
});
|
|
642
|
+
if (response.ok) {
|
|
643
|
+
return parseSuccessResponse(response, supportedResponseSchema, "supported");
|
|
644
|
+
}
|
|
645
|
+
const errorText = await response.text().catch(() => response.statusText);
|
|
646
|
+
lastError = new Error(
|
|
647
|
+
`Facilitator getSupported failed (${response.status}): ${responseExcerpt(errorText)}`
|
|
648
|
+
);
|
|
649
|
+
if (response.status === 429 && attempt < GET_SUPPORTED_RETRIES - 1) {
|
|
650
|
+
const delay = computeRetryDelay(response.headers.get("Retry-After"), attempt);
|
|
651
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
throw lastError;
|
|
655
|
+
}
|
|
656
|
+
throw lastError ?? new Error("Facilitator getSupported failed after retries");
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Creates authentication headers for a specific path.
|
|
660
|
+
*
|
|
661
|
+
* @param path - The path to create authentication headers for (e.g., "verify", "settle", "supported")
|
|
662
|
+
* @returns An object containing the authentication headers for the specified path
|
|
663
|
+
*/
|
|
664
|
+
async createAuthHeaders(path) {
|
|
665
|
+
if (this._createAuthHeaders) {
|
|
666
|
+
const authHeaders = await this._createAuthHeaders();
|
|
667
|
+
return {
|
|
668
|
+
headers: authHeaders[path] ?? {}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
headers: {}
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Helper to convert objects to JSON-safe format.
|
|
677
|
+
* Handles BigInt and other non-JSON types.
|
|
678
|
+
*
|
|
679
|
+
* @param obj - The object to convert
|
|
680
|
+
* @returns The JSON-safe representation of the object
|
|
681
|
+
*/
|
|
682
|
+
toJsonSafe(obj) {
|
|
683
|
+
return JSON.parse(
|
|
684
|
+
JSON.stringify(obj, (_, value) => typeof value === "bigint" ? value.toString() : value)
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// src/index.ts
|
|
690
|
+
var x402Version = 2;
|
|
691
|
+
|
|
692
|
+
// src/server/x402ResourceServer.ts
|
|
693
|
+
function resolveSettlementOverrideAmount(rawAmount, requirements, decimals = 6) {
|
|
694
|
+
const percentMatch = rawAmount.match(/^(\d+(?:\.\d{0,2})?)%$/);
|
|
695
|
+
if (percentMatch) {
|
|
696
|
+
const [intPart, decPart = ""] = percentMatch[1].split(".");
|
|
697
|
+
const scaledPercent = BigInt(intPart) * 100n + BigInt(decPart.padEnd(2, "0").slice(0, 2));
|
|
698
|
+
const base = BigInt(requirements.amount);
|
|
699
|
+
return (base * scaledPercent / 10000n).toString();
|
|
700
|
+
}
|
|
701
|
+
const dollarMatch = rawAmount.match(/^\$(\d+(?:\.\d+)?)$/);
|
|
702
|
+
if (dollarMatch) {
|
|
703
|
+
const dollars = parseFloat(dollarMatch[1]);
|
|
704
|
+
return Math.round(dollars * 10 ** decimals).toString();
|
|
705
|
+
}
|
|
706
|
+
return rawAmount;
|
|
707
|
+
}
|
|
708
|
+
var x402ResourceServer = class {
|
|
709
|
+
/**
|
|
710
|
+
* Creates a new x402ResourceServer instance.
|
|
711
|
+
*
|
|
712
|
+
* @param facilitatorClients - Optional facilitator client(s) for payment processing
|
|
713
|
+
*/
|
|
714
|
+
constructor(facilitatorClients) {
|
|
715
|
+
this.registeredServerSchemes = /* @__PURE__ */ new Map();
|
|
716
|
+
this.schemeHookAdapters = /* @__PURE__ */ new Map();
|
|
717
|
+
this.supportedResponsesMap = /* @__PURE__ */ new Map();
|
|
718
|
+
this.facilitatorClientsMap = /* @__PURE__ */ new Map();
|
|
719
|
+
this.registeredExtensions = /* @__PURE__ */ new Map();
|
|
720
|
+
this.extensionHookAdapters = /* @__PURE__ */ new Map();
|
|
721
|
+
this.beforeVerifyHooks = [];
|
|
722
|
+
this.afterVerifyHooks = [];
|
|
723
|
+
this.onVerifyFailureHooks = [];
|
|
724
|
+
this.beforeSettleHooks = [];
|
|
725
|
+
this.afterSettleHooks = [];
|
|
726
|
+
this.onSettleFailureHooks = [];
|
|
727
|
+
this.onVerifiedPaymentCanceledHooks = [];
|
|
728
|
+
if (!facilitatorClients) {
|
|
729
|
+
this.facilitatorClients = [new HTTPFacilitatorClient()];
|
|
730
|
+
} else if (Array.isArray(facilitatorClients)) {
|
|
731
|
+
this.facilitatorClients = facilitatorClients.length > 0 ? facilitatorClients : [new HTTPFacilitatorClient()];
|
|
732
|
+
} else {
|
|
733
|
+
this.facilitatorClients = [facilitatorClients];
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Register a scheme/network server implementation.
|
|
738
|
+
*
|
|
739
|
+
* @param network - The network identifier
|
|
740
|
+
* @param server - The scheme/network server implementation
|
|
741
|
+
* @returns The x402ResourceServer instance for chaining
|
|
742
|
+
*/
|
|
743
|
+
register(network, server) {
|
|
744
|
+
if (!this.registeredServerSchemes.has(network)) {
|
|
745
|
+
this.registeredServerSchemes.set(network, /* @__PURE__ */ new Map());
|
|
746
|
+
}
|
|
747
|
+
const serverByScheme = this.registeredServerSchemes.get(network);
|
|
748
|
+
serverByScheme.set(server.scheme, server);
|
|
749
|
+
if (!this.schemeHookAdapters.has(network)) {
|
|
750
|
+
this.schemeHookAdapters.set(network, /* @__PURE__ */ new Map());
|
|
751
|
+
}
|
|
752
|
+
const hooksByScheme = this.schemeHookAdapters.get(network);
|
|
753
|
+
const hooks = server.schemeHooks;
|
|
754
|
+
if (!hooks) {
|
|
755
|
+
hooksByScheme.delete(server.scheme);
|
|
756
|
+
return this;
|
|
757
|
+
}
|
|
758
|
+
const handles = {};
|
|
759
|
+
if (hooks.onBeforeVerify) handles.beforeVerify = hooks.onBeforeVerify;
|
|
760
|
+
if (hooks.onAfterVerify) handles.afterVerify = hooks.onAfterVerify;
|
|
761
|
+
if (hooks.onVerifyFailure) handles.onVerifyFailure = hooks.onVerifyFailure;
|
|
762
|
+
if (hooks.onBeforeSettle) handles.beforeSettle = hooks.onBeforeSettle;
|
|
763
|
+
if (hooks.onAfterSettle) handles.afterSettle = hooks.onAfterSettle;
|
|
764
|
+
if (hooks.onSettleFailure) handles.onSettleFailure = hooks.onSettleFailure;
|
|
765
|
+
if (hooks.onVerifiedPaymentCanceled) {
|
|
766
|
+
handles.onVerifiedPaymentCanceled = hooks.onVerifiedPaymentCanceled;
|
|
767
|
+
}
|
|
768
|
+
if (Object.keys(handles).length > 0) {
|
|
769
|
+
hooksByScheme.set(server.scheme, handles);
|
|
770
|
+
} else {
|
|
771
|
+
hooksByScheme.delete(server.scheme);
|
|
772
|
+
}
|
|
773
|
+
return this;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Check if a scheme is registered for a given network.
|
|
777
|
+
*
|
|
778
|
+
* @param network - The network identifier
|
|
779
|
+
* @param scheme - The payment scheme name
|
|
780
|
+
* @returns True if the scheme is registered for the network, false otherwise
|
|
781
|
+
*/
|
|
782
|
+
hasRegisteredScheme(network, scheme) {
|
|
783
|
+
return !!findByNetworkAndScheme(this.registeredServerSchemes, scheme, network);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Returns the decimal precision for the asset specified in the given payment requirements.
|
|
787
|
+
* Looks up the registered scheme for the network and delegates to its getAssetDecimals
|
|
788
|
+
* method if available. Falls back to 6 (standard for USDC stablecoins) when the scheme
|
|
789
|
+
* does not implement getAssetDecimals or is not registered.
|
|
790
|
+
*
|
|
791
|
+
* @param requirements - The payment requirements containing scheme, network, and asset
|
|
792
|
+
* @returns The number of decimal places for the asset
|
|
793
|
+
*/
|
|
794
|
+
getAssetDecimalsForRequirements(requirements) {
|
|
795
|
+
const scheme = findByNetworkAndScheme(
|
|
796
|
+
this.registeredServerSchemes,
|
|
797
|
+
requirements.scheme,
|
|
798
|
+
requirements.network
|
|
799
|
+
);
|
|
800
|
+
return scheme?.getAssetDecimals?.(requirements.asset ?? "", requirements.network) ?? 6;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Registers a resource server extension (enrichment and optional verify/settle hooks).
|
|
804
|
+
* Re-registering the same key overwrites; omitting `hooks` removes adapter handles for that key.
|
|
805
|
+
*
|
|
806
|
+
* @param extension - Extension definition including `key` and optional `hooks`
|
|
807
|
+
* @returns This server instance for chaining
|
|
808
|
+
*/
|
|
809
|
+
registerExtension(extension) {
|
|
810
|
+
this.registeredExtensions.set(extension.key, extension);
|
|
811
|
+
const extensionKey = extension.key;
|
|
812
|
+
const extensionHooks = extension.hooks;
|
|
813
|
+
if (!extensionHooks) {
|
|
814
|
+
this.extensionHookAdapters.delete(extensionKey);
|
|
815
|
+
return this;
|
|
816
|
+
}
|
|
817
|
+
const handles = {};
|
|
818
|
+
const bindExtensionHookAdapter = (extensionHookKey, adapterPhase) => {
|
|
819
|
+
const impl = extensionHooks[extensionHookKey];
|
|
820
|
+
if (!impl) return;
|
|
821
|
+
handles[adapterPhase] = (async (ctx) => {
|
|
822
|
+
if (ctx.declaredExtensions[extensionKey] === void 0) return;
|
|
823
|
+
return impl(
|
|
824
|
+
ctx.declaredExtensions[extensionKey],
|
|
825
|
+
ctx
|
|
826
|
+
);
|
|
827
|
+
});
|
|
828
|
+
};
|
|
829
|
+
bindExtensionHookAdapter("onBeforeVerify", "beforeVerify");
|
|
830
|
+
bindExtensionHookAdapter("onAfterVerify", "afterVerify");
|
|
831
|
+
bindExtensionHookAdapter("onVerifyFailure", "onVerifyFailure");
|
|
832
|
+
bindExtensionHookAdapter("onBeforeSettle", "beforeSettle");
|
|
833
|
+
bindExtensionHookAdapter("onAfterSettle", "afterSettle");
|
|
834
|
+
bindExtensionHookAdapter("onSettleFailure", "onSettleFailure");
|
|
835
|
+
bindExtensionHookAdapter("onVerifiedPaymentCanceled", "onVerifiedPaymentCanceled");
|
|
836
|
+
if (Object.keys(handles).length > 0) {
|
|
837
|
+
this.extensionHookAdapters.set(extensionKey, handles);
|
|
838
|
+
} else {
|
|
839
|
+
this.extensionHookAdapters.delete(extensionKey);
|
|
840
|
+
}
|
|
841
|
+
return this;
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Check if an extension is registered.
|
|
845
|
+
*
|
|
846
|
+
* @param key - The extension key
|
|
847
|
+
* @returns True if the extension is registered
|
|
848
|
+
*/
|
|
849
|
+
hasExtension(key) {
|
|
850
|
+
return this.registeredExtensions.has(key);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Get all registered extensions.
|
|
854
|
+
*
|
|
855
|
+
* @returns Array of registered extensions
|
|
856
|
+
*/
|
|
857
|
+
getExtensions() {
|
|
858
|
+
return Array.from(this.registeredExtensions.values());
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Enriches declared extensions using registered extension hooks.
|
|
862
|
+
*
|
|
863
|
+
* @param declaredExtensions - Extensions declared on the route
|
|
864
|
+
* @param transportContext - Transport-specific context (HTTP, A2A, MCP, etc.)
|
|
865
|
+
* @returns Enriched extensions map
|
|
866
|
+
*/
|
|
867
|
+
enrichExtensions(declaredExtensions, transportContext) {
|
|
868
|
+
const enriched = {};
|
|
869
|
+
for (const [key, declaration] of Object.entries(declaredExtensions)) {
|
|
870
|
+
const extension = this.registeredExtensions.get(key);
|
|
871
|
+
if (extension?.enrichDeclaration) {
|
|
872
|
+
try {
|
|
873
|
+
enriched[key] = extension.enrichDeclaration(declaration, transportContext);
|
|
874
|
+
} catch (error) {
|
|
875
|
+
this.warnExtensionHookFailure(key, "enrichDeclaration", error);
|
|
876
|
+
enriched[key] = declaration;
|
|
877
|
+
}
|
|
878
|
+
} else {
|
|
879
|
+
enriched[key] = declaration;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return enriched;
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Register a hook to execute before payment verification.
|
|
886
|
+
* Can abort verification by returning { abort: true, reason: string }
|
|
887
|
+
*
|
|
888
|
+
* @param hook - The hook function to register
|
|
889
|
+
* @returns The x402ResourceServer instance for chaining
|
|
890
|
+
*/
|
|
891
|
+
onBeforeVerify(hook) {
|
|
892
|
+
this.beforeVerifyHooks.push(hook);
|
|
893
|
+
return this;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Register a hook to execute after successful payment verification.
|
|
897
|
+
*
|
|
898
|
+
* @param hook - The hook function to register
|
|
899
|
+
* @returns The x402ResourceServer instance for chaining
|
|
900
|
+
*/
|
|
901
|
+
onAfterVerify(hook) {
|
|
902
|
+
this.afterVerifyHooks.push(hook);
|
|
903
|
+
return this;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Register a hook to execute when payment verification fails.
|
|
907
|
+
* Can recover from failure by returning { recovered: true, result: VerifyResponse }
|
|
908
|
+
*
|
|
909
|
+
* @param hook - The hook function to register
|
|
910
|
+
* @returns The x402ResourceServer instance for chaining
|
|
911
|
+
*/
|
|
912
|
+
onVerifyFailure(hook) {
|
|
913
|
+
this.onVerifyFailureHooks.push(hook);
|
|
914
|
+
return this;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Register a hook to execute before payment settlement.
|
|
918
|
+
* Can abort settlement by returning { abort: true, reason: string }
|
|
919
|
+
*
|
|
920
|
+
* @param hook - The hook function to register
|
|
921
|
+
* @returns The x402ResourceServer instance for chaining
|
|
922
|
+
*/
|
|
923
|
+
onBeforeSettle(hook) {
|
|
924
|
+
this.beforeSettleHooks.push(hook);
|
|
925
|
+
return this;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Register a hook to execute after successful payment settlement.
|
|
929
|
+
*
|
|
930
|
+
* @param hook - The hook function to register
|
|
931
|
+
* @returns The x402ResourceServer instance for chaining
|
|
932
|
+
*/
|
|
933
|
+
onAfterSettle(hook) {
|
|
934
|
+
this.afterSettleHooks.push(hook);
|
|
935
|
+
return this;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Register a hook to execute when payment settlement fails.
|
|
939
|
+
* Can recover from failure by returning { recovered: true, result: SettleResponse }
|
|
940
|
+
*
|
|
941
|
+
* @param hook - The hook function to register
|
|
942
|
+
* @returns The x402ResourceServer instance for chaining
|
|
943
|
+
*/
|
|
944
|
+
onSettleFailure(hook) {
|
|
945
|
+
this.onSettleFailureHooks.push(hook);
|
|
946
|
+
return this;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Register a hook to execute when verified payment work is canceled before settlement.
|
|
950
|
+
*
|
|
951
|
+
* @param hook - The hook function to register
|
|
952
|
+
* @returns The x402ResourceServer instance for chaining
|
|
953
|
+
*/
|
|
954
|
+
onVerifiedPaymentCanceled(hook) {
|
|
955
|
+
this.onVerifiedPaymentCanceledHooks.push(hook);
|
|
956
|
+
return this;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Initialize by fetching supported kinds from all facilitators
|
|
960
|
+
* Creates mappings for supported responses and facilitator clients
|
|
961
|
+
* Earlier facilitators in the array get precedence
|
|
962
|
+
*/
|
|
963
|
+
async initialize() {
|
|
964
|
+
this.supportedResponsesMap.clear();
|
|
965
|
+
this.facilitatorClientsMap.clear();
|
|
966
|
+
let lastError;
|
|
967
|
+
for (const facilitatorClient of this.facilitatorClients) {
|
|
968
|
+
try {
|
|
969
|
+
const supported = await facilitatorClient.getSupported();
|
|
970
|
+
for (const kind of supported.kinds) {
|
|
971
|
+
const x402Version2 = kind.x402Version;
|
|
972
|
+
if (!this.supportedResponsesMap.has(x402Version2)) {
|
|
973
|
+
this.supportedResponsesMap.set(x402Version2, /* @__PURE__ */ new Map());
|
|
974
|
+
}
|
|
975
|
+
const responseVersionMap = this.supportedResponsesMap.get(x402Version2);
|
|
976
|
+
if (!this.facilitatorClientsMap.has(x402Version2)) {
|
|
977
|
+
this.facilitatorClientsMap.set(x402Version2, /* @__PURE__ */ new Map());
|
|
978
|
+
}
|
|
979
|
+
const clientVersionMap = this.facilitatorClientsMap.get(x402Version2);
|
|
980
|
+
if (!responseVersionMap.has(kind.network)) {
|
|
981
|
+
responseVersionMap.set(kind.network, /* @__PURE__ */ new Map());
|
|
982
|
+
}
|
|
983
|
+
const responseNetworkMap = responseVersionMap.get(kind.network);
|
|
984
|
+
if (!clientVersionMap.has(kind.network)) {
|
|
985
|
+
clientVersionMap.set(kind.network, /* @__PURE__ */ new Map());
|
|
986
|
+
}
|
|
987
|
+
const clientNetworkMap = clientVersionMap.get(kind.network);
|
|
988
|
+
if (!responseNetworkMap.has(kind.scheme)) {
|
|
989
|
+
responseNetworkMap.set(kind.scheme, supported);
|
|
990
|
+
clientNetworkMap.set(kind.scheme, facilitatorClient);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
} catch (error) {
|
|
994
|
+
lastError = error;
|
|
995
|
+
console.warn(`Failed to fetch supported kinds from facilitator: ${error}`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (this.supportedResponsesMap.size === 0) {
|
|
999
|
+
throw lastError ? new Error(
|
|
1000
|
+
"Failed to initialize: no supported payment kinds loaded from any facilitator.",
|
|
1001
|
+
{
|
|
1002
|
+
cause: lastError
|
|
1003
|
+
}
|
|
1004
|
+
) : new Error(
|
|
1005
|
+
"Failed to initialize: no supported payment kinds loaded from any facilitator."
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Get supported kind for a specific version, network, and scheme
|
|
1011
|
+
*
|
|
1012
|
+
* @param x402Version - The x402 version
|
|
1013
|
+
* @param network - The network identifier
|
|
1014
|
+
* @param scheme - The payment scheme
|
|
1015
|
+
* @returns The supported kind or undefined if not found
|
|
1016
|
+
*/
|
|
1017
|
+
getSupportedKind(x402Version2, network, scheme) {
|
|
1018
|
+
const versionMap = this.supportedResponsesMap.get(x402Version2);
|
|
1019
|
+
if (!versionMap) return void 0;
|
|
1020
|
+
const supportedResponse = findByNetworkAndScheme(versionMap, scheme, network);
|
|
1021
|
+
if (!supportedResponse) return void 0;
|
|
1022
|
+
return supportedResponse.kinds.find(
|
|
1023
|
+
(kind) => kind.x402Version === x402Version2 && kind.network === network && kind.scheme === scheme
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Get facilitator extensions for a specific version, network, and scheme
|
|
1028
|
+
*
|
|
1029
|
+
* @param x402Version - The x402 version
|
|
1030
|
+
* @param network - The network identifier
|
|
1031
|
+
* @param scheme - The payment scheme
|
|
1032
|
+
* @returns The facilitator extensions or empty array if not found
|
|
1033
|
+
*/
|
|
1034
|
+
getFacilitatorExtensions(x402Version2, network, scheme) {
|
|
1035
|
+
const versionMap = this.supportedResponsesMap.get(x402Version2);
|
|
1036
|
+
if (!versionMap) return [];
|
|
1037
|
+
const supportedResponse = findByNetworkAndScheme(versionMap, scheme, network);
|
|
1038
|
+
return supportedResponse?.extensions || [];
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Build payment requirements for a protected resource
|
|
1042
|
+
*
|
|
1043
|
+
* @param resourceConfig - Configuration for the protected resource
|
|
1044
|
+
* @returns Array of payment requirements
|
|
1045
|
+
*/
|
|
1046
|
+
async buildPaymentRequirements(resourceConfig) {
|
|
1047
|
+
const requirements = [];
|
|
1048
|
+
const scheme = resourceConfig.scheme;
|
|
1049
|
+
const SchemeNetworkServer = findByNetworkAndScheme(
|
|
1050
|
+
this.registeredServerSchemes,
|
|
1051
|
+
scheme,
|
|
1052
|
+
resourceConfig.network
|
|
1053
|
+
);
|
|
1054
|
+
if (!SchemeNetworkServer) {
|
|
1055
|
+
console.warn(
|
|
1056
|
+
`No server implementation registered for scheme: ${scheme}, network: ${resourceConfig.network}`
|
|
1057
|
+
);
|
|
1058
|
+
return requirements;
|
|
1059
|
+
}
|
|
1060
|
+
const supportedKind = this.getSupportedKind(
|
|
1061
|
+
x402Version,
|
|
1062
|
+
resourceConfig.network,
|
|
1063
|
+
SchemeNetworkServer.scheme
|
|
1064
|
+
);
|
|
1065
|
+
if (!supportedKind) {
|
|
1066
|
+
throw new Error(
|
|
1067
|
+
`Facilitator does not support ${SchemeNetworkServer.scheme} on ${resourceConfig.network}. Make sure to call initialize() to fetch supported kinds from facilitators.`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
const facilitatorExtensions = this.getFacilitatorExtensions(
|
|
1071
|
+
x402Version,
|
|
1072
|
+
resourceConfig.network,
|
|
1073
|
+
SchemeNetworkServer.scheme
|
|
1074
|
+
);
|
|
1075
|
+
const parsedPrice = await SchemeNetworkServer.parsePrice(
|
|
1076
|
+
resourceConfig.price,
|
|
1077
|
+
resourceConfig.network
|
|
1078
|
+
);
|
|
1079
|
+
const baseRequirements = {
|
|
1080
|
+
scheme: SchemeNetworkServer.scheme,
|
|
1081
|
+
network: resourceConfig.network,
|
|
1082
|
+
amount: parsedPrice.amount,
|
|
1083
|
+
asset: parsedPrice.asset,
|
|
1084
|
+
payTo: resourceConfig.payTo,
|
|
1085
|
+
maxTimeoutSeconds: resourceConfig.maxTimeoutSeconds || 300,
|
|
1086
|
+
// Default 5 minutes
|
|
1087
|
+
extra: {
|
|
1088
|
+
...parsedPrice.extra,
|
|
1089
|
+
...resourceConfig.extra
|
|
1090
|
+
// Merge user-provided extra
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
const requirement = await SchemeNetworkServer.enhancePaymentRequirements(
|
|
1094
|
+
baseRequirements,
|
|
1095
|
+
supportedKind,
|
|
1096
|
+
facilitatorExtensions
|
|
1097
|
+
);
|
|
1098
|
+
requirements.push(requirement);
|
|
1099
|
+
return requirements;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Build payment requirements from multiple payment options
|
|
1103
|
+
* This method handles resolving dynamic payTo/price functions and builds requirements for each option
|
|
1104
|
+
*
|
|
1105
|
+
* @param paymentOptions - Array of payment options to convert
|
|
1106
|
+
* @param context - HTTP request context for resolving dynamic functions
|
|
1107
|
+
* @returns Array of payment requirements (one per option)
|
|
1108
|
+
*/
|
|
1109
|
+
async buildPaymentRequirementsFromOptions(paymentOptions, context) {
|
|
1110
|
+
const allRequirements = [];
|
|
1111
|
+
for (const option of paymentOptions) {
|
|
1112
|
+
const resolvedPayTo = typeof option.payTo === "function" ? await option.payTo(context) : option.payTo;
|
|
1113
|
+
const resolvedPrice = typeof option.price === "function" ? await option.price(context) : option.price;
|
|
1114
|
+
const resourceConfig = {
|
|
1115
|
+
scheme: option.scheme,
|
|
1116
|
+
payTo: resolvedPayTo,
|
|
1117
|
+
price: resolvedPrice,
|
|
1118
|
+
network: option.network,
|
|
1119
|
+
maxTimeoutSeconds: option.maxTimeoutSeconds,
|
|
1120
|
+
extra: option.extra
|
|
1121
|
+
};
|
|
1122
|
+
const requirements = await this.buildPaymentRequirements(resourceConfig);
|
|
1123
|
+
allRequirements.push(...requirements);
|
|
1124
|
+
}
|
|
1125
|
+
return allRequirements;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Create a payment required response
|
|
1129
|
+
*
|
|
1130
|
+
* @param requirements - Payment requirements
|
|
1131
|
+
* @param resourceInfo - Resource information
|
|
1132
|
+
* @param error - Error message
|
|
1133
|
+
* @param extensions - Optional declared extensions (for per-key enrichment)
|
|
1134
|
+
* @param transportContext - Optional transport-specific context (e.g., HTTP request, MCP tool context)
|
|
1135
|
+
* @param paymentPayload - Optional failed payment payload for response-time scheme enrichment
|
|
1136
|
+
* @returns Payment required response object
|
|
1137
|
+
*/
|
|
1138
|
+
async createPaymentRequiredResponse(requirements, resourceInfo, error, extensions, transportContext, paymentPayload) {
|
|
1139
|
+
const acceptsClone = requirements.map((req) => ({
|
|
1140
|
+
...req,
|
|
1141
|
+
extra: structuredClone(req.extra)
|
|
1142
|
+
}));
|
|
1143
|
+
let workingAccepts = acceptsClone;
|
|
1144
|
+
let baselineAccepts = snapshotPaymentRequirementsList(workingAccepts);
|
|
1145
|
+
let response = {
|
|
1146
|
+
x402Version: 2,
|
|
1147
|
+
error,
|
|
1148
|
+
resource: resourceInfo,
|
|
1149
|
+
accepts: workingAccepts
|
|
1150
|
+
};
|
|
1151
|
+
if (extensions && Object.keys(extensions).length > 0) {
|
|
1152
|
+
response.extensions = extensions;
|
|
1153
|
+
}
|
|
1154
|
+
for (let i = 0; i < workingAccepts.length; i++) {
|
|
1155
|
+
const accept = workingAccepts[i];
|
|
1156
|
+
const scheme = findByNetworkAndScheme(
|
|
1157
|
+
this.registeredServerSchemes,
|
|
1158
|
+
accept.scheme,
|
|
1159
|
+
accept.network
|
|
1160
|
+
);
|
|
1161
|
+
if (!scheme?.enrichPaymentRequiredResponse) {
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
const context = {
|
|
1165
|
+
requirements: workingAccepts,
|
|
1166
|
+
paymentPayload,
|
|
1167
|
+
resourceInfo,
|
|
1168
|
+
error,
|
|
1169
|
+
paymentRequiredResponse: response,
|
|
1170
|
+
transportContext
|
|
1171
|
+
};
|
|
1172
|
+
const enrichedAccepts = await scheme.enrichPaymentRequiredResponse(context);
|
|
1173
|
+
if (enrichedAccepts !== void 0) {
|
|
1174
|
+
workingAccepts = enrichedAccepts;
|
|
1175
|
+
response.accepts = workingAccepts;
|
|
1176
|
+
}
|
|
1177
|
+
assertAcceptsAdditiveExtraAfterSchemeEnrich(
|
|
1178
|
+
baselineAccepts,
|
|
1179
|
+
response.accepts,
|
|
1180
|
+
accept.scheme,
|
|
1181
|
+
accept.network
|
|
1182
|
+
);
|
|
1183
|
+
baselineAccepts = snapshotPaymentRequirementsList(response.accepts);
|
|
1184
|
+
}
|
|
1185
|
+
if (extensions) {
|
|
1186
|
+
for (const [key, declaration] of Object.entries(extensions)) {
|
|
1187
|
+
const extension = this.registeredExtensions.get(key);
|
|
1188
|
+
if (extension?.enrichPaymentRequiredResponse) {
|
|
1189
|
+
try {
|
|
1190
|
+
const context = {
|
|
1191
|
+
requirements: workingAccepts,
|
|
1192
|
+
resourceInfo,
|
|
1193
|
+
error,
|
|
1194
|
+
paymentRequiredResponse: response,
|
|
1195
|
+
transportContext
|
|
1196
|
+
};
|
|
1197
|
+
const extensionData = await extension.enrichPaymentRequiredResponse(
|
|
1198
|
+
declaration,
|
|
1199
|
+
context
|
|
1200
|
+
);
|
|
1201
|
+
if (extensionData !== void 0) {
|
|
1202
|
+
if (!response.extensions) {
|
|
1203
|
+
response.extensions = {};
|
|
1204
|
+
}
|
|
1205
|
+
response.extensions[key] = extensionData;
|
|
1206
|
+
}
|
|
1207
|
+
} catch (error2) {
|
|
1208
|
+
this.warnExtensionHookFailure(key, "enrichPaymentRequiredResponse", error2);
|
|
1209
|
+
}
|
|
1210
|
+
assertAcceptsAllowlistedAfterExtensionEnrich(baselineAccepts, workingAccepts, key);
|
|
1211
|
+
baselineAccepts = snapshotPaymentRequirementsList(workingAccepts);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return response;
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Verifies a payment against requirements, running manual and in-use extension hooks.
|
|
1219
|
+
*
|
|
1220
|
+
* @param paymentPayload - Signed payment payload from the client
|
|
1221
|
+
* @param requirements - Requirements matched to the payload
|
|
1222
|
+
* @param declaredExtensions - Optional per-extension declarations for the request
|
|
1223
|
+
* @param transportContext - Optional transport-specific context (e.g. HTTP, MCP)
|
|
1224
|
+
* @returns Facilitator verify outcome (optionally carrying a `skipHandler` directive),
|
|
1225
|
+
* or abort/recovery as driven by hooks
|
|
1226
|
+
*/
|
|
1227
|
+
async verifyPayment(paymentPayload, requirements, declaredExtensions, transportContext) {
|
|
1228
|
+
const resolvedDeclaredExtensions = declaredExtensions ?? {};
|
|
1229
|
+
const extensionKeysInUse = Object.keys(resolvedDeclaredExtensions);
|
|
1230
|
+
const matchedScheme = {
|
|
1231
|
+
network: requirements.network,
|
|
1232
|
+
scheme: requirements.scheme
|
|
1233
|
+
};
|
|
1234
|
+
const context = {
|
|
1235
|
+
paymentPayload,
|
|
1236
|
+
requirements,
|
|
1237
|
+
declaredExtensions: resolvedDeclaredExtensions,
|
|
1238
|
+
transportContext
|
|
1239
|
+
};
|
|
1240
|
+
for (const { label, hook } of this.getLabeledHooks(
|
|
1241
|
+
"beforeVerify",
|
|
1242
|
+
extensionKeysInUse,
|
|
1243
|
+
matchedScheme
|
|
1244
|
+
)) {
|
|
1245
|
+
try {
|
|
1246
|
+
const result = await hook(context);
|
|
1247
|
+
if (result && "abort" in result && result.abort) {
|
|
1248
|
+
return {
|
|
1249
|
+
isValid: false,
|
|
1250
|
+
invalidReason: result.reason,
|
|
1251
|
+
invalidMessage: result.message
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
if (result && "skip" in result && result.skip) {
|
|
1255
|
+
return this.runAfterVerifyHooks(
|
|
1256
|
+
result.result,
|
|
1257
|
+
context,
|
|
1258
|
+
extensionKeysInUse,
|
|
1259
|
+
matchedScheme
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
this.warnResourceServerHookFailure("beforeVerify", label, error);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
try {
|
|
1267
|
+
const facilitatorClient = this.getFacilitatorClient(
|
|
1268
|
+
paymentPayload.x402Version,
|
|
1269
|
+
requirements.network,
|
|
1270
|
+
requirements.scheme
|
|
1271
|
+
);
|
|
1272
|
+
let verifyResult;
|
|
1273
|
+
if (!facilitatorClient) {
|
|
1274
|
+
let lastError;
|
|
1275
|
+
for (const client of this.facilitatorClients) {
|
|
1276
|
+
try {
|
|
1277
|
+
verifyResult = await client.verify(paymentPayload, requirements);
|
|
1278
|
+
break;
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
lastError = error;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (!verifyResult) {
|
|
1284
|
+
throw lastError || new Error(
|
|
1285
|
+
`No facilitator supports ${requirements.scheme} on ${requirements.network} for v${paymentPayload.x402Version}`
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
} else {
|
|
1289
|
+
verifyResult = await facilitatorClient.verify(paymentPayload, requirements);
|
|
1290
|
+
}
|
|
1291
|
+
return this.runAfterVerifyHooks(verifyResult, context, extensionKeysInUse, matchedScheme);
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
const failureContext = {
|
|
1294
|
+
...context,
|
|
1295
|
+
error
|
|
1296
|
+
};
|
|
1297
|
+
for (const { label, hook } of this.getLabeledHooks(
|
|
1298
|
+
"onVerifyFailure",
|
|
1299
|
+
extensionKeysInUse,
|
|
1300
|
+
matchedScheme
|
|
1301
|
+
)) {
|
|
1302
|
+
try {
|
|
1303
|
+
const result = await hook(failureContext);
|
|
1304
|
+
if (result && "recovered" in result && result.recovered) {
|
|
1305
|
+
return result.result;
|
|
1306
|
+
}
|
|
1307
|
+
} catch (error2) {
|
|
1308
|
+
this.warnResourceServerHookFailure("onVerifyFailure", label, error2);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
throw error;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Create cancellation controls for a verified payment attempt.
|
|
1316
|
+
*
|
|
1317
|
+
* @param paymentPayload - Signed payment payload from the client
|
|
1318
|
+
* @param requirements - Requirements matched to the payload
|
|
1319
|
+
* @param declaredExtensions - Optional per-extension declarations for the request
|
|
1320
|
+
* @param transportContext - Optional transport-specific context
|
|
1321
|
+
* @returns Cancellation controls for the verified payment attempt
|
|
1322
|
+
*/
|
|
1323
|
+
createPaymentCancellationDispatcher(paymentPayload, requirements, declaredExtensions, transportContext) {
|
|
1324
|
+
const resolvedDeclaredExtensions = declaredExtensions ?? {};
|
|
1325
|
+
let cancelPromise;
|
|
1326
|
+
return {
|
|
1327
|
+
cancel: (options) => {
|
|
1328
|
+
if (!cancelPromise) {
|
|
1329
|
+
cancelPromise = this.dispatchVerifiedPaymentCanceled(
|
|
1330
|
+
paymentPayload,
|
|
1331
|
+
requirements,
|
|
1332
|
+
resolvedDeclaredExtensions,
|
|
1333
|
+
options,
|
|
1334
|
+
transportContext
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
return cancelPromise;
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Settle a verified payment
|
|
1343
|
+
*
|
|
1344
|
+
* @param paymentPayload - The payment payload to settle
|
|
1345
|
+
* @param requirements - The payment requirements
|
|
1346
|
+
* @param declaredExtensions - Optional declared extensions (for per-key enrichment)
|
|
1347
|
+
* @param transportContext - Optional transport-specific context (e.g., HTTP request/response, MCP tool context)
|
|
1348
|
+
* @param settlementOverrides - Optional overrides for settlement parameters (e.g., partial settlement amount)
|
|
1349
|
+
* @returns Settlement response
|
|
1350
|
+
*/
|
|
1351
|
+
async settlePayment(paymentPayload, requirements, declaredExtensions, transportContext, settlementOverrides) {
|
|
1352
|
+
const resolvedDeclaredExtensions = declaredExtensions ?? {};
|
|
1353
|
+
const extensionKeysInUse = Object.keys(resolvedDeclaredExtensions);
|
|
1354
|
+
let effectiveRequirements = requirements;
|
|
1355
|
+
if (settlementOverrides?.amount !== void 0) {
|
|
1356
|
+
const scheme = findByNetworkAndScheme(
|
|
1357
|
+
this.registeredServerSchemes,
|
|
1358
|
+
requirements.scheme,
|
|
1359
|
+
requirements.network
|
|
1360
|
+
);
|
|
1361
|
+
const decimals = scheme?.getAssetDecimals?.(requirements.asset ?? "", requirements.network) ?? 6;
|
|
1362
|
+
effectiveRequirements = {
|
|
1363
|
+
...requirements,
|
|
1364
|
+
amount: resolveSettlementOverrideAmount(settlementOverrides.amount, requirements, decimals)
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
const context = {
|
|
1368
|
+
paymentPayload,
|
|
1369
|
+
requirements: effectiveRequirements,
|
|
1370
|
+
declaredExtensions: resolvedDeclaredExtensions,
|
|
1371
|
+
transportContext
|
|
1372
|
+
};
|
|
1373
|
+
const matchedScheme = {
|
|
1374
|
+
network: effectiveRequirements.network,
|
|
1375
|
+
scheme: effectiveRequirements.scheme
|
|
1376
|
+
};
|
|
1377
|
+
for (const { label, hook } of this.getLabeledHooks(
|
|
1378
|
+
"beforeSettle",
|
|
1379
|
+
extensionKeysInUse,
|
|
1380
|
+
matchedScheme
|
|
1381
|
+
)) {
|
|
1382
|
+
try {
|
|
1383
|
+
const result = await hook(context);
|
|
1384
|
+
if (result && "abort" in result && result.abort) {
|
|
1385
|
+
throw new SettleError(400, {
|
|
1386
|
+
success: false,
|
|
1387
|
+
errorReason: result.reason,
|
|
1388
|
+
errorMessage: result.message,
|
|
1389
|
+
transaction: "",
|
|
1390
|
+
network: requirements.network
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
if (result && "skip" in result && result.skip) {
|
|
1394
|
+
const settleResult = result.result;
|
|
1395
|
+
const skipResultContext = {
|
|
1396
|
+
...context,
|
|
1397
|
+
result: settleResult,
|
|
1398
|
+
transportContext
|
|
1399
|
+
};
|
|
1400
|
+
for (const { label: label2, hook: hook2 } of this.getLabeledHooks(
|
|
1401
|
+
"afterSettle",
|
|
1402
|
+
extensionKeysInUse,
|
|
1403
|
+
matchedScheme
|
|
1404
|
+
)) {
|
|
1405
|
+
try {
|
|
1406
|
+
await hook2(skipResultContext);
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
this.warnResourceServerHookFailure("afterSettle", label2, error);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
await this.enrichSettlementResponse(
|
|
1412
|
+
settleResult,
|
|
1413
|
+
skipResultContext,
|
|
1414
|
+
resolvedDeclaredExtensions,
|
|
1415
|
+
matchedScheme
|
|
1416
|
+
);
|
|
1417
|
+
return settleResult;
|
|
1418
|
+
}
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
if (error instanceof SettleError) {
|
|
1421
|
+
throw error;
|
|
1422
|
+
}
|
|
1423
|
+
this.warnResourceServerHookFailure("beforeSettle", label, error);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
try {
|
|
1427
|
+
const scheme = findByNetworkAndScheme(
|
|
1428
|
+
this.registeredServerSchemes,
|
|
1429
|
+
matchedScheme.scheme,
|
|
1430
|
+
matchedScheme.network
|
|
1431
|
+
);
|
|
1432
|
+
const payloadEnrichmentHook = scheme?.enrichSettlementPayload;
|
|
1433
|
+
if (payloadEnrichmentHook) {
|
|
1434
|
+
const label = `scheme "${matchedScheme.scheme}" enrichSettlementPayload`;
|
|
1435
|
+
const enrichment = await payloadEnrichmentHook(context);
|
|
1436
|
+
if (enrichment !== void 0) {
|
|
1437
|
+
assertAdditivePayloadEnrichment(paymentPayload.payload, enrichment, label);
|
|
1438
|
+
paymentPayload.payload = { ...paymentPayload.payload, ...enrichment };
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
const facilitatorClient = this.getFacilitatorClient(
|
|
1442
|
+
paymentPayload.x402Version,
|
|
1443
|
+
effectiveRequirements.network,
|
|
1444
|
+
effectiveRequirements.scheme
|
|
1445
|
+
);
|
|
1446
|
+
let settleResult;
|
|
1447
|
+
if (!facilitatorClient) {
|
|
1448
|
+
let lastError;
|
|
1449
|
+
for (const client of this.facilitatorClients) {
|
|
1450
|
+
try {
|
|
1451
|
+
settleResult = await client.settle(paymentPayload, effectiveRequirements);
|
|
1452
|
+
break;
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
lastError = error;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (!settleResult) {
|
|
1458
|
+
throw lastError || new Error(
|
|
1459
|
+
`No facilitator supports ${effectiveRequirements.scheme} on ${effectiveRequirements.network} for v${paymentPayload.x402Version}`
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
} else {
|
|
1463
|
+
settleResult = await facilitatorClient.settle(paymentPayload, effectiveRequirements);
|
|
1464
|
+
}
|
|
1465
|
+
const resultContext = {
|
|
1466
|
+
...context,
|
|
1467
|
+
result: settleResult
|
|
1468
|
+
};
|
|
1469
|
+
for (const { label, hook } of this.getLabeledHooks(
|
|
1470
|
+
"afterSettle",
|
|
1471
|
+
extensionKeysInUse,
|
|
1472
|
+
matchedScheme
|
|
1473
|
+
)) {
|
|
1474
|
+
try {
|
|
1475
|
+
await hook(resultContext);
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
this.warnResourceServerHookFailure("afterSettle", label, error);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
await this.enrichSettlementResponse(
|
|
1481
|
+
settleResult,
|
|
1482
|
+
resultContext,
|
|
1483
|
+
resolvedDeclaredExtensions,
|
|
1484
|
+
matchedScheme
|
|
1485
|
+
);
|
|
1486
|
+
return settleResult;
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
const failureContext = {
|
|
1489
|
+
...context,
|
|
1490
|
+
error
|
|
1491
|
+
};
|
|
1492
|
+
for (const { label, hook } of this.getLabeledHooks(
|
|
1493
|
+
"onSettleFailure",
|
|
1494
|
+
extensionKeysInUse,
|
|
1495
|
+
matchedScheme
|
|
1496
|
+
)) {
|
|
1497
|
+
try {
|
|
1498
|
+
const result = await hook(failureContext);
|
|
1499
|
+
if (result && "recovered" in result && result.recovered) {
|
|
1500
|
+
return result.result;
|
|
1501
|
+
}
|
|
1502
|
+
} catch (error2) {
|
|
1503
|
+
this.warnResourceServerHookFailure("onSettleFailure", label, error2);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
throw error;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Find matching payment requirements for a payment
|
|
1511
|
+
*
|
|
1512
|
+
* @param availableRequirements - Array of available payment requirements
|
|
1513
|
+
* @param paymentPayload - The payment payload
|
|
1514
|
+
* @returns Matching payment requirements or undefined
|
|
1515
|
+
*/
|
|
1516
|
+
/**
|
|
1517
|
+
* Validates optional client extension echoes against server-advertised extension info.
|
|
1518
|
+
* When the client omits extensions entirely, validation passes.
|
|
1519
|
+
*
|
|
1520
|
+
* @param paymentRequired - Server payment required response used for matching
|
|
1521
|
+
* @param paymentPayload - Client payment payload
|
|
1522
|
+
* @returns Whether echoed extension info preserves server-advertised values
|
|
1523
|
+
*/
|
|
1524
|
+
validateExtensions(paymentRequired, paymentPayload) {
|
|
1525
|
+
if (paymentPayload.x402Version !== 2) {
|
|
1526
|
+
return { valid: true };
|
|
1527
|
+
}
|
|
1528
|
+
const serverExtensions = paymentRequired.extensions;
|
|
1529
|
+
if (!serverExtensions || Object.keys(serverExtensions).length === 0) {
|
|
1530
|
+
return { valid: true };
|
|
1531
|
+
}
|
|
1532
|
+
const clientExtensions = paymentPayload.extensions;
|
|
1533
|
+
if (!clientExtensions || Object.keys(clientExtensions).length === 0) {
|
|
1534
|
+
return { valid: true };
|
|
1535
|
+
}
|
|
1536
|
+
for (const [key, echoedValue] of Object.entries(clientExtensions)) {
|
|
1537
|
+
if (!Object.prototype.hasOwnProperty.call(serverExtensions, key)) {
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
const advertisedInfo = getExtensionInfo(serverExtensions[key]);
|
|
1541
|
+
const echoedInfo = getExtensionInfo(echoedValue);
|
|
1542
|
+
if (!extensionInfoMatchesAdvertised(advertisedInfo, echoedInfo)) {
|
|
1543
|
+
return {
|
|
1544
|
+
valid: false,
|
|
1545
|
+
invalidReason: "extension_echo_mismatch",
|
|
1546
|
+
extensionKey: key
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return { valid: true };
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Finds the server-advertised requirement that matches a client payment payload.
|
|
1554
|
+
*
|
|
1555
|
+
* @param availableRequirements - Payment requirements advertised for the resource.
|
|
1556
|
+
* @param paymentPayload - Signed payment payload from the client.
|
|
1557
|
+
* @returns The matching requirement, or undefined when none match.
|
|
1558
|
+
*/
|
|
1559
|
+
findMatchingRequirements(availableRequirements, paymentPayload) {
|
|
1560
|
+
switch (paymentPayload.x402Version) {
|
|
1561
|
+
case 2:
|
|
1562
|
+
return availableRequirements.find(
|
|
1563
|
+
(paymentRequirements) => paymentRequirementsMatchAccepted(paymentRequirements, paymentPayload.accepted)
|
|
1564
|
+
);
|
|
1565
|
+
case 1:
|
|
1566
|
+
return availableRequirements.find(
|
|
1567
|
+
(req) => req.scheme === paymentPayload.accepted.scheme && req.network === paymentPayload.accepted.network
|
|
1568
|
+
);
|
|
1569
|
+
default:
|
|
1570
|
+
throw new Error(
|
|
1571
|
+
`Unsupported x402 version: ${paymentPayload.x402Version}`
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Logs a warning when a manual or extension adapter lifecycle hook throws.
|
|
1577
|
+
*
|
|
1578
|
+
* @param phase - Lifecycle phase name (e.g. `beforeVerify`)
|
|
1579
|
+
* @param label - Hook source label from {@link getLabeledHooks} (manual index or extension key)
|
|
1580
|
+
* @param error - Thrown value or rejection reason
|
|
1581
|
+
*/
|
|
1582
|
+
warnResourceServerHookFailure(phase, label, error) {
|
|
1583
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1584
|
+
console.warn(`[x402] Resource server ${phase} hook threw (${label}): ${detail}`);
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Logs a warning when a registered extension enrichment hook throws.
|
|
1588
|
+
*
|
|
1589
|
+
* @param extensionKey - Registered extension identifier
|
|
1590
|
+
* @param hookName - Hook method name (e.g. `enrichDeclaration`)
|
|
1591
|
+
* @param error - Thrown value or rejection reason
|
|
1592
|
+
*/
|
|
1593
|
+
warnExtensionHookFailure(extensionKey, hookName, error) {
|
|
1594
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1595
|
+
console.warn(`[x402] extension "${extensionKey}" ${hookName} threw: ${detail}`);
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Executes after-verify hooks for facilitator and hook-provided verify results.
|
|
1599
|
+
*
|
|
1600
|
+
* @param verifyResult - Verify response passed to after-verify hooks.
|
|
1601
|
+
* @param context - Verify context shared with before-verify hooks.
|
|
1602
|
+
* @param extensionKeysInUse - Declared extension keys for this request.
|
|
1603
|
+
* @param matchedScheme - Scheme/network selected for this payment.
|
|
1604
|
+
* @param matchedScheme.network - Matched payment network.
|
|
1605
|
+
* @param matchedScheme.scheme - Matched payment scheme.
|
|
1606
|
+
* @returns Verify response with any in-process skip handler directive.
|
|
1607
|
+
*/
|
|
1608
|
+
async runAfterVerifyHooks(verifyResult, context, extensionKeysInUse, matchedScheme) {
|
|
1609
|
+
const resultContext = {
|
|
1610
|
+
...context,
|
|
1611
|
+
result: verifyResult
|
|
1612
|
+
};
|
|
1613
|
+
let skipHandler;
|
|
1614
|
+
for (const { label, hook } of this.getLabeledHooks(
|
|
1615
|
+
"afterVerify",
|
|
1616
|
+
extensionKeysInUse,
|
|
1617
|
+
matchedScheme
|
|
1618
|
+
)) {
|
|
1619
|
+
try {
|
|
1620
|
+
const directive = await hook(resultContext);
|
|
1621
|
+
if (directive && "skipHandler" in directive && directive.skipHandler) {
|
|
1622
|
+
skipHandler = directive.response ?? {};
|
|
1623
|
+
}
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
this.warnResourceServerHookFailure("afterVerify", label, error);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return skipHandler ? { ...verifyResult, skipHandler } : verifyResult;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Runs response enrichment after settlement lifecycle hooks complete.
|
|
1632
|
+
*
|
|
1633
|
+
* @param settleResult - Mutable settlement result being returned to the caller
|
|
1634
|
+
* @param context - Read-only hook context for enrichment callbacks
|
|
1635
|
+
* @param declaredExtensions - Extension declarations present on this payment
|
|
1636
|
+
* @param matchedScheme - Scheme/network selected for this settlement
|
|
1637
|
+
* @param matchedScheme.network - Matched payment network
|
|
1638
|
+
* @param matchedScheme.scheme - Matched payment scheme
|
|
1639
|
+
*/
|
|
1640
|
+
async enrichSettlementResponse(settleResult, context, declaredExtensions, matchedScheme) {
|
|
1641
|
+
if (Object.keys(declaredExtensions).length > 0) {
|
|
1642
|
+
const settleCoreSnapshot = snapshotSettleResponseCore(settleResult);
|
|
1643
|
+
for (const [key, declaration] of Object.entries(declaredExtensions)) {
|
|
1644
|
+
const extension = this.registeredExtensions.get(key);
|
|
1645
|
+
if (!extension?.enrichSettlementResponse) continue;
|
|
1646
|
+
try {
|
|
1647
|
+
const extensionData = await extension.enrichSettlementResponse(declaration, context);
|
|
1648
|
+
if (extensionData !== void 0) {
|
|
1649
|
+
if (!settleResult.extensions) {
|
|
1650
|
+
settleResult.extensions = {};
|
|
1651
|
+
}
|
|
1652
|
+
settleResult.extensions[key] = extensionData;
|
|
1653
|
+
}
|
|
1654
|
+
} catch (error) {
|
|
1655
|
+
this.warnExtensionHookFailure(key, "enrichSettlementResponse", error);
|
|
1656
|
+
}
|
|
1657
|
+
assertSettleResponseCoreUnchanged(settleCoreSnapshot, settleResult, key);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const scheme = findByNetworkAndScheme(
|
|
1661
|
+
this.registeredServerSchemes,
|
|
1662
|
+
matchedScheme.scheme,
|
|
1663
|
+
matchedScheme.network
|
|
1664
|
+
);
|
|
1665
|
+
const hook = scheme?.enrichSettlementResponse;
|
|
1666
|
+
if (!hook) return;
|
|
1667
|
+
const label = `scheme "${matchedScheme.scheme}" enrichSettlementResponse`;
|
|
1668
|
+
try {
|
|
1669
|
+
const enrichment = await hook(context);
|
|
1670
|
+
if (enrichment === void 0) return;
|
|
1671
|
+
assertAdditiveSettlementExtra(settleResult.extra ?? {}, enrichment, label);
|
|
1672
|
+
settleResult.extra = mergeAdditiveSettlementExtra(settleResult.extra ?? {}, enrichment);
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
this.warnResourceServerHookFailure("enrichSettlementResponse", label, error);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Notify hooks that verified work ended before settlement.
|
|
1679
|
+
*
|
|
1680
|
+
* @param paymentPayload - Signed payment payload from the client
|
|
1681
|
+
* @param requirements - Requirements matched to the payload
|
|
1682
|
+
* @param declaredExtensions - Optional per-extension declarations for the request
|
|
1683
|
+
* @param options - Cancellation reason and optional diagnostics
|
|
1684
|
+
* @param fallbackTransportContext - Optional transport-specific context
|
|
1685
|
+
*/
|
|
1686
|
+
async dispatchVerifiedPaymentCanceled(paymentPayload, requirements, declaredExtensions, options, fallbackTransportContext) {
|
|
1687
|
+
const extensionKeysInUse = Object.keys(declaredExtensions);
|
|
1688
|
+
const matchedScheme = {
|
|
1689
|
+
network: requirements.network,
|
|
1690
|
+
scheme: requirements.scheme
|
|
1691
|
+
};
|
|
1692
|
+
const context = {
|
|
1693
|
+
paymentPayload,
|
|
1694
|
+
requirements,
|
|
1695
|
+
declaredExtensions,
|
|
1696
|
+
transportContext: fallbackTransportContext,
|
|
1697
|
+
reason: options.reason,
|
|
1698
|
+
error: options.error,
|
|
1699
|
+
responseStatus: options.responseStatus
|
|
1700
|
+
};
|
|
1701
|
+
for (const { label, hook } of this.getLabeledHooks(
|
|
1702
|
+
"onVerifiedPaymentCanceled",
|
|
1703
|
+
extensionKeysInUse,
|
|
1704
|
+
matchedScheme
|
|
1705
|
+
)) {
|
|
1706
|
+
try {
|
|
1707
|
+
await hook(context);
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
this.warnResourceServerHookFailure("onVerifiedPaymentCanceled", label, error);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Manual hooks first, then the matched scheme adapter, then extension adapters for keys in use.
|
|
1715
|
+
* Each entry carries a stable label for logging when a hook throws.
|
|
1716
|
+
*
|
|
1717
|
+
* @param phase - Hook slot (e.g. `beforeVerify`)
|
|
1718
|
+
* @param extensionKeysInUse - Declared extension keys for this request
|
|
1719
|
+
* @param matchedScheme - Scheme/network selected for this payment
|
|
1720
|
+
* @param matchedScheme.network - Matched payment network
|
|
1721
|
+
* @param matchedScheme.scheme - Matched payment scheme
|
|
1722
|
+
* @returns Hooks in invocation order with source labels
|
|
1723
|
+
*/
|
|
1724
|
+
getLabeledHooks(phase, extensionKeysInUse, matchedScheme) {
|
|
1725
|
+
const manualKey = `${phase}Hooks`;
|
|
1726
|
+
const manual = this[manualKey];
|
|
1727
|
+
const out = [];
|
|
1728
|
+
manual.forEach((hook, index) => {
|
|
1729
|
+
out.push({ label: `manual ${phase} hook #${index}`, hook });
|
|
1730
|
+
});
|
|
1731
|
+
if (matchedScheme) {
|
|
1732
|
+
const schemeHandles = findByNetworkAndScheme(
|
|
1733
|
+
this.schemeHookAdapters,
|
|
1734
|
+
matchedScheme.scheme,
|
|
1735
|
+
matchedScheme.network
|
|
1736
|
+
);
|
|
1737
|
+
const hook = schemeHandles?.[phase];
|
|
1738
|
+
if (hook !== void 0) {
|
|
1739
|
+
out.push({
|
|
1740
|
+
label: `scheme "${matchedScheme.scheme}" ${phase}`,
|
|
1741
|
+
hook
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
const inUse = new Set(extensionKeysInUse);
|
|
1746
|
+
for (const [extensionKey, adapterHandles] of this.extensionHookAdapters.entries()) {
|
|
1747
|
+
if (!inUse.has(extensionKey)) continue;
|
|
1748
|
+
const hook = adapterHandles[phase];
|
|
1749
|
+
if (hook !== void 0) {
|
|
1750
|
+
out.push({ label: `extension "${extensionKey}" ${phase}`, hook });
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return out;
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Get facilitator client for a specific version, network, and scheme
|
|
1757
|
+
*
|
|
1758
|
+
* @param x402Version - The x402 version
|
|
1759
|
+
* @param network - The network identifier
|
|
1760
|
+
* @param scheme - The payment scheme
|
|
1761
|
+
* @returns The facilitator client or undefined if not found
|
|
1762
|
+
*/
|
|
1763
|
+
getFacilitatorClient(x402Version2, network, scheme) {
|
|
1764
|
+
const versionMap = this.facilitatorClientsMap.get(x402Version2);
|
|
1765
|
+
if (!versionMap) return void 0;
|
|
1766
|
+
return findByNetworkAndScheme(versionMap, scheme, network);
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
function getExtensionInfo(value) {
|
|
1770
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && Object.prototype.hasOwnProperty.call(value, "info")) {
|
|
1771
|
+
return value.info;
|
|
1772
|
+
}
|
|
1773
|
+
return value;
|
|
1774
|
+
}
|
|
1775
|
+
function extensionInfoMatchesAdvertised(advertised, echoed) {
|
|
1776
|
+
return objectContainsSubset(advertised, echoed);
|
|
1777
|
+
}
|
|
1778
|
+
function paymentRequirementsMatchAccepted(required, accepted) {
|
|
1779
|
+
const { extra: requiredExtra, ...requiredCore } = required;
|
|
1780
|
+
const { extra: acceptedExtra, ...acceptedCore } = accepted;
|
|
1781
|
+
if (!deepEqual(requiredCore, acceptedCore)) {
|
|
1782
|
+
return false;
|
|
1783
|
+
}
|
|
1784
|
+
if (requiredExtra === void 0) {
|
|
1785
|
+
return true;
|
|
1786
|
+
}
|
|
1787
|
+
return objectContainsSubset(requiredExtra, acceptedExtra);
|
|
1788
|
+
}
|
|
1789
|
+
function objectContainsSubset(expected, actual) {
|
|
1790
|
+
if (expected === null || typeof expected !== "object" || Array.isArray(expected)) {
|
|
1791
|
+
return deepEqual(expected, actual);
|
|
1792
|
+
}
|
|
1793
|
+
if (actual === null || typeof actual !== "object" || Array.isArray(actual)) {
|
|
1794
|
+
return false;
|
|
1795
|
+
}
|
|
1796
|
+
const actualRecord = actual;
|
|
1797
|
+
return Object.entries(expected).every(([key, value]) => {
|
|
1798
|
+
const hasActualKey = Object.prototype.hasOwnProperty.call(actualRecord, key);
|
|
1799
|
+
if (!hasActualKey) {
|
|
1800
|
+
return value === void 0;
|
|
1801
|
+
}
|
|
1802
|
+
return objectContainsSubset(value, actualRecord[key]);
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// src/http/index.ts
|
|
1807
|
+
function decodePaymentSignatureHeader(paymentSignatureHeader) {
|
|
1808
|
+
if (!Base64EncodedRegex.test(paymentSignatureHeader)) {
|
|
1809
|
+
throw new Error("Invalid payment signature header");
|
|
1810
|
+
}
|
|
1811
|
+
return JSON.parse(safeBase64Decode(paymentSignatureHeader));
|
|
1812
|
+
}
|
|
1813
|
+
function encodePaymentRequiredHeader(paymentRequired) {
|
|
1814
|
+
return safeBase64Encode(JSON.stringify(paymentRequired));
|
|
1815
|
+
}
|
|
1816
|
+
function encodePaymentResponseHeader(paymentResponse) {
|
|
1817
|
+
return safeBase64Encode(JSON.stringify(paymentResponse));
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// src/http/x402HTTPResourceServer.ts
|
|
1821
|
+
var SETTLEMENT_OVERRIDES_HEADER = "Settlement-Overrides";
|
|
1822
|
+
function checkIfBazaarNeeded(routes) {
|
|
1823
|
+
if ("accepts" in routes) {
|
|
1824
|
+
return !!(routes.extensions && "bazaar" in routes.extensions);
|
|
1825
|
+
}
|
|
1826
|
+
return Object.values(routes).some((routeConfig) => {
|
|
1827
|
+
return !!(routeConfig.extensions && "bazaar" in routeConfig.extensions);
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
var RouteConfigurationError = class extends Error {
|
|
1831
|
+
/**
|
|
1832
|
+
* Creates a new RouteConfigurationError with the given validation errors.
|
|
1833
|
+
*
|
|
1834
|
+
* @param errors - The validation errors that caused this exception.
|
|
1835
|
+
*/
|
|
1836
|
+
constructor(errors) {
|
|
1837
|
+
const message = `x402 Route Configuration Errors:
|
|
1838
|
+
${errors.map((e) => ` - ${e.message}`).join("\n")}`;
|
|
1839
|
+
super(message);
|
|
1840
|
+
this.name = "RouteConfigurationError";
|
|
1841
|
+
this.errors = errors;
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
var FALLBACK_PAYWALL_HTML = `<!DOCTYPE html>
|
|
1845
|
+
<html>
|
|
1846
|
+
<head>
|
|
1847
|
+
<title>Payment Required</title>
|
|
1848
|
+
<meta charset="UTF-8">
|
|
1849
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1850
|
+
</head>
|
|
1851
|
+
<body>
|
|
1852
|
+
<div style="max-width: 600px; margin: 50px auto; padding: 20px; font-family: system-ui, -apple-system, sans-serif;">
|
|
1853
|
+
<h1>Payment Required</h1>
|
|
1854
|
+
<p>This resource is protected by the x402 payment protocol.</p>
|
|
1855
|
+
<p style="margin-top: 2rem; padding: 1rem; background: #fef3c7; border-radius: 0.5rem;">
|
|
1856
|
+
<strong>Note to developers:</strong> install <code>@bankofai/x402-paywall</code> to enable
|
|
1857
|
+
the in-browser wallet connection and payment UI. Programmatic clients should read
|
|
1858
|
+
the payment requirements from the 402 response headers and JSON body.
|
|
1859
|
+
</p>
|
|
1860
|
+
</div>
|
|
1861
|
+
</body>
|
|
1862
|
+
</html>`;
|
|
1863
|
+
var x402HTTPResourceServer = class {
|
|
1864
|
+
/**
|
|
1865
|
+
* Creates a new x402HTTPResourceServer instance.
|
|
1866
|
+
*
|
|
1867
|
+
* @param ResourceServer - The core x402ResourceServer instance to use
|
|
1868
|
+
* @param routes - Route configuration for payment-protected endpoints
|
|
1869
|
+
*/
|
|
1870
|
+
constructor(ResourceServer, routes) {
|
|
1871
|
+
this.compiledRoutes = [];
|
|
1872
|
+
this.protectedRequestHooks = [];
|
|
1873
|
+
this.ResourceServer = ResourceServer;
|
|
1874
|
+
this.routesConfig = routes;
|
|
1875
|
+
const normalizedRoutes = typeof routes === "object" && !("accepts" in routes) ? routes : { "*": routes };
|
|
1876
|
+
for (const [pattern, config] of Object.entries(normalizedRoutes)) {
|
|
1877
|
+
const parsed = this.parseRoutePattern(pattern);
|
|
1878
|
+
this.compiledRoutes.push({
|
|
1879
|
+
verb: parsed.verb,
|
|
1880
|
+
regex: parsed.regex,
|
|
1881
|
+
config,
|
|
1882
|
+
pattern: parsed.path
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Get the underlying x402ResourceServer instance.
|
|
1888
|
+
*
|
|
1889
|
+
* @returns The underlying x402ResourceServer instance
|
|
1890
|
+
*/
|
|
1891
|
+
get server() {
|
|
1892
|
+
return this.ResourceServer;
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Get the routes configuration.
|
|
1896
|
+
*
|
|
1897
|
+
* @returns The routes configuration
|
|
1898
|
+
*/
|
|
1899
|
+
get routes() {
|
|
1900
|
+
return this.routesConfig;
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Initialize the HTTP resource server.
|
|
1904
|
+
*
|
|
1905
|
+
* This method initializes the underlying resource server (fetching facilitator support)
|
|
1906
|
+
* and then validates that all route payment configurations have corresponding
|
|
1907
|
+
* registered schemes and facilitator support.
|
|
1908
|
+
*
|
|
1909
|
+
* @throws RouteConfigurationError if any route's payment options don't have
|
|
1910
|
+
* corresponding registered schemes or facilitator support
|
|
1911
|
+
*
|
|
1912
|
+
* @example
|
|
1913
|
+
* ```typescript
|
|
1914
|
+
* const httpServer = new x402HTTPResourceServer(server, routes);
|
|
1915
|
+
* await httpServer.initialize();
|
|
1916
|
+
* ```
|
|
1917
|
+
*/
|
|
1918
|
+
async initialize() {
|
|
1919
|
+
await this.ResourceServer.initialize();
|
|
1920
|
+
const errors = this.validateRouteConfiguration();
|
|
1921
|
+
if (errors.length > 0) {
|
|
1922
|
+
throw new RouteConfigurationError(errors);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Register a custom paywall provider for generating HTML
|
|
1927
|
+
*
|
|
1928
|
+
* @param provider - PaywallProvider instance
|
|
1929
|
+
* @returns This service instance for chaining
|
|
1930
|
+
*/
|
|
1931
|
+
registerPaywallProvider(provider) {
|
|
1932
|
+
this.paywallProvider = provider;
|
|
1933
|
+
return this;
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Register a hook that runs on every request to a protected route, before payment processing.
|
|
1937
|
+
* Hooks are executed in order of registration. The first hook to return a non-void result wins.
|
|
1938
|
+
*
|
|
1939
|
+
* @param hook - The request hook function
|
|
1940
|
+
* @returns The x402HTTPResourceServer instance for chaining
|
|
1941
|
+
*/
|
|
1942
|
+
onProtectedRequest(hook) {
|
|
1943
|
+
this.protectedRequestHooks.push(hook);
|
|
1944
|
+
return this;
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Process HTTP request and return response instructions
|
|
1948
|
+
* This is the main entry point for framework middleware
|
|
1949
|
+
*
|
|
1950
|
+
* @param context - HTTP request context
|
|
1951
|
+
* @param paywallConfig - Optional paywall configuration
|
|
1952
|
+
* @returns Process result indicating next action for middleware
|
|
1953
|
+
*/
|
|
1954
|
+
async processHTTPRequest(context, paywallConfig) {
|
|
1955
|
+
const method = context.method || context.adapter.getMethod();
|
|
1956
|
+
context = { ...context, method };
|
|
1957
|
+
const { adapter, path } = context;
|
|
1958
|
+
const routeMatch = this.getRouteConfig(path, method);
|
|
1959
|
+
if (!routeMatch) {
|
|
1960
|
+
return { type: "no-payment-required" };
|
|
1961
|
+
}
|
|
1962
|
+
const { config: routeConfig, pattern: routePattern } = routeMatch;
|
|
1963
|
+
const enrichedContext = { ...context, routePattern };
|
|
1964
|
+
for (const hook of this.getProtectedRequestHooks(routeConfig)) {
|
|
1965
|
+
const result = await hook(enrichedContext, routeConfig);
|
|
1966
|
+
if (result && "grantAccess" in result) {
|
|
1967
|
+
return { type: "no-payment-required" };
|
|
1968
|
+
}
|
|
1969
|
+
if (result && "abort" in result) {
|
|
1970
|
+
return {
|
|
1971
|
+
type: "payment-error",
|
|
1972
|
+
response: {
|
|
1973
|
+
status: 403,
|
|
1974
|
+
headers: { "Content-Type": "application/json" },
|
|
1975
|
+
body: { error: result.reason }
|
|
1976
|
+
}
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
const paymentOptions = this.normalizePaymentOptions(routeConfig);
|
|
1981
|
+
const paymentPayload = this.extractPayment(adapter);
|
|
1982
|
+
const resourceInfo = {
|
|
1983
|
+
url: routeConfig.resource || enrichedContext.adapter.getUrl(),
|
|
1984
|
+
description: routeConfig.description || "",
|
|
1985
|
+
mimeType: routeConfig.mimeType || "",
|
|
1986
|
+
...routeConfig.serviceName !== void 0 && { serviceName: routeConfig.serviceName },
|
|
1987
|
+
...routeConfig.tags !== void 0 && { tags: routeConfig.tags },
|
|
1988
|
+
...routeConfig.iconUrl !== void 0 && { iconUrl: routeConfig.iconUrl }
|
|
1989
|
+
};
|
|
1990
|
+
let requirements = await this.ResourceServer.buildPaymentRequirementsFromOptions(
|
|
1991
|
+
paymentOptions,
|
|
1992
|
+
enrichedContext
|
|
1993
|
+
);
|
|
1994
|
+
let extensions = routeConfig.extensions;
|
|
1995
|
+
if (extensions) {
|
|
1996
|
+
extensions = this.ResourceServer.enrichExtensions(extensions, enrichedContext);
|
|
1997
|
+
}
|
|
1998
|
+
const transportContext = { request: enrichedContext };
|
|
1999
|
+
const paymentRequired = await this.ResourceServer.createPaymentRequiredResponse(
|
|
2000
|
+
requirements,
|
|
2001
|
+
resourceInfo,
|
|
2002
|
+
!paymentPayload ? "Payment required" : void 0,
|
|
2003
|
+
extensions,
|
|
2004
|
+
transportContext
|
|
2005
|
+
);
|
|
2006
|
+
if (!paymentPayload) {
|
|
2007
|
+
const unpaidBody = routeConfig.unpaidResponseBody ? await routeConfig.unpaidResponseBody(enrichedContext) : void 0;
|
|
2008
|
+
return {
|
|
2009
|
+
type: "payment-error",
|
|
2010
|
+
response: this.createHTTPResponse(
|
|
2011
|
+
paymentRequired,
|
|
2012
|
+
this.isWebBrowser(adapter),
|
|
2013
|
+
paywallConfig,
|
|
2014
|
+
routeConfig.customPaywallHtml,
|
|
2015
|
+
unpaidBody
|
|
2016
|
+
)
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
try {
|
|
2020
|
+
const matchingRequirements = this.ResourceServer.findMatchingRequirements(
|
|
2021
|
+
paymentRequired.accepts,
|
|
2022
|
+
paymentPayload
|
|
2023
|
+
);
|
|
2024
|
+
if (!matchingRequirements) {
|
|
2025
|
+
const errorResponse = await this.ResourceServer.createPaymentRequiredResponse(
|
|
2026
|
+
requirements,
|
|
2027
|
+
resourceInfo,
|
|
2028
|
+
"No matching payment requirements",
|
|
2029
|
+
extensions,
|
|
2030
|
+
transportContext
|
|
2031
|
+
);
|
|
2032
|
+
return {
|
|
2033
|
+
type: "payment-error",
|
|
2034
|
+
response: this.createHTTPResponse(errorResponse, false, paywallConfig)
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
const extensionResult = this.ResourceServer.validateExtensions(
|
|
2038
|
+
paymentRequired,
|
|
2039
|
+
paymentPayload
|
|
2040
|
+
);
|
|
2041
|
+
if (!extensionResult.valid) {
|
|
2042
|
+
const errorResponse = await this.ResourceServer.createPaymentRequiredResponse(
|
|
2043
|
+
requirements,
|
|
2044
|
+
resourceInfo,
|
|
2045
|
+
extensionResult.invalidReason,
|
|
2046
|
+
extensions,
|
|
2047
|
+
transportContext,
|
|
2048
|
+
paymentPayload
|
|
2049
|
+
);
|
|
2050
|
+
return {
|
|
2051
|
+
type: "payment-error",
|
|
2052
|
+
response: this.createHTTPResponse(errorResponse, false, paywallConfig)
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
const verifyResult = await this.ResourceServer.verifyPayment(
|
|
2056
|
+
paymentPayload,
|
|
2057
|
+
matchingRequirements,
|
|
2058
|
+
extensions,
|
|
2059
|
+
transportContext
|
|
2060
|
+
);
|
|
2061
|
+
if (!verifyResult.isValid) {
|
|
2062
|
+
const errorResponse = await this.ResourceServer.createPaymentRequiredResponse(
|
|
2063
|
+
requirements,
|
|
2064
|
+
resourceInfo,
|
|
2065
|
+
verifyResult.invalidReason,
|
|
2066
|
+
extensions,
|
|
2067
|
+
transportContext,
|
|
2068
|
+
paymentPayload
|
|
2069
|
+
);
|
|
2070
|
+
return {
|
|
2071
|
+
type: "payment-error",
|
|
2072
|
+
response: this.createHTTPResponse(errorResponse, false, paywallConfig)
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
if (verifyResult.skipHandler) {
|
|
2076
|
+
return await this.processSkipHandlerSettlement(
|
|
2077
|
+
paymentPayload,
|
|
2078
|
+
matchingRequirements,
|
|
2079
|
+
extensions,
|
|
2080
|
+
transportContext,
|
|
2081
|
+
verifyResult.skipHandler
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
const cancellationDispatcher = this.ResourceServer.createPaymentCancellationDispatcher(
|
|
2085
|
+
paymentPayload,
|
|
2086
|
+
matchingRequirements,
|
|
2087
|
+
extensions,
|
|
2088
|
+
transportContext
|
|
2089
|
+
);
|
|
2090
|
+
return {
|
|
2091
|
+
type: "payment-verified",
|
|
2092
|
+
cancellationDispatcher,
|
|
2093
|
+
paymentPayload,
|
|
2094
|
+
paymentRequirements: matchingRequirements,
|
|
2095
|
+
declaredExtensions: extensions
|
|
2096
|
+
};
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
if (error instanceof FacilitatorResponseError) {
|
|
2099
|
+
throw error;
|
|
2100
|
+
}
|
|
2101
|
+
const errorResponse = await this.ResourceServer.createPaymentRequiredResponse(
|
|
2102
|
+
requirements,
|
|
2103
|
+
resourceInfo,
|
|
2104
|
+
error instanceof Error ? error.message : "Payment verification failed",
|
|
2105
|
+
extensions,
|
|
2106
|
+
transportContext
|
|
2107
|
+
);
|
|
2108
|
+
return {
|
|
2109
|
+
type: "payment-error",
|
|
2110
|
+
response: this.createHTTPResponse(errorResponse, false, paywallConfig)
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Process settlement after successful response
|
|
2116
|
+
*
|
|
2117
|
+
* @param paymentPayload - The verified payment payload
|
|
2118
|
+
* @param requirements - The matching payment requirements
|
|
2119
|
+
* @param declaredExtensions - Optional declared extensions (for per-key enrichment)
|
|
2120
|
+
* @param transportContext - Optional HTTP transport context
|
|
2121
|
+
* @param settlementOverrides - Optional settlement overrides (e.g., partial settlement amount)
|
|
2122
|
+
* @returns ProcessSettleResultResponse - SettleResponse with headers if success or errorReason if failure
|
|
2123
|
+
*/
|
|
2124
|
+
async processSettlement(paymentPayload, requirements, declaredExtensions, transportContext, settlementOverrides) {
|
|
2125
|
+
if (transportContext?.request && !transportContext.request.method) {
|
|
2126
|
+
transportContext = {
|
|
2127
|
+
...transportContext,
|
|
2128
|
+
request: {
|
|
2129
|
+
...transportContext.request,
|
|
2130
|
+
method: transportContext.request.adapter.getMethod()
|
|
2131
|
+
}
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
try {
|
|
2135
|
+
let resolvedOverrides = settlementOverrides;
|
|
2136
|
+
if (!resolvedOverrides && transportContext?.responseHeaders) {
|
|
2137
|
+
const overridesKey = SETTLEMENT_OVERRIDES_HEADER.toLowerCase();
|
|
2138
|
+
const rawValue = Object.entries(transportContext.responseHeaders).find(
|
|
2139
|
+
([key]) => key.toLowerCase() === overridesKey
|
|
2140
|
+
)?.[1];
|
|
2141
|
+
if (rawValue) {
|
|
2142
|
+
try {
|
|
2143
|
+
resolvedOverrides = JSON.parse(rawValue);
|
|
2144
|
+
} catch {
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
const settleResponse = await this.ResourceServer.settlePayment(
|
|
2149
|
+
paymentPayload,
|
|
2150
|
+
requirements,
|
|
2151
|
+
declaredExtensions,
|
|
2152
|
+
transportContext,
|
|
2153
|
+
resolvedOverrides
|
|
2154
|
+
);
|
|
2155
|
+
if (!settleResponse.success) {
|
|
2156
|
+
const failure = {
|
|
2157
|
+
...settleResponse,
|
|
2158
|
+
success: false,
|
|
2159
|
+
errorReason: settleResponse.errorReason || "Settlement failed",
|
|
2160
|
+
errorMessage: settleResponse.errorMessage || settleResponse.errorReason || "Settlement failed",
|
|
2161
|
+
headers: this.createSettlementHeaders(settleResponse)
|
|
2162
|
+
};
|
|
2163
|
+
const response = await this.buildSettlementFailureResponse(failure, transportContext);
|
|
2164
|
+
return { ...failure, response };
|
|
2165
|
+
}
|
|
2166
|
+
return {
|
|
2167
|
+
...settleResponse,
|
|
2168
|
+
success: true,
|
|
2169
|
+
headers: this.createSettlementHeaders(settleResponse),
|
|
2170
|
+
requirements
|
|
2171
|
+
};
|
|
2172
|
+
} catch (error) {
|
|
2173
|
+
if (error instanceof FacilitatorResponseError) {
|
|
2174
|
+
throw error;
|
|
2175
|
+
}
|
|
2176
|
+
if (error instanceof SettleError) {
|
|
2177
|
+
const errorReason2 = error.errorReason || error.message;
|
|
2178
|
+
const settleResponse2 = {
|
|
2179
|
+
success: false,
|
|
2180
|
+
errorReason: errorReason2,
|
|
2181
|
+
errorMessage: error.errorMessage || errorReason2,
|
|
2182
|
+
payer: error.payer,
|
|
2183
|
+
network: error.network,
|
|
2184
|
+
transaction: error.transaction
|
|
2185
|
+
};
|
|
2186
|
+
const failure2 = {
|
|
2187
|
+
...settleResponse2,
|
|
2188
|
+
success: false,
|
|
2189
|
+
errorReason: errorReason2,
|
|
2190
|
+
headers: this.createSettlementHeaders(settleResponse2)
|
|
2191
|
+
};
|
|
2192
|
+
const response2 = await this.buildSettlementFailureResponse(failure2, transportContext);
|
|
2193
|
+
return { ...failure2, response: response2 };
|
|
2194
|
+
}
|
|
2195
|
+
const errorReason = error instanceof Error ? error.message : "Settlement failed";
|
|
2196
|
+
const settleResponse = {
|
|
2197
|
+
success: false,
|
|
2198
|
+
errorReason,
|
|
2199
|
+
errorMessage: errorReason,
|
|
2200
|
+
network: requirements.network,
|
|
2201
|
+
transaction: ""
|
|
2202
|
+
};
|
|
2203
|
+
const failure = {
|
|
2204
|
+
...settleResponse,
|
|
2205
|
+
success: false,
|
|
2206
|
+
errorReason,
|
|
2207
|
+
headers: this.createSettlementHeaders(settleResponse)
|
|
2208
|
+
};
|
|
2209
|
+
const response = await this.buildSettlementFailureResponse(failure, transportContext);
|
|
2210
|
+
return { ...failure, response };
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Check if a request requires payment based on route configuration
|
|
2215
|
+
*
|
|
2216
|
+
* @param context - HTTP request context
|
|
2217
|
+
* @returns True if the route requires payment, false otherwise
|
|
2218
|
+
*/
|
|
2219
|
+
requiresPayment(context) {
|
|
2220
|
+
const method = context.method || context.adapter.getMethod();
|
|
2221
|
+
return this.getRouteConfig(context.path, method) !== void 0;
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Settle a verified payment that requested `skipHandler`, packaging the
|
|
2225
|
+
* result as a `payment-error` HTTPProcessResult so framework adapters can
|
|
2226
|
+
* write the response without invoking the route handler.
|
|
2227
|
+
*
|
|
2228
|
+
* - On success: status 200 + PAYMENT-RESPONSE header + configured body.
|
|
2229
|
+
* - On failure: the standard 402 settlement-failure response.
|
|
2230
|
+
*
|
|
2231
|
+
* @param paymentPayload - Verified payment payload.
|
|
2232
|
+
* @param requirements - Matched payment requirements.
|
|
2233
|
+
* @param declaredExtensions - Optional declared extensions for the route.
|
|
2234
|
+
* @param transportContext - Optional HTTP transport context.
|
|
2235
|
+
* @param skipHandlerResponse - Optional content type + body to return on success.
|
|
2236
|
+
* @returns A `payment-error` HTTPProcessResult carrying the final response.
|
|
2237
|
+
*/
|
|
2238
|
+
async processSkipHandlerSettlement(paymentPayload, requirements, declaredExtensions, transportContext, skipHandlerResponse) {
|
|
2239
|
+
const settleResult = await this.processSettlement(
|
|
2240
|
+
paymentPayload,
|
|
2241
|
+
requirements,
|
|
2242
|
+
declaredExtensions,
|
|
2243
|
+
transportContext
|
|
2244
|
+
);
|
|
2245
|
+
if (!settleResult.success) {
|
|
2246
|
+
return { type: "payment-error", response: settleResult.response };
|
|
2247
|
+
}
|
|
2248
|
+
const contentType = skipHandlerResponse?.contentType ?? "application/json";
|
|
2249
|
+
const body = skipHandlerResponse?.body ?? {};
|
|
2250
|
+
return {
|
|
2251
|
+
type: "payment-error",
|
|
2252
|
+
response: {
|
|
2253
|
+
status: 200,
|
|
2254
|
+
headers: {
|
|
2255
|
+
"Content-Type": contentType,
|
|
2256
|
+
...settleResult.headers
|
|
2257
|
+
},
|
|
2258
|
+
body,
|
|
2259
|
+
isHtml: contentType.includes("text/html")
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Build HTTPResponseInstructions for settlement failure.
|
|
2265
|
+
* Uses settlementFailedResponseBody hook if configured, otherwise defaults to empty body.
|
|
2266
|
+
*
|
|
2267
|
+
* @param failure - Settlement failure result with headers
|
|
2268
|
+
* @param transportContext - Optional HTTP transport context for the request
|
|
2269
|
+
* @returns HTTP response instructions for the 402 settlement failure response
|
|
2270
|
+
*/
|
|
2271
|
+
async buildSettlementFailureResponse(failure, transportContext) {
|
|
2272
|
+
const settlementHeaders = failure.headers;
|
|
2273
|
+
const routeConfig = transportContext ? this.getRouteConfig(transportContext.request.path, transportContext.request.method) : void 0;
|
|
2274
|
+
const customBody = routeConfig?.config.settlementFailedResponseBody ? await routeConfig.config.settlementFailedResponseBody(transportContext.request, failure) : void 0;
|
|
2275
|
+
const contentType = customBody ? customBody.contentType : "application/json";
|
|
2276
|
+
const body = customBody ? customBody.body : {};
|
|
2277
|
+
return {
|
|
2278
|
+
status: 402,
|
|
2279
|
+
headers: {
|
|
2280
|
+
"Content-Type": contentType,
|
|
2281
|
+
...settlementHeaders
|
|
2282
|
+
},
|
|
2283
|
+
body,
|
|
2284
|
+
isHtml: contentType.includes("text/html")
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* Normalizes a RouteConfig's accepts field into an array of PaymentOptions
|
|
2289
|
+
* Handles both single PaymentOption and array formats
|
|
2290
|
+
*
|
|
2291
|
+
* @param routeConfig - Route configuration
|
|
2292
|
+
* @returns Array of payment options
|
|
2293
|
+
*/
|
|
2294
|
+
normalizePaymentOptions(routeConfig) {
|
|
2295
|
+
return Array.isArray(routeConfig.accepts) ? routeConfig.accepts : [routeConfig.accepts];
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Manual request hooks run before extension transport hooks for declared extensions.
|
|
2299
|
+
*
|
|
2300
|
+
* @param routeConfig - Route configuration for the matched request
|
|
2301
|
+
* @returns Hooks in invocation order
|
|
2302
|
+
*/
|
|
2303
|
+
getProtectedRequestHooks(routeConfig) {
|
|
2304
|
+
const hooks = [...this.protectedRequestHooks];
|
|
2305
|
+
const declaredExtensions = routeConfig.extensions;
|
|
2306
|
+
if (!declaredExtensions) return hooks;
|
|
2307
|
+
for (const extension of this.ResourceServer.getExtensions()) {
|
|
2308
|
+
const hook = extension.transportHooks?.http?.onProtectedRequest;
|
|
2309
|
+
if (!hook || !(extension.key in declaredExtensions)) continue;
|
|
2310
|
+
hooks.push(
|
|
2311
|
+
(context, routeConfig2) => hook(declaredExtensions[extension.key], context, routeConfig2)
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2314
|
+
return hooks;
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Validates that all payment options in routes have corresponding registered schemes
|
|
2318
|
+
* and facilitator support.
|
|
2319
|
+
*
|
|
2320
|
+
* @returns Array of validation errors (empty if all routes are valid)
|
|
2321
|
+
*/
|
|
2322
|
+
validateRouteConfiguration() {
|
|
2323
|
+
const errors = [];
|
|
2324
|
+
const normalizedRoutes = typeof this.routesConfig === "object" && !("accepts" in this.routesConfig) ? Object.entries(this.routesConfig) : [["*", this.routesConfig]];
|
|
2325
|
+
for (const [pattern, config] of normalizedRoutes) {
|
|
2326
|
+
const pathPart = pattern.includes(" ") ? pattern.split(/\s+/)[1] : pattern;
|
|
2327
|
+
if (pathPart && pathPart.includes("*") && config.extensions && "bazaar" in config.extensions) {
|
|
2328
|
+
console.warn(
|
|
2329
|
+
`[x402] Route "${pattern}": Wildcard (*) patterns with bazaar discovery extensions will auto-generate parameter names (var1, var2, ...). Consider using named parameters instead (e.g. /weather/:city) for better discovery metadata.`
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
const paymentOptions = this.normalizePaymentOptions(config);
|
|
2333
|
+
for (const option of paymentOptions) {
|
|
2334
|
+
if (!this.ResourceServer.hasRegisteredScheme(option.network, option.scheme)) {
|
|
2335
|
+
errors.push({
|
|
2336
|
+
routePattern: pattern,
|
|
2337
|
+
scheme: option.scheme,
|
|
2338
|
+
network: option.network,
|
|
2339
|
+
reason: "missing_scheme",
|
|
2340
|
+
message: `Route "${pattern}": No scheme implementation registered for "${option.scheme}" on network "${option.network}"`
|
|
2341
|
+
});
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
const supportedKind = this.ResourceServer.getSupportedKind(
|
|
2345
|
+
x402Version,
|
|
2346
|
+
option.network,
|
|
2347
|
+
option.scheme
|
|
2348
|
+
);
|
|
2349
|
+
if (!supportedKind) {
|
|
2350
|
+
errors.push({
|
|
2351
|
+
routePattern: pattern,
|
|
2352
|
+
scheme: option.scheme,
|
|
2353
|
+
network: option.network,
|
|
2354
|
+
reason: "missing_facilitator",
|
|
2355
|
+
message: `Route "${pattern}": Facilitator does not support scheme "${option.scheme}" on network "${option.network}"`
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
return errors;
|
|
2361
|
+
}
|
|
2362
|
+
/**
|
|
2363
|
+
* Get route configuration for a request
|
|
2364
|
+
*
|
|
2365
|
+
* @param path - Request path
|
|
2366
|
+
* @param method - HTTP method
|
|
2367
|
+
* @returns Route configuration and pattern, or undefined if no match
|
|
2368
|
+
*/
|
|
2369
|
+
getRouteConfig(path, method) {
|
|
2370
|
+
const normalizedPath = this.normalizePath(path);
|
|
2371
|
+
const upperMethod = method.toUpperCase();
|
|
2372
|
+
const matchingRoute = this.compiledRoutes.find(
|
|
2373
|
+
(route) => route.regex.test(normalizedPath) && (route.verb === "*" || route.verb === upperMethod)
|
|
2374
|
+
);
|
|
2375
|
+
if (!matchingRoute) return void 0;
|
|
2376
|
+
return { config: matchingRoute.config, pattern: matchingRoute.pattern };
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Extract payment from HTTP headers (handles v1 and v2)
|
|
2380
|
+
*
|
|
2381
|
+
* @param adapter - HTTP adapter
|
|
2382
|
+
* @returns Decoded payment payload or null
|
|
2383
|
+
*/
|
|
2384
|
+
extractPayment(adapter) {
|
|
2385
|
+
const header = adapter.getHeader("payment-signature") || adapter.getHeader("PAYMENT-SIGNATURE");
|
|
2386
|
+
if (header) {
|
|
2387
|
+
try {
|
|
2388
|
+
return decodePaymentSignatureHeader(header);
|
|
2389
|
+
} catch (error) {
|
|
2390
|
+
console.warn("Failed to decode PAYMENT-SIGNATURE header:", error);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
return null;
|
|
2394
|
+
}
|
|
2395
|
+
/**
|
|
2396
|
+
* Check if request is from a web browser
|
|
2397
|
+
*
|
|
2398
|
+
* @param adapter - HTTP adapter
|
|
2399
|
+
* @returns True if request appears to be from a browser
|
|
2400
|
+
*/
|
|
2401
|
+
isWebBrowser(adapter) {
|
|
2402
|
+
const accept = adapter.getAcceptHeader();
|
|
2403
|
+
const userAgent = adapter.getUserAgent();
|
|
2404
|
+
return accept.includes("text/html") && userAgent.includes("Mozilla");
|
|
2405
|
+
}
|
|
2406
|
+
/**
|
|
2407
|
+
* Create HTTP response instructions from payment required
|
|
2408
|
+
*
|
|
2409
|
+
* @param paymentRequired - Payment requirements
|
|
2410
|
+
* @param isWebBrowser - Whether request is from browser
|
|
2411
|
+
* @param paywallConfig - Paywall configuration
|
|
2412
|
+
* @param customHtml - Custom HTML template
|
|
2413
|
+
* @param unpaidResponse - Optional custom response (content type and body) for unpaid API requests
|
|
2414
|
+
* @returns Response instructions
|
|
2415
|
+
*/
|
|
2416
|
+
createHTTPResponse(paymentRequired, isWebBrowser, paywallConfig, customHtml, unpaidResponse) {
|
|
2417
|
+
const status = paymentRequired.error === "permit2_allowance_required" ? 412 : 402;
|
|
2418
|
+
const response = this.createHTTPPaymentRequiredResponse(paymentRequired);
|
|
2419
|
+
if (isWebBrowser) {
|
|
2420
|
+
const html = this.generatePaywallHTML(paymentRequired, paywallConfig, customHtml);
|
|
2421
|
+
return {
|
|
2422
|
+
status,
|
|
2423
|
+
headers: {
|
|
2424
|
+
"Content-Type": "text/html",
|
|
2425
|
+
...response.headers
|
|
2426
|
+
},
|
|
2427
|
+
body: html,
|
|
2428
|
+
isHtml: true
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
const contentType = unpaidResponse ? unpaidResponse.contentType : "application/json";
|
|
2432
|
+
const body = unpaidResponse ? unpaidResponse.body : {};
|
|
2433
|
+
return {
|
|
2434
|
+
status,
|
|
2435
|
+
headers: {
|
|
2436
|
+
"Content-Type": contentType,
|
|
2437
|
+
...response.headers
|
|
2438
|
+
},
|
|
2439
|
+
body
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Create HTTP payment required response (v1 puts in body, v2 puts in header)
|
|
2444
|
+
*
|
|
2445
|
+
* @param paymentRequired - Payment required object
|
|
2446
|
+
* @returns Headers and body for the HTTP response
|
|
2447
|
+
*/
|
|
2448
|
+
createHTTPPaymentRequiredResponse(paymentRequired) {
|
|
2449
|
+
return {
|
|
2450
|
+
headers: {
|
|
2451
|
+
"PAYMENT-REQUIRED": encodePaymentRequiredHeader(paymentRequired)
|
|
2452
|
+
}
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Create settlement response headers
|
|
2457
|
+
*
|
|
2458
|
+
* @param settleResponse - Settlement response
|
|
2459
|
+
* @returns Headers to add to response
|
|
2460
|
+
*/
|
|
2461
|
+
createSettlementHeaders(settleResponse) {
|
|
2462
|
+
const encoded = encodePaymentResponseHeader(settleResponse);
|
|
2463
|
+
return { "PAYMENT-RESPONSE": encoded };
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Parse route pattern into verb and regex
|
|
2467
|
+
*
|
|
2468
|
+
* @param pattern - Route pattern like "GET /api/*", "/api/[id]", or "/api/:id"
|
|
2469
|
+
* @returns Parsed pattern with verb and regex
|
|
2470
|
+
*/
|
|
2471
|
+
parseRoutePattern(pattern) {
|
|
2472
|
+
const [verb, path] = pattern.includes(" ") ? pattern.split(/\s+/) : ["*", pattern];
|
|
2473
|
+
const regex = new RegExp(
|
|
2474
|
+
`^${path.replace(/\\/g, "\\\\").replace(/[$()+.?^{|}]/g, "\\$&").replace(/\*/g, ".*?").replace(/\[([^\]]+)\]/g, "[^/]+").replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "[^/]+").replace(/\//g, "\\/")}$`,
|
|
2475
|
+
"i"
|
|
2476
|
+
);
|
|
2477
|
+
return { verb: verb.toUpperCase(), regex, path };
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Normalize path for matching
|
|
2481
|
+
*
|
|
2482
|
+
* @param path - Raw path from request
|
|
2483
|
+
* @returns Normalized path
|
|
2484
|
+
*/
|
|
2485
|
+
normalizePath(path) {
|
|
2486
|
+
const pathWithoutQuery = path.split(/[?#]/)[0];
|
|
2487
|
+
const parts = pathWithoutQuery.split(/(%2[fF]|%5[cC])/);
|
|
2488
|
+
const decoded = parts.map((part, i) => {
|
|
2489
|
+
if (i % 2 === 1) return part;
|
|
2490
|
+
try {
|
|
2491
|
+
return decodeURIComponent(part);
|
|
2492
|
+
} catch {
|
|
2493
|
+
return part;
|
|
2494
|
+
}
|
|
2495
|
+
}).join("");
|
|
2496
|
+
return decoded.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/(.+?)\/+$/, "$1");
|
|
2497
|
+
}
|
|
2498
|
+
/**
|
|
2499
|
+
* Generate paywall HTML for browser requests
|
|
2500
|
+
*
|
|
2501
|
+
* @param paymentRequired - Payment required response
|
|
2502
|
+
* @param paywallConfig - Optional paywall configuration
|
|
2503
|
+
* @param customHtml - Optional custom HTML template
|
|
2504
|
+
* @returns HTML string
|
|
2505
|
+
*/
|
|
2506
|
+
generatePaywallHTML(paymentRequired, paywallConfig, customHtml) {
|
|
2507
|
+
if (customHtml) {
|
|
2508
|
+
return customHtml;
|
|
2509
|
+
}
|
|
2510
|
+
if (this.paywallProvider) {
|
|
2511
|
+
return this.paywallProvider.generateHtml(paymentRequired, paywallConfig);
|
|
2512
|
+
}
|
|
2513
|
+
try {
|
|
2514
|
+
const paywall = require("@bankofai/x402-paywall");
|
|
2515
|
+
const displayAmount = this.getDisplayAmount(paymentRequired);
|
|
2516
|
+
const resource = paymentRequired.resource;
|
|
2517
|
+
return paywall.getPaywallHtml({
|
|
2518
|
+
amount: displayAmount,
|
|
2519
|
+
paymentRequired,
|
|
2520
|
+
currentUrl: resource?.url || paywallConfig?.currentUrl || "",
|
|
2521
|
+
testnet: paywallConfig?.testnet ?? true,
|
|
2522
|
+
appName: paywallConfig?.appName,
|
|
2523
|
+
appLogo: paywallConfig?.appLogo,
|
|
2524
|
+
sessionTokenEndpoint: paywallConfig?.sessionTokenEndpoint
|
|
2525
|
+
});
|
|
2526
|
+
} catch {
|
|
2527
|
+
}
|
|
2528
|
+
return FALLBACK_PAYWALL_HTML;
|
|
2529
|
+
}
|
|
2530
|
+
/**
|
|
2531
|
+
* Extract display amount from payment requirements.
|
|
2532
|
+
* Uses the registered scheme's decimal precision for the asset, falling back to 6.
|
|
2533
|
+
*
|
|
2534
|
+
* @param paymentRequired - The payment required object
|
|
2535
|
+
* @returns The display amount in decimal format
|
|
2536
|
+
*/
|
|
2537
|
+
getDisplayAmount(paymentRequired) {
|
|
2538
|
+
const accepts = paymentRequired.accepts;
|
|
2539
|
+
if (accepts && accepts.length > 0) {
|
|
2540
|
+
const firstReq = accepts[0];
|
|
2541
|
+
if ("amount" in firstReq) {
|
|
2542
|
+
const decimals = this.ResourceServer.getAssetDecimalsForRequirements(firstReq);
|
|
2543
|
+
return parseFloat(firstReq.amount) / 10 ** decimals;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
return 0;
|
|
2547
|
+
}
|
|
2548
|
+
};
|
|
2549
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2550
|
+
0 && (module.exports = {
|
|
2551
|
+
FacilitatorResponseError,
|
|
2552
|
+
HTTPFacilitatorClient,
|
|
2553
|
+
RouteConfigurationError,
|
|
2554
|
+
SETTLEMENT_OVERRIDES_HEADER,
|
|
2555
|
+
assertAcceptsAdditiveExtraAfterSchemeEnrich,
|
|
2556
|
+
assertAcceptsAllowlistedAfterExtensionEnrich,
|
|
2557
|
+
assertAdditivePayloadEnrichment,
|
|
2558
|
+
assertAdditiveSettlementExtra,
|
|
2559
|
+
assertSettleResponseCoreUnchanged,
|
|
2560
|
+
checkIfBazaarNeeded,
|
|
2561
|
+
getFacilitatorResponseError,
|
|
2562
|
+
isVacantStringField,
|
|
2563
|
+
snapshotPaymentRequirementsList,
|
|
2564
|
+
snapshotSettleResponseCore,
|
|
2565
|
+
x402HTTPResourceServer,
|
|
2566
|
+
x402ResourceServer
|
|
2567
|
+
});
|
|
2568
|
+
//# sourceMappingURL=index.js.map
|