@elizaos/plugin-x402 2.0.0-alpha.5 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +57 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30914 -1844
- package/dist/index.js.map +114 -21
- package/dist/payment-config.d.ts +256 -0
- package/dist/payment-config.d.ts.map +1 -0
- package/dist/payment-wrapper.d.ts +42 -0
- package/dist/payment-wrapper.d.ts.map +1 -0
- package/dist/startup-validator.d.ts +28 -0
- package/dist/startup-validator.d.ts.map +1 -0
- package/dist/types.d.ts +158 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/x402-facilitator-binding.d.ts +9 -0
- package/dist/x402-facilitator-binding.d.ts.map +1 -0
- package/dist/x402-replay-durable.d.ts +30 -0
- package/dist/x402-replay-durable.d.ts.map +1 -0
- package/dist/x402-replay-guard.d.ts +28 -0
- package/dist/x402-replay-guard.d.ts.map +1 -0
- package/dist/x402-replay-keys.d.ts +21 -0
- package/dist/x402-replay-keys.d.ts.map +1 -0
- package/dist/x402-resolve.d.ts +6 -0
- package/dist/x402-resolve.d.ts.map +1 -0
- package/dist/x402-standard-payment.d.ts +130 -0
- package/dist/x402-standard-payment.d.ts.map +1 -0
- package/dist/x402-types.d.ts +130 -0
- package/dist/x402-types.d.ts.map +1 -0
- package/package.json +43 -94
- package/src/index.ts +113 -0
- package/src/payment-config.ts +737 -0
- package/src/payment-wrapper.ts +1991 -0
- package/src/startup-validator.ts +349 -0
- package/src/types.ts +177 -0
- package/src/x402-facilitator-binding.ts +104 -0
- package/src/x402-replay-durable.ts +320 -0
- package/src/x402-replay-guard.ts +165 -0
- package/src/x402-replay-keys.ts +151 -0
- package/src/x402-resolve.ts +43 -0
- package/src/x402-standard-payment.ts +519 -0
- package/src/x402-types.ts +376 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup validation for x402 payment system
|
|
3
|
+
* Validates payment configs and routes before the server starts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
Character,
|
|
8
|
+
CharacterX402Settings,
|
|
9
|
+
PaymentEnabledRoute,
|
|
10
|
+
Route,
|
|
11
|
+
} from "@elizaos/core";
|
|
12
|
+
import { logger } from "@elizaos/core";
|
|
13
|
+
import {
|
|
14
|
+
BUILT_IN_NETWORKS,
|
|
15
|
+
getPaymentConfig,
|
|
16
|
+
getX402Health,
|
|
17
|
+
listX402Configs,
|
|
18
|
+
paymentAddressIsBundledExample,
|
|
19
|
+
} from "./payment-config.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validation result with warnings and errors
|
|
23
|
+
*/
|
|
24
|
+
export interface StartupValidationResult {
|
|
25
|
+
valid: boolean;
|
|
26
|
+
errors: string[];
|
|
27
|
+
warnings: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate a payment config is properly configured
|
|
32
|
+
*/
|
|
33
|
+
function validatePaymentConfig(
|
|
34
|
+
configName: string,
|
|
35
|
+
agentId?: string,
|
|
36
|
+
): {
|
|
37
|
+
errors: string[];
|
|
38
|
+
warnings: string[];
|
|
39
|
+
} {
|
|
40
|
+
const errors: string[] = [];
|
|
41
|
+
const warnings: string[] = [];
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const config = getPaymentConfig(configName, agentId);
|
|
45
|
+
|
|
46
|
+
// Check required fields
|
|
47
|
+
if (!config.network) {
|
|
48
|
+
errors.push(`Config '${configName}': missing 'network'`);
|
|
49
|
+
}
|
|
50
|
+
if (!config.assetNamespace) {
|
|
51
|
+
errors.push(`Config '${configName}': missing 'assetNamespace'`);
|
|
52
|
+
}
|
|
53
|
+
if (!config.assetReference) {
|
|
54
|
+
errors.push(`Config '${configName}': missing 'assetReference'`);
|
|
55
|
+
}
|
|
56
|
+
if (!config.paymentAddress) {
|
|
57
|
+
errors.push(
|
|
58
|
+
`Config '${configName}': missing 'paymentAddress' (wallet address required)`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!config.symbol) {
|
|
62
|
+
errors.push(`Config '${configName}': missing 'symbol'`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Validate address format
|
|
66
|
+
if (config.paymentAddress) {
|
|
67
|
+
// Solana addresses: base58, 32-44 chars
|
|
68
|
+
if (config.network === "SOLANA") {
|
|
69
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(config.paymentAddress)) {
|
|
70
|
+
errors.push(`Config '${configName}': invalid Solana address format`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// EVM addresses: 0x + 40 hex chars
|
|
74
|
+
else if (
|
|
75
|
+
config.network === "BASE" ||
|
|
76
|
+
config.network === "POLYGON" ||
|
|
77
|
+
config.assetNamespace === "erc20"
|
|
78
|
+
) {
|
|
79
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(config.paymentAddress)) {
|
|
80
|
+
errors.push(
|
|
81
|
+
`Config '${configName}': invalid EVM address format (should be 0x...)`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check if address looks like default/example
|
|
87
|
+
if (
|
|
88
|
+
config.paymentAddress === "0x0000000000000000000000000000000000000000"
|
|
89
|
+
) {
|
|
90
|
+
warnings.push(
|
|
91
|
+
`Config '${configName}': using zero address (0x0...0) - is this intentional?`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate asset reference (contract address / token mint)
|
|
97
|
+
if (config.assetReference && config.assetNamespace === "erc20") {
|
|
98
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(config.assetReference)) {
|
|
99
|
+
errors.push(
|
|
100
|
+
`Config '${configName}': invalid ERC20 token address format`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (paymentAddressIsBundledExample(config.network, config.paymentAddress)) {
|
|
106
|
+
if (process.env.NODE_ENV === "production") {
|
|
107
|
+
errors.push(
|
|
108
|
+
`Config '${configName}': paymentAddress is the bundled dev example for ${config.network}. Set ${config.network}_PUBLIC_KEY or PAYMENT_WALLET_${config.network} to your payout wallet before production.`,
|
|
109
|
+
);
|
|
110
|
+
} else {
|
|
111
|
+
warnings.push(
|
|
112
|
+
`Config '${configName}': paymentAddress matches the bundled dev example for ${config.network} — set env payout keys for real settlement.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check if network is built-in (warn if custom)
|
|
118
|
+
if (
|
|
119
|
+
!(BUILT_IN_NETWORKS as readonly string[]).includes(
|
|
120
|
+
config.network as string,
|
|
121
|
+
)
|
|
122
|
+
) {
|
|
123
|
+
warnings.push(
|
|
124
|
+
`Config '${configName}': using custom network '${config.network}' ` +
|
|
125
|
+
`(not in built-in networks: ${BUILT_IN_NETWORKS.join(", ")})`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
errors.push(
|
|
130
|
+
`Config '${configName}': ${error instanceof Error ? error.message : "unknown error"}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { errors, warnings };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Validate an x402 route configuration
|
|
139
|
+
*/
|
|
140
|
+
function validateX402Route(
|
|
141
|
+
route: Route,
|
|
142
|
+
character?: Character,
|
|
143
|
+
agentId?: string,
|
|
144
|
+
): { errors: string[]; warnings: string[] } {
|
|
145
|
+
const errors: string[] = [];
|
|
146
|
+
const warnings: string[] = [];
|
|
147
|
+
const x402Route = route as PaymentEnabledRoute;
|
|
148
|
+
|
|
149
|
+
if (!route.path) {
|
|
150
|
+
errors.push(`Route missing 'path' property`);
|
|
151
|
+
return { errors, warnings };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const routePath = route.path;
|
|
155
|
+
|
|
156
|
+
if (x402Route.x402 == null) {
|
|
157
|
+
return { errors, warnings };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const cx = character?.settings?.x402 as CharacterX402Settings | undefined;
|
|
161
|
+
const raw = x402Route.x402;
|
|
162
|
+
let priceInCents: number | undefined;
|
|
163
|
+
let paymentConfigs: string[] | undefined;
|
|
164
|
+
|
|
165
|
+
if (raw === true) {
|
|
166
|
+
priceInCents = cx?.defaultPriceInCents;
|
|
167
|
+
paymentConfigs = cx?.defaultPaymentConfigs as string[] | undefined;
|
|
168
|
+
if (priceInCents == null) {
|
|
169
|
+
errors.push(
|
|
170
|
+
`${routePath}: x402: true requires character.settings.x402.defaultPriceInCents`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (!paymentConfigs?.length) {
|
|
174
|
+
errors.push(
|
|
175
|
+
`${routePath}: x402: true requires character.settings.x402.defaultPaymentConfigs (non-empty array)`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
} else if (typeof raw === "object") {
|
|
179
|
+
priceInCents = raw.priceInCents ?? cx?.defaultPriceInCents;
|
|
180
|
+
paymentConfigs = (raw.paymentConfigs ?? cx?.defaultPaymentConfigs) as
|
|
181
|
+
| string[]
|
|
182
|
+
| undefined;
|
|
183
|
+
if (priceInCents == null) {
|
|
184
|
+
errors.push(
|
|
185
|
+
`${routePath}: x402.priceInCents is required (or set character.settings.x402.defaultPriceInCents)`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (!paymentConfigs?.length) {
|
|
189
|
+
errors.push(
|
|
190
|
+
`${routePath}: x402.paymentConfigs is required (or set character.settings.x402.defaultPaymentConfigs)`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (priceInCents !== undefined && priceInCents !== null) {
|
|
196
|
+
if (typeof priceInCents !== "number") {
|
|
197
|
+
errors.push(`${routePath}: resolved x402.priceInCents must be a number`);
|
|
198
|
+
} else if (priceInCents <= 0) {
|
|
199
|
+
errors.push(`${routePath}: x402.priceInCents must be > 0`);
|
|
200
|
+
} else if (!Number.isInteger(priceInCents)) {
|
|
201
|
+
errors.push(`${routePath}: x402.priceInCents must be an integer (cents)`);
|
|
202
|
+
} else if (priceInCents > 10000) {
|
|
203
|
+
warnings.push(
|
|
204
|
+
`${routePath}: price is $${(priceInCents / 100).toFixed(2)} — is this intentional?`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (paymentConfigs && !Array.isArray(paymentConfigs)) {
|
|
210
|
+
errors.push(`${routePath}: x402.paymentConfigs must be an array`);
|
|
211
|
+
} else if (paymentConfigs?.length === 0) {
|
|
212
|
+
errors.push(`${routePath}: x402.paymentConfigs cannot be empty`);
|
|
213
|
+
} else if (paymentConfigs?.length) {
|
|
214
|
+
const availableConfigs = listX402Configs(agentId);
|
|
215
|
+
for (const configName of paymentConfigs) {
|
|
216
|
+
if (typeof configName !== "string") {
|
|
217
|
+
errors.push(
|
|
218
|
+
`${routePath}: x402.paymentConfigs contains non-string value`,
|
|
219
|
+
);
|
|
220
|
+
} else if (!availableConfigs.includes(configName)) {
|
|
221
|
+
errors.push(
|
|
222
|
+
`${routePath}: unknown payment config '${configName}'. Available: ${availableConfigs.join(", ")}`,
|
|
223
|
+
);
|
|
224
|
+
} else {
|
|
225
|
+
const configValidation = validatePaymentConfig(configName, agentId);
|
|
226
|
+
errors.push(
|
|
227
|
+
...configValidation.errors.map((e) => `${routePath}: ${e}`),
|
|
228
|
+
);
|
|
229
|
+
warnings.push(
|
|
230
|
+
...configValidation.warnings.map((w) => `${routePath}: ${w}`),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!route.handler) {
|
|
237
|
+
errors.push(
|
|
238
|
+
`${routePath}: route has x402 protection but no handler function`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { errors, warnings };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Validate environment configuration
|
|
247
|
+
*/
|
|
248
|
+
function validateEnvironment(): { errors: string[]; warnings: string[] } {
|
|
249
|
+
const errors: string[] = [];
|
|
250
|
+
const warnings: string[] = [];
|
|
251
|
+
|
|
252
|
+
// Check network configuration
|
|
253
|
+
const health = getX402Health();
|
|
254
|
+
|
|
255
|
+
for (const network of health.networks) {
|
|
256
|
+
if (!network.configured || !network.address) {
|
|
257
|
+
warnings.push(
|
|
258
|
+
`Network '${network.network}' not configured. ` +
|
|
259
|
+
`Set ${network.network}_PUBLIC_KEY in .env to accept payments on this network.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check facilitator configuration (optional)
|
|
265
|
+
if (!health.facilitator.configured) {
|
|
266
|
+
warnings.push(
|
|
267
|
+
"X402_FACILITATOR_URL not set. Direct blockchain verification will be used. " +
|
|
268
|
+
"Consider setting up a facilitator for better UX.",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (
|
|
273
|
+
process.env.NODE_ENV === "production" &&
|
|
274
|
+
(process.env.X402_TEST_MODE === "true" ||
|
|
275
|
+
process.env.X402_TEST_MODE === "1")
|
|
276
|
+
) {
|
|
277
|
+
warnings.push(
|
|
278
|
+
"X402_TEST_MODE is set while NODE_ENV=production — clients can bypass payment verification; unset X402_TEST_MODE in production.",
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { errors, warnings };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Comprehensive startup validation
|
|
287
|
+
* Call this before starting the server to catch configuration issues early
|
|
288
|
+
*/
|
|
289
|
+
export function validateX402Startup(
|
|
290
|
+
routes: Route[],
|
|
291
|
+
character?: Character,
|
|
292
|
+
options?: { agentId?: string },
|
|
293
|
+
): StartupValidationResult {
|
|
294
|
+
const allErrors: string[] = [];
|
|
295
|
+
const allWarnings: string[] = [];
|
|
296
|
+
|
|
297
|
+
let protectedRouteCount = 0;
|
|
298
|
+
for (const route of routes) {
|
|
299
|
+
const x402Route = route as PaymentEnabledRoute;
|
|
300
|
+
if (x402Route.x402 != null) {
|
|
301
|
+
protectedRouteCount++;
|
|
302
|
+
const routeValidation = validateX402Route(
|
|
303
|
+
route,
|
|
304
|
+
character,
|
|
305
|
+
options?.agentId,
|
|
306
|
+
);
|
|
307
|
+
allErrors.push(...routeValidation.errors);
|
|
308
|
+
allWarnings.push(...routeValidation.warnings);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (protectedRouteCount > 0) {
|
|
313
|
+
const envValidation = validateEnvironment();
|
|
314
|
+
allErrors.push(...envValidation.errors);
|
|
315
|
+
allWarnings.push(...envValidation.warnings);
|
|
316
|
+
|
|
317
|
+
logger.info(
|
|
318
|
+
`[x402] validated ${protectedRouteCount}/${routes.length} protected route(s); ` +
|
|
319
|
+
`configs=${listX402Configs(options?.agentId).length}, ` +
|
|
320
|
+
`errors=${allErrors.length}, warnings=${allWarnings.length}`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
valid: allErrors.length === 0,
|
|
326
|
+
errors: allErrors,
|
|
327
|
+
warnings: allWarnings,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Validate routes and throw if invalid
|
|
333
|
+
* This is used by applyPaymentProtection to fail fast on startup
|
|
334
|
+
*/
|
|
335
|
+
export function validateAndThrowIfInvalid(
|
|
336
|
+
routes: Route[],
|
|
337
|
+
character?: Character,
|
|
338
|
+
options?: { agentId?: string },
|
|
339
|
+
): void {
|
|
340
|
+
const result = validateX402Startup(routes, character, options);
|
|
341
|
+
|
|
342
|
+
if (!result.valid) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`x402 Configuration Invalid (${result.errors.length} error${result.errors.length > 1 ? "s" : ""}):\n\n` +
|
|
345
|
+
result.errors.map((e) => ` • ${e}`).join("\n") +
|
|
346
|
+
"\n\nPlease fix these errors and try again.",
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict TypeScript types for x402 payment middleware
|
|
3
|
+
* Replaces all 'any' types with proper interfaces
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AgentRuntime, RouteRequest } from "@elizaos/core";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Request shape for x402 (matches plugin routes + IncomingMessage headers)
|
|
10
|
+
*/
|
|
11
|
+
export type X402Request = RouteRequest & {
|
|
12
|
+
method: string;
|
|
13
|
+
path: string;
|
|
14
|
+
headers: Record<string, string | string[] | undefined>;
|
|
15
|
+
query: Record<string, string | string[] | undefined>;
|
|
16
|
+
params: Record<string, string>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Express-like response object
|
|
21
|
+
*/
|
|
22
|
+
export interface X402Response {
|
|
23
|
+
status(code: number): X402ResponseStatus;
|
|
24
|
+
json(data: unknown): void;
|
|
25
|
+
setHeader?(name: string, value: string | readonly string[]): void;
|
|
26
|
+
headersSent?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface X402ResponseStatus {
|
|
30
|
+
json(data: unknown): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* EIP-712 Authorization data structure
|
|
35
|
+
*/
|
|
36
|
+
export interface EIP712Authorization {
|
|
37
|
+
from: string;
|
|
38
|
+
to: string;
|
|
39
|
+
value: string;
|
|
40
|
+
validAfter: string;
|
|
41
|
+
validBefore: string;
|
|
42
|
+
nonce: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* EIP-712 Domain structure
|
|
47
|
+
*/
|
|
48
|
+
export interface EIP712Domain {
|
|
49
|
+
name: string;
|
|
50
|
+
version: string;
|
|
51
|
+
chainId: number;
|
|
52
|
+
verifyingContract: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Export for use in payment-wrapper
|
|
56
|
+
export type {
|
|
57
|
+
EIP712Authorization as EIP712AuthorizationType,
|
|
58
|
+
EIP712Domain as EIP712DomainType,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Payment proof data (EIP-712 format)
|
|
63
|
+
*/
|
|
64
|
+
export interface EIP712PaymentProof {
|
|
65
|
+
signature: string;
|
|
66
|
+
authorization: EIP712Authorization;
|
|
67
|
+
domain?: EIP712Domain;
|
|
68
|
+
network?: string;
|
|
69
|
+
scheme?: string;
|
|
70
|
+
// Alternative format with v, r, s
|
|
71
|
+
v?: number;
|
|
72
|
+
r?: string;
|
|
73
|
+
s?: string;
|
|
74
|
+
// Wrapped format from gateways
|
|
75
|
+
payload?: {
|
|
76
|
+
signature: string;
|
|
77
|
+
authorization: EIP712Authorization;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Solana payment proof
|
|
83
|
+
*/
|
|
84
|
+
export interface SolanaPaymentProof {
|
|
85
|
+
signature: string;
|
|
86
|
+
network: "SOLANA";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Legacy payment proof format
|
|
91
|
+
*/
|
|
92
|
+
export interface LegacyPaymentProof {
|
|
93
|
+
network: string;
|
|
94
|
+
address: string;
|
|
95
|
+
signature: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Runtime interface with required methods for x402
|
|
100
|
+
* Uses IAgentRuntime directly to avoid type conflicts
|
|
101
|
+
*/
|
|
102
|
+
export type X402Runtime = AgentRuntime;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Payment verification parameters (route price + allowed presets)
|
|
106
|
+
*/
|
|
107
|
+
export interface PaymentVerificationParams {
|
|
108
|
+
paymentProof?: string;
|
|
109
|
+
paymentId?: string;
|
|
110
|
+
route: string;
|
|
111
|
+
/** Integer USD cents (same as route `x402.priceInCents`) */
|
|
112
|
+
priceInCents: number;
|
|
113
|
+
/** Names from `x402.paymentConfigs` (resolved), in declaration order */
|
|
114
|
+
paymentConfigNames: string[];
|
|
115
|
+
agentId?: string;
|
|
116
|
+
runtime: X402Runtime;
|
|
117
|
+
req?: X402Request;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Successful verification metadata for events / receipts */
|
|
121
|
+
export interface PaymentVerifiedDetails {
|
|
122
|
+
paymentConfig: string;
|
|
123
|
+
network: string;
|
|
124
|
+
/** Smallest units of the paid asset */
|
|
125
|
+
amountAtomic: string;
|
|
126
|
+
symbol?: string;
|
|
127
|
+
payer?: string;
|
|
128
|
+
proofId?: string;
|
|
129
|
+
paymentResponse?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type VerifyPaymentResult =
|
|
133
|
+
| { ok: false }
|
|
134
|
+
| { ok: true; details: PaymentVerifiedDetails };
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Payment receipt for tracking
|
|
138
|
+
*/
|
|
139
|
+
export interface PaymentReceipt {
|
|
140
|
+
paymentId: string;
|
|
141
|
+
route: string;
|
|
142
|
+
amount: string;
|
|
143
|
+
network: string;
|
|
144
|
+
timestamp: number;
|
|
145
|
+
signature?: string;
|
|
146
|
+
verified: boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Facilitator verification response
|
|
151
|
+
*/
|
|
152
|
+
export interface FacilitatorVerificationResponse {
|
|
153
|
+
valid?: boolean;
|
|
154
|
+
verified?: boolean;
|
|
155
|
+
status?: string;
|
|
156
|
+
message?: string;
|
|
157
|
+
/** When present, must equal the route’s `resource` URL we sent on verify */
|
|
158
|
+
resource?: string;
|
|
159
|
+
/** When present, must match the plugin route path */
|
|
160
|
+
routePath?: string;
|
|
161
|
+
/** Alias some facilitators use for path */
|
|
162
|
+
route?: string;
|
|
163
|
+
/** When present, must match the route’s `priceInCents` */
|
|
164
|
+
priceInCents?: number;
|
|
165
|
+
/** When present, must be one of the route’s allowed preset names */
|
|
166
|
+
paymentConfig?: string;
|
|
167
|
+
/** When present, every entry must be in the route’s allowlist */
|
|
168
|
+
paymentConfigs?: string[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Sent to the facilitator so responses can be bound to a specific purchase */
|
|
172
|
+
export interface FacilitatorVerifyContext {
|
|
173
|
+
resource: string;
|
|
174
|
+
routePath: string;
|
|
175
|
+
priceInCents: number;
|
|
176
|
+
paymentConfigNames: string[];
|
|
177
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FacilitatorVerificationResponse,
|
|
3
|
+
FacilitatorVerifyContext,
|
|
4
|
+
} from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* When true, facilitator JSON must echo `resource`, route, `priceInCents`, and
|
|
8
|
+
* `paymentConfig` so a generic 200 cannot unlock unrelated routes.
|
|
9
|
+
* Set `X402_FACILITATOR_RELAXED_BINDING=1` if your facilitator does not return these fields yet.
|
|
10
|
+
*/
|
|
11
|
+
export function isFacilitatorBindingRelaxed(): boolean {
|
|
12
|
+
return (
|
|
13
|
+
process.env.X402_FACILITATOR_RELAXED_BINDING === "true" ||
|
|
14
|
+
process.env.X402_FACILITATOR_RELAXED_BINDING === "1"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function relaxedPayloadMatchesContext(
|
|
19
|
+
data: FacilitatorVerificationResponse,
|
|
20
|
+
ctx: FacilitatorVerifyContext,
|
|
21
|
+
): boolean {
|
|
22
|
+
if (typeof data.resource === "string" && data.resource !== ctx.resource) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (typeof data.routePath === "string" && data.routePath !== ctx.routePath) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (typeof data.route === "string" && data.route !== ctx.routePath) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (
|
|
32
|
+
typeof data.priceInCents === "number" &&
|
|
33
|
+
Number.isFinite(data.priceInCents) &&
|
|
34
|
+
data.priceInCents !== ctx.priceInCents
|
|
35
|
+
) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (typeof data.paymentConfig === "string") {
|
|
39
|
+
if (!ctx.paymentConfigNames.includes(data.paymentConfig)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(data.paymentConfigs)) {
|
|
44
|
+
for (const n of data.paymentConfigs) {
|
|
45
|
+
if (typeof n === "string" && !ctx.paymentConfigNames.includes(n)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function strictPaymentConfigOk(
|
|
54
|
+
data: FacilitatorVerificationResponse,
|
|
55
|
+
ctx: FacilitatorVerifyContext,
|
|
56
|
+
): boolean {
|
|
57
|
+
if (typeof data.paymentConfig === "string") {
|
|
58
|
+
return ctx.paymentConfigNames.includes(data.paymentConfig);
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(data.paymentConfigs) && data.paymentConfigs.length > 0) {
|
|
61
|
+
const names = data.paymentConfigs.filter(
|
|
62
|
+
(x): x is string => typeof x === "string",
|
|
63
|
+
);
|
|
64
|
+
if (names.length === 0) return false;
|
|
65
|
+
return names.every((n) => ctx.paymentConfigNames.includes(n));
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function strictPayloadMatchesContext(
|
|
71
|
+
data: FacilitatorVerificationResponse,
|
|
72
|
+
ctx: FacilitatorVerifyContext,
|
|
73
|
+
): boolean {
|
|
74
|
+
if (typeof data.resource !== "string" || data.resource !== ctx.resource) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const routeOk =
|
|
78
|
+
(typeof data.routePath === "string" && data.routePath === ctx.routePath) ||
|
|
79
|
+
(typeof data.route === "string" && data.route === ctx.routePath);
|
|
80
|
+
if (!routeOk) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (
|
|
84
|
+
typeof data.priceInCents !== "number" ||
|
|
85
|
+
!Number.isFinite(data.priceInCents) ||
|
|
86
|
+
data.priceInCents !== ctx.priceInCents
|
|
87
|
+
) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (!strictPaymentConfigOk(data, ctx)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function facilitatorVerifyResponseMatchesRoute(
|
|
97
|
+
data: FacilitatorVerificationResponse,
|
|
98
|
+
ctx: FacilitatorVerifyContext,
|
|
99
|
+
relaxed: boolean,
|
|
100
|
+
): boolean {
|
|
101
|
+
return relaxed
|
|
102
|
+
? relaxedPayloadMatchesContext(data, ctx)
|
|
103
|
+
: strictPayloadMatchesContext(data, ctx);
|
|
104
|
+
}
|