@ch4p/plugin-x402 0.1.4

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.js ADDED
@@ -0,0 +1,330 @@
1
+ // src/middleware.ts
2
+ var PUBLIC_PATHS = /* @__PURE__ */ new Set([
3
+ "/health",
4
+ "/.well-known/agent.json",
5
+ "/pair"
6
+ ]);
7
+ var DEFAULT_ASSET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
8
+ var DEFAULT_NETWORK = "base";
9
+ var DEFAULT_TIMEOUT = 300;
10
+ function pathMatches(urlPath, pattern) {
11
+ if (pattern === "*" || pattern === "/**") return true;
12
+ if (pattern.endsWith("/*")) {
13
+ const prefix = pattern.slice(0, -2);
14
+ return urlPath === prefix || urlPath.startsWith(prefix + "/");
15
+ }
16
+ return urlPath === pattern;
17
+ }
18
+ function extractPath(url) {
19
+ const qIdx = url.indexOf("?");
20
+ return qIdx >= 0 ? url.slice(0, qIdx) : url;
21
+ }
22
+ function send402(res, requirements) {
23
+ const body = JSON.stringify({
24
+ x402Version: 1,
25
+ error: "X402",
26
+ accepts: [requirements]
27
+ });
28
+ res.writeHead(402, {
29
+ "Content-Type": "application/json",
30
+ "Content-Length": Buffer.byteLength(body)
31
+ });
32
+ res.end(body);
33
+ }
34
+ function decodePaymentHeader(header) {
35
+ try {
36
+ const json = Buffer.from(header, "base64").toString("utf-8");
37
+ const parsed = JSON.parse(json);
38
+ if (typeof parsed !== "object" || parsed === null) return null;
39
+ const p = parsed;
40
+ if (p["x402Version"] !== 1) return null;
41
+ if (p["scheme"] !== "exact") return null;
42
+ if (typeof p["network"] !== "string") return null;
43
+ const payload = p["payload"];
44
+ if (typeof payload !== "object" || payload === null) return null;
45
+ const pl = payload;
46
+ if (typeof pl["signature"] !== "string") return null;
47
+ const auth = pl["authorization"];
48
+ if (typeof auth !== "object" || auth === null) return null;
49
+ const a = auth;
50
+ if (typeof a["from"] !== "string" || typeof a["to"] !== "string" || typeof a["value"] !== "string") {
51
+ return null;
52
+ }
53
+ return parsed;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+ function createX402Middleware(config) {
59
+ if (!config.enabled || !config.server) return null;
60
+ const serverCfg = config.server;
61
+ const asset = serverCfg.asset ?? DEFAULT_ASSET;
62
+ const network = serverCfg.network ?? DEFAULT_NETWORK;
63
+ const maxTimeoutSeconds = serverCfg.maxTimeoutSeconds ?? DEFAULT_TIMEOUT;
64
+ const description = serverCfg.description ?? "Payment required to access this gateway resource.";
65
+ const protectedPaths = serverCfg.protectedPaths ?? ["/*"];
66
+ return async (req, res) => {
67
+ const rawUrl = req.url ?? "/";
68
+ const urlPath = extractPath(rawUrl);
69
+ if (PUBLIC_PATHS.has(urlPath)) return false;
70
+ const isProtected = protectedPaths.some((p) => pathMatches(urlPath, p));
71
+ if (!isProtected) return false;
72
+ const requirements = {
73
+ scheme: "exact",
74
+ network,
75
+ maxAmountRequired: serverCfg.amount,
76
+ resource: urlPath,
77
+ description,
78
+ mimeType: "application/json",
79
+ payTo: serverCfg.payTo,
80
+ maxTimeoutSeconds,
81
+ asset,
82
+ extra: {}
83
+ };
84
+ const paymentHeader = req.headers["x-payment"];
85
+ if (!paymentHeader || typeof paymentHeader !== "string") {
86
+ send402(res, requirements);
87
+ return true;
88
+ }
89
+ const payment = decodePaymentHeader(paymentHeader);
90
+ if (!payment) {
91
+ send402(res, requirements);
92
+ return true;
93
+ }
94
+ if (payment.network !== network) {
95
+ send402(res, requirements);
96
+ return true;
97
+ }
98
+ if (serverCfg.verifyPayment) {
99
+ const allowed = await serverCfg.verifyPayment(payment, requirements);
100
+ if (!allowed) {
101
+ send402(res, requirements);
102
+ return true;
103
+ }
104
+ }
105
+ req["_x402Authenticated"] = true;
106
+ return false;
107
+ };
108
+ }
109
+
110
+ // src/x402-pay-tool.ts
111
+ var PLACEHOLDER_SIG = "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
112
+ var X402PayTool = class {
113
+ name = "x402_pay";
114
+ description = "Generate an X-PAYMENT header for a resource that returned HTTP 402 Payment Required. Provide the 402 response body JSON and a payer wallet address. Returns the base64-encoded X-PAYMENT value to include when retrying the request. Full payment execution requires an IIdentityProvider with wallet signing support.";
115
+ weight = "lightweight";
116
+ parameters = {
117
+ type: "object",
118
+ properties: {
119
+ url: {
120
+ type: "string",
121
+ description: "The URL of the resource that returned 402 (used to match the requirement).",
122
+ minLength: 1
123
+ },
124
+ x402_response: {
125
+ type: "string",
126
+ description: "The JSON body of the 402 response. Stringify the x402 error response object.",
127
+ minLength: 2
128
+ },
129
+ wallet_address: {
130
+ type: "string",
131
+ description: "Payer wallet address (0x + 40 hex chars). If omitted, the identity provider wallet address is used when available.",
132
+ pattern: "^0x[0-9a-fA-F]{40}$"
133
+ }
134
+ },
135
+ required: ["url", "x402_response"],
136
+ additionalProperties: false
137
+ };
138
+ validate(args) {
139
+ if (typeof args !== "object" || args === null) {
140
+ return { valid: false, errors: ["Arguments must be an object."] };
141
+ }
142
+ const { url, x402_response, wallet_address } = args;
143
+ const errors = [];
144
+ if (typeof url !== "string" || url.trim().length === 0) {
145
+ errors.push("url must be a non-empty string.");
146
+ }
147
+ if (typeof x402_response !== "string" || x402_response.trim().length === 0) {
148
+ errors.push("x402_response must be a non-empty string.");
149
+ }
150
+ if (wallet_address !== void 0) {
151
+ if (typeof wallet_address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(wallet_address)) {
152
+ errors.push("wallet_address must be a valid Ethereum address (0x + 40 hex chars).");
153
+ }
154
+ }
155
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
156
+ }
157
+ async execute(args, context) {
158
+ const validation = this.validate(args);
159
+ if (!validation.valid) {
160
+ return {
161
+ success: false,
162
+ output: "",
163
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
164
+ };
165
+ }
166
+ const { url, x402_response, wallet_address } = args;
167
+ const x402Context = context;
168
+ let response;
169
+ try {
170
+ response = JSON.parse(x402_response);
171
+ } catch {
172
+ return { success: false, output: "", error: "x402_response is not valid JSON." };
173
+ }
174
+ if (!Array.isArray(response.accepts) || response.accepts.length === 0) {
175
+ return {
176
+ success: false,
177
+ output: "",
178
+ error: 'x402 response contains no payment requirements in "accepts".'
179
+ };
180
+ }
181
+ const requirements = response.accepts.find((r) => r.scheme === "exact" && r.network === "base") ?? response.accepts.find((r) => r.scheme === "exact") ?? response.accepts[0];
182
+ const payer = wallet_address ?? x402Context.agentWalletAddress;
183
+ if (!payer) {
184
+ return {
185
+ success: false,
186
+ output: "",
187
+ error: "No wallet address available. Provide wallet_address or configure agentWalletAddress via toolContextExtensions in the agent runtime."
188
+ };
189
+ }
190
+ const nowSecs = Math.floor(Date.now() / 1e3);
191
+ const randomBytes = new Uint8Array(32);
192
+ if (typeof globalThis.crypto !== "undefined") {
193
+ globalThis.crypto.getRandomValues(randomBytes);
194
+ } else {
195
+ console.warn("x402: crypto.getRandomValues unavailable; using insecure Math.random fallback for nonce.");
196
+ for (let i = 0; i < 32; i++) {
197
+ randomBytes[i] = Math.floor(Math.random() * 256);
198
+ }
199
+ }
200
+ const nonce = "0x" + Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
201
+ const authorization = {
202
+ from: payer,
203
+ to: requirements.payTo,
204
+ value: requirements.maxAmountRequired,
205
+ validAfter: "0",
206
+ validBefore: String(nowSecs + requirements.maxTimeoutSeconds),
207
+ nonce
208
+ };
209
+ let signature;
210
+ let unsigned = false;
211
+ if (x402Context.x402Signer) {
212
+ try {
213
+ signature = await x402Context.x402Signer(authorization);
214
+ } catch (err) {
215
+ return {
216
+ success: false,
217
+ output: "",
218
+ error: `Signing failed: ${err.message}`
219
+ };
220
+ }
221
+ } else {
222
+ signature = PLACEHOLDER_SIG;
223
+ unsigned = true;
224
+ }
225
+ const paymentPayload = {
226
+ x402Version: 1,
227
+ scheme: "exact",
228
+ network: requirements.network,
229
+ payload: { signature, authorization }
230
+ };
231
+ const headerValue = Buffer.from(JSON.stringify(paymentPayload)).toString("base64");
232
+ const lines = [
233
+ `Resource: ${url}`,
234
+ `Network: ${requirements.network}`,
235
+ `Amount: ${requirements.maxAmountRequired} (asset ${requirements.asset})`,
236
+ `Pay to: ${requirements.payTo}`,
237
+ `From: ${payer}`,
238
+ "",
239
+ "X-PAYMENT header value (add to your retry request):",
240
+ headerValue
241
+ ];
242
+ if (unsigned) {
243
+ lines.push(
244
+ "",
245
+ "WARNING: Placeholder signature \u2014 cannot be used for real on-chain payments.",
246
+ "Configure an IIdentityProvider with a bound wallet to enable live signing."
247
+ );
248
+ }
249
+ return {
250
+ success: true,
251
+ output: lines.join("\n"),
252
+ metadata: {
253
+ headerValue,
254
+ network: requirements.network,
255
+ amount: requirements.maxAmountRequired,
256
+ payTo: requirements.payTo,
257
+ asset: requirements.asset,
258
+ unsigned
259
+ }
260
+ };
261
+ }
262
+ };
263
+
264
+ // src/signer.ts
265
+ import { ethers } from "ethers";
266
+ var KNOWN_TOKENS = {
267
+ base: {
268
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
269
+ chainId: 8453,
270
+ name: "USD Coin",
271
+ version: "2"
272
+ },
273
+ "base-sepolia": {
274
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
275
+ chainId: 84532,
276
+ name: "USD Coin",
277
+ version: "2"
278
+ }
279
+ };
280
+ var TRANSFER_WITH_AUTHORIZATION_TYPES = {
281
+ TransferWithAuthorization: [
282
+ { name: "from", type: "address" },
283
+ { name: "to", type: "address" },
284
+ { name: "value", type: "uint256" },
285
+ { name: "validAfter", type: "uint256" },
286
+ { name: "validBefore", type: "uint256" },
287
+ { name: "nonce", type: "bytes32" }
288
+ ]
289
+ };
290
+ function assertValidPrivateKey(key) {
291
+ if (!/^0x[a-fA-F0-9]{64}$/.test(key)) {
292
+ throw new Error(
293
+ "Invalid private key: expected a 0x-prefixed 64-character hex string (32 bytes)."
294
+ );
295
+ }
296
+ }
297
+ function createEIP712Signer(privateKey, opts = {}) {
298
+ assertValidPrivateKey(privateKey);
299
+ const wallet = new ethers.Wallet(privateKey);
300
+ const domain = {
301
+ name: opts.tokenName ?? KNOWN_TOKENS.base.name,
302
+ version: opts.tokenVersion ?? KNOWN_TOKENS.base.version,
303
+ chainId: opts.chainId ?? KNOWN_TOKENS.base.chainId,
304
+ verifyingContract: opts.tokenAddress ?? KNOWN_TOKENS.base.address
305
+ };
306
+ return async (auth) => {
307
+ const message = {
308
+ from: auth.from,
309
+ to: auth.to,
310
+ value: BigInt(auth.value),
311
+ validAfter: BigInt(auth.validAfter),
312
+ validBefore: BigInt(auth.validBefore),
313
+ nonce: auth.nonce
314
+ };
315
+ return wallet.signTypedData(domain, TRANSFER_WITH_AUTHORIZATION_TYPES, message);
316
+ };
317
+ }
318
+ function walletAddress(privateKey) {
319
+ assertValidPrivateKey(privateKey);
320
+ return new ethers.Wallet(privateKey).address;
321
+ }
322
+ export {
323
+ KNOWN_TOKENS,
324
+ X402PayTool,
325
+ createEIP712Signer,
326
+ createX402Middleware,
327
+ decodePaymentHeader,
328
+ pathMatches,
329
+ walletAddress
330
+ };
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@ch4p/plugin-x402",
3
+ "version": "0.1.4",
4
+ "description": "x402 HTTP micropayment plugin for ch4p — server middleware and agent payment tool",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "dependencies": {
16
+ "ethers": "^6.14.0",
17
+ "@ch4p/core": "0.1.4"
18
+ },
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format esm --dts",
21
+ "dev": "tsup src/index.ts --format esm --dts --watch",
22
+ "clean": "rm -rf dist"
23
+ }
24
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @ch4p/plugin-x402 — x402 HTTP micropayment plugin
3
+ *
4
+ * Provides two components:
5
+ *
6
+ * **Server-side** (`createX402Middleware`):
7
+ * Register on GatewayServer via the `preHandler` option to protect gateway
8
+ * endpoints with HTTP 402 Payment Required. Responds with structured x402
9
+ * payment requirements and verifies X-PAYMENT headers on retry.
10
+ *
11
+ * **Client-side** (`X402PayTool`):
12
+ * Register in the agent's ToolRegistry to let the agent pay for x402-gated
13
+ * resources. Builds the X-PAYMENT header value from a 402 response body.
14
+ * Plug in an `x402Signer` via toolContextExtensions for live signing.
15
+ *
16
+ * Config key in ~/.ch4p/config.json: `"x402"`
17
+ */
18
+
19
+ export { createX402Middleware } from './middleware.js';
20
+ export type { X402PreHandler } from './middleware.js';
21
+ export { pathMatches, decodePaymentHeader } from './middleware.js';
22
+
23
+ export { X402PayTool } from './x402-pay-tool.js';
24
+ export type { X402ToolContext } from './x402-pay-tool.js';
25
+
26
+ export { createEIP712Signer, walletAddress, KNOWN_TOKENS } from './signer.js';
27
+ export type { EIP712SignerOpts } from './signer.js';
28
+
29
+ export type {
30
+ X402Config,
31
+ X402ClientConfig,
32
+ X402ServerConfig,
33
+ X402Response,
34
+ X402PaymentRequirements,
35
+ X402PaymentPayload,
36
+ X402PaymentAuthorization,
37
+ } from './types.js';