diff --git a/eslint.config.mjs b/eslint.config.mjs index d47e0dd..d0c58d1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,7 +9,7 @@ export default [ }, eslint.configs.recommended, { - files: ["packages/*/src/**/*.ts"], + files: ["packages/*/src/**/*.ts", "samples/*/src/**/*.ts"], languageOptions: { parser: tsParser, ecmaVersion: 2020, diff --git a/package-lock.json b/package-lock.json index 264d096..2d13345 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,8 @@ "": { "name": "agent-sdk", "workspaces": [ - "packages/*" + "packages/*", + "samples/*" ], "devDependencies": { "@changesets/changelog-github": "^0.5.2", @@ -20,6 +21,10 @@ "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", "license": "MIT" }, + "node_modules/@agent-sdk/x402-browser-proxy": { + "resolved": "samples/x402-browser-proxy", + "link": true + }, "node_modules/@babel/runtime": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", @@ -1789,6 +1794,49 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@spruceid/siwe-parser": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@spruceid/siwe-parser/-/siwe-parser-2.1.2.tgz", + "integrity": "sha512-d/r3S1LwJyMaRAKQ0awmo9whfXeE88Qt00vRj91q5uv5ATtWIQEGJ67Yr5eSZw5zp1/fZCXZYuEckt8lSkereQ==", + "license": "Apache-2.0", + "dependencies": { + "@noble/hashes": "^1.1.2", + "apg-js": "^4.3.0", + "uri-js": "^4.4.1", + "valid-url": "^1.0.9" + } + }, + "node_modules/@stablelib/binary": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-1.0.1.tgz", + "integrity": "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==", + "license": "MIT", + "dependencies": { + "@stablelib/int": "^1.0.1" + } + }, + "node_modules/@stablelib/int": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz", + "integrity": "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==", + "license": "MIT" + }, + "node_modules/@stablelib/random": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@stablelib/random/-/random-1.0.2.tgz", + "integrity": "sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w==", + "license": "MIT", + "dependencies": { + "@stablelib/binary": "^1.0.1", + "@stablelib/wipe": "^1.0.1" + } + }, + "node_modules/@stablelib/wipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz", + "integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==", + "license": "MIT" + }, "node_modules/@toon-format/toon": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-2.1.0.tgz", @@ -2477,6 +2525,91 @@ "tslib": "1.14.1" } }, + "node_modules/@x402/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@x402/core/-/core-2.5.0.tgz", + "integrity": "sha512-nUr8HW8WhkU1DvrpUfsRvALy5NF8UWKoFezZOtX61mohxp2lWZpJ2GnvscxDM8nmBAbtIollmksd5z5pj8InXw==", + "license": "Apache-2.0", + "dependencies": { + "zod": "^3.24.2" + } + }, + "node_modules/@x402/core/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@x402/evm": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@x402/evm/-/evm-2.5.0.tgz", + "integrity": "sha512-MBSTQZwLobMVcmYO7itOMJRkxfHstsDyr7F94o9Rk/Oinz0kjvCe4DFgZmFXyz3nQUgQFmDVgTK5KIzfYR5uIA==", + "license": "Apache-2.0", + "dependencies": { + "@x402/core": "~2.5.0", + "@x402/extensions": "~2.5.0", + "viem": "^2.39.3", + "zod": "^3.24.2" + } + }, + "node_modules/@x402/evm/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@x402/extensions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@x402/extensions/-/extensions-2.5.0.tgz", + "integrity": "sha512-e7IQShbGUM/XQmzI8DQh2tX/k2XDUGI9DNF+ij2NHUyPEqAt5/mJCwOlaxS/60FWFdfFRfWjTsqaoS7Z8WLi+A==", + "license": "Apache-2.0", + "dependencies": { + "@scure/base": "^1.2.6", + "@x402/core": "~2.5.0", + "ajv": "^8.17.1", + "siwe": "^2.3.2", + "tweetnacl": "^1.0.3", + "viem": "^2.43.5", + "zod": "^3.24.2" + } + }, + "node_modules/@x402/extensions/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@x402/extensions/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@x402/extensions/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/abitype": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", @@ -2534,6 +2667,13 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT", + "peer": true + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2604,7 +2744,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2614,7 +2753,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2658,6 +2796,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/apg-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/apg-js/-/apg-js-4.4.0.tgz", + "integrity": "sha512-fefmXFknJmtgtNEXfPwZKYkMFX4Fyeyz+fNF6JWp87biGOPslJbCBVU158zvKRZfHBKnJDy8CMM40oLFGkXT8Q==", + "license": "BSD-2-Clause" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2685,6 +2829,12 @@ "node": ">=12" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -2954,11 +3104,24 @@ "node": ">=8" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2971,7 +3134,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/commander": { @@ -3255,6 +3417,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -3367,6 +3535,15 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -3633,6 +3810,114 @@ "node": ">= 0.6" } }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT", + "peer": true + }, + "node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD", + "peer": true + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT", + "peer": true + }, + "node_modules/ethers/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -3995,6 +4280,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4032,6 +4326,19 @@ "node": ">= 0.4" } }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4188,6 +4495,49 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-mitm-proxy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/http-mitm-proxy/-/http-mitm-proxy-1.1.0.tgz", + "integrity": "sha512-GyjWXiukopLzp6Yg4Dk1M+6Yfp5c/rPtqXDGaRopeJH1bd9F6k2cyJrB98kKmJaMU2P7kA5LLHbtuFc8HcNvrA==", + "license": "MIT", + "dependencies": { + "async": "^3.2.5", + "debug": "^4.3.4", + "mkdirp": "^1.0.4", + "node-forge": "^1.3.1", + "semaphore": "^1.1.0", + "uuid": "^9.0.1", + "ws": "^8.14.2", + "yargs": "^17.7.2" + }, + "bin": { + "http-mitm-proxy": "dist/bin/mitm-proxy.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/http-mitm-proxy/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/human-id": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.3.tgz", @@ -4331,6 +4681,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4737,6 +5096,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mlly": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", @@ -4846,6 +5217,15 @@ "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", "license": "MIT" }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-mock-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", @@ -5405,7 +5785,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5571,6 +5950,15 @@ "node": ">= 12.13.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -5590,6 +5978,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5713,6 +6111,14 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semaphore": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", + "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -5890,6 +6296,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/siwe": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/siwe/-/siwe-2.3.2.tgz", + "integrity": "sha512-aSf+6+Latyttbj5nMu6GF3doMfv2UYj83hhwZgUF20ky6fTS83uVhkQABdIVnEuS8y1bBdk7p6ltb9SmlhTTlA==", + "license": "Apache-2.0", + "dependencies": { + "@spruceid/siwe-parser": "^2.1.2", + "@stablelib/random": "^1.0.1", + "uri-js": "^4.4.1", + "valid-url": "^1.0.9" + }, + "peerDependencies": { + "ethers": "^5.6.8 || ^6.0.8" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -5985,11 +6406,24 @@ "dev": true, "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6304,6 +6738,26 @@ "node": ">=8" } }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/turbo": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.8.7.tgz", @@ -6406,6 +6860,12 @@ "win32" ] }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6622,12 +7082,29 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -6976,6 +7453,23 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7018,6 +7512,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yaml": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", @@ -7033,6 +7536,33 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -7066,7 +7596,7 @@ }, "packages/cli-sdk": { "name": "@walletconnect/cli-sdk", - "version": "0.5.0", + "version": "0.6.0", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@walletconnect/keyvaluestorage": "1.1.1", @@ -7092,7 +7622,7 @@ }, "packages/companion-wallet": { "name": "@walletconnect/companion-wallet", - "version": "0.5.0", + "version": "0.6.0", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@walletconnect/cli-sdk": "*", @@ -7114,7 +7644,7 @@ }, "packages/pay-cli": { "name": "@walletconnect/pay-cli", - "version": "0.5.0", + "version": "0.6.0", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@walletconnect/cli-sdk": "*", @@ -7135,7 +7665,7 @@ }, "packages/staking-cli": { "name": "@walletconnect/staking-cli", - "version": "0.5.0", + "version": "0.6.0", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@walletconnect/cli-sdk": "*", @@ -7193,6 +7723,55 @@ "typescript": "^5.5.0", "vitest": "^3.2.0" } + }, + "samples/x402-browser-proxy": { + "name": "@agent-sdk/x402-browser-proxy", + "version": "0.0.0", + "dependencies": { + "@walletconnect/cli-sdk": "*", + "@x402/core": "^2.2.0", + "@x402/evm": "^2.2.0", + "commander": "^12.1.0", + "http-mitm-proxy": "^1.1.0" + }, + "bin": { + "x402-browser-proxy": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsup": "^8.4.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "samples/x402-browser-proxy/node_modules/@types/node": { + "version": "22.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", + "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "samples/x402-browser-proxy/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "samples/x402-browser-proxy/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index eb535f1..0f6db81 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "turbo": "^2.8.0" }, "workspaces": [ - "packages/*" + "packages/*", + "samples/*" ] } diff --git a/samples/x402-browser-proxy/.gitignore b/samples/x402-browser-proxy/.gitignore new file mode 100644 index 0000000..c0fcab0 --- /dev/null +++ b/samples/x402-browser-proxy/.gitignore @@ -0,0 +1 @@ +.http-mitm-proxy/ diff --git a/samples/x402-browser-proxy/README.md b/samples/x402-browser-proxy/README.md new file mode 100644 index 0000000..19f3e80 --- /dev/null +++ b/samples/x402-browser-proxy/README.md @@ -0,0 +1,183 @@ +# x402 Browser Proxy + +A local MITM proxy that intercepts HTTP 402 (Payment Required) responses and automatically signs [x402](https://www.x402.org/) payments using a CWP (CLI Wallet Protocol) wallet provider. + +## How It Works + +``` +Any HTTP client → MITM proxy (port 8402) → origin server + ↓ (on 402) + parse x402 payment header + ↓ + CWP sign-typed-data → wallet provider (e.g., companion-wallet) + ↓ + retry request with payment signature + ↓ + return 200 to client +``` + +The proxy sits between any HTTP client and the internet. When a server responds with `402 Payment Required` and an x402 payment header, the proxy automatically: + +1. Parses the payment requirements (amount, network, asset) +2. Signs the payment via CWP (`wallet-companion sign-typed-data`) +3. Retries the request with the payment signature +4. Returns the paid response to the client + +No code changes needed in the client — just point it at the proxy. + +## Prerequisites + +- A CWP wallet provider installed and on PATH (e.g., `companion-wallet`) +- The wallet must have an EVM account with funds on the target network +- To install companion-wallet: `npm install -g @walletconnect/companion-wallet` (or `npm link` from `packages/companion-wallet`) + +## Quick Start + +```bash +# From the repo root +npm run build + +# Start the proxy (auto-discovers wallet-companion) +cd samples/x402-browser-proxy +npm run dev start +``` + +## Consuming the Proxy + +Any HTTP client can use the proxy. The key idea: route your traffic through `http://127.0.0.1:8402` and the proxy handles payments transparently. + +### curl + +```bash +# Direct request — gets 402 +curl https://x402.payai.network/api/base/paid-content +# → 402 Payment Required + +# Through proxy — gets 200 (proxy pays automatically) +curl -x http://127.0.0.1:8402 https://x402.payai.network/api/base/paid-content +# → 200 OK with paid content +``` + +### agent-browser + +```bash +export AGENT_BROWSER_PROXY="http://127.0.0.1:8402" +export AGENT_BROWSER_IGNORE_HTTPS_ERRORS=1 +agent-browser open "https://x402.payai.network/api/base/paid-content" +``` + +### Node.js / fetch + +```javascript +import { ProxyAgent } from 'undici'; + +const agent = new ProxyAgent('http://127.0.0.1:8402'); +const res = await fetch('https://x402.payai.network/api/base/paid-content', { + dispatcher: agent, +}); +// res.status === 200 — proxy handled the 402 + payment +``` + +### Environment variables (system-wide) + +```bash +export HTTP_PROXY="http://127.0.0.1:8402" +export HTTPS_PROXY="http://127.0.0.1:8402" +export NODE_TLS_REJECT_UNAUTHORIZED=0 # needed for HTTPS MITM + +# Now any tool that respects HTTP_PROXY will auto-pay x402 endpoints +curl https://x402.payai.network/api/base/paid-content +``` + +### Python / requests + +```python +import requests + +proxies = {"http": "http://127.0.0.1:8402", "https": "http://127.0.0.1:8402"} +r = requests.get("https://x402.payai.network/api/base/paid-content", + proxies=proxies, verify=False) +# r.status_code == 200 +``` + +## CLI Reference + +```bash +# Start proxy with auto-discovered CWP provider +npm run dev start + +# Start with a specific provider +npm run dev start -- --wallet companion + +# Start with session-constrained signing (companion-wallet spending limits) +npm run dev start -- --session + +# Start with custom options +npm run dev start -- --port 8402 --max-payment 100000 --host 127.0.0.1 + +# List available CWP providers +npm run dev list-wallets +``` + +### Options + +| Flag | Default | Description | +|------|---------|-------------| +| `-p, --port` | `8402` | Proxy listen port (env: `X402_PROXY_PORT`) | +| `-m, --max-payment` | `100000` | Max payment in smallest unit (env: `X402_MAX_PAYMENT`) | +| `--host` | `127.0.0.1` | Host to bind to | +| `-w, --wallet` | auto | CWP provider name | +| `-s, --session` | none | Companion-wallet session ID | + +## Testing + +### With the mock server + +```bash +# Terminal 1: Start the mock x402 server +node test-server.mjs + +# Terminal 2: Start the proxy +npm run dev start + +# Terminal 3: Run tests +node test-server.mjs --test +``` + +The test suite verifies: +1. Direct request to mock server returns 402 +2. Same request through proxy returns 200 (payment signed and accepted) +3. Same flow against the real PayAI endpoint (`x402.payai.network`) + +## How the CWP Bridge Works + +The core innovation is `cwp-signer.ts` — an adapter that implements the `ClientEvmSigner` interface (from `@x402/evm`) by delegating to `walletExec` from `@walletconnect/cli-sdk`: + +```typescript +// @x402/evm expects: { address, signTypedData({domain, types, primaryType, message}) } +// CWP provides: wallet-companion sign-typed-data < {account, typedData: {...}} + +function createCwpSigner({ path, account, sessionId }): ClientEvmSigner { + return { + address: account, + async signTypedData({ domain, types, primaryType, message }) { + const result = await walletExec(path, "sign-typed-data", { + account, + typedData: { domain, types, primaryType, message }, + ...(sessionId && { sessionId }), + }); + return result.signature; + }, + }; +} +``` + +This means any CWP-compatible wallet can sign x402 payments — not just `companion-wallet`. + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `X402_PROXY_PORT` | Default proxy port | +| `X402_MAX_PAYMENT` | Default max payment amount | +| `DEBUG` | Enable debug logging (verbose proxy + payment details) | diff --git a/samples/x402-browser-proxy/package.json b/samples/x402-browser-proxy/package.json new file mode 100644 index 0000000..6d5bc0c --- /dev/null +++ b/samples/x402-browser-proxy/package.json @@ -0,0 +1,32 @@ +{ + "name": "@agent-sdk/x402-browser-proxy", + "version": "0.0.0", + "private": true, + "description": "MITM proxy that intercepts HTTP 402 responses and auto-signs x402 payments via CWP", + "type": "module", + "main": "dist/index.js", + "bin": { + "x402-browser-proxy": "./dist/index.js" + }, + "scripts": { + "build": "tsup", + "dev": "tsx src/index.ts", + "lint": "eslint src/" + }, + "dependencies": { + "@walletconnect/cli-sdk": "*", + "@x402/evm": "^2.2.0", + "@x402/core": "^2.2.0", + "commander": "^12.1.0", + "http-mitm-proxy": "^1.1.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsup": "^8.4.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0" + }, + "engines": { + "node": ">=18" + } +} diff --git a/samples/x402-browser-proxy/src/cwp-signer.ts b/samples/x402-browser-proxy/src/cwp-signer.ts new file mode 100644 index 0000000..941533b --- /dev/null +++ b/samples/x402-browser-proxy/src/cwp-signer.ts @@ -0,0 +1,67 @@ +import { walletExec } from "@walletconnect/cli-sdk"; +import type { ClientEvmSigner } from "@x402/evm"; + +export interface CwpSignerOptions { + /** Full path to the wallet-* executable */ + path: string; + /** EVM account address */ + account: `0x${string}`; + /** Optional companion-wallet session ID for spending limits */ + sessionId?: string; +} + +/** + * Recursively converts BigInt values to strings so the object is JSON-serializable. + * walletExec uses JSON.stringify internally, which throws on BigInt. + */ +function bigintToString(obj: unknown): unknown { + if (typeof obj === "bigint") return obj.toString(); + if (Array.isArray(obj)) return obj.map(bigintToString); + if (obj !== null && typeof obj === "object") { + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + result[key] = bigintToString(value); + } + return result; + } + return obj; +} + +/** + * Creates a ClientEvmSigner that delegates signing to a CWP wallet provider. + * + * This bridges the x402 payment signing interface with the CLI Wallet Protocol, + * allowing any CWP-compatible wallet (e.g., companion-wallet) to sign x402 payments. + */ +export function createCwpSigner(options: CwpSignerOptions): ClientEvmSigner { + const { path: providerPath, account, sessionId } = options; + + return { + address: account, + + async signTypedData({ + domain, + types, + primaryType, + message, + }: { + domain: Record; + types: Record; + primaryType: string; + message: Record; + }): Promise<`0x${string}`> { + const result = (await walletExec( + providerPath, + "sign-typed-data", + { + account, + typedData: bigintToString({ domain, types, primaryType, message }) as Record, + ...(sessionId && { sessionId }), + }, + 30000, // 30s timeout for signing + )) as { signature: `0x${string}` }; + + return result.signature; + }, + }; +} diff --git a/samples/x402-browser-proxy/src/index.ts b/samples/x402-browser-proxy/src/index.ts new file mode 100644 index 0000000..98898a0 --- /dev/null +++ b/samples/x402-browser-proxy/src/index.ts @@ -0,0 +1,153 @@ +#!/usr/bin/env node + +import { program } from "commander"; +import { + discoverProviders, + getProvider, + getDefaultProvider, + walletExec, +} from "@walletconnect/cli-sdk"; +import { createCwpSigner } from "./cwp-signer.js"; +import { initializeClient } from "./x402-client.js"; +import { startProxy } from "./proxy.js"; +import { logger } from "./logger.js"; + +const DEFAULT_PORT = 8402; +const DEFAULT_MAX_PAYMENT = BigInt("100000"); // 0.1 USDC (6 decimals) + +function getDefaultPort(): number { + const envPort = process.env.X402_PROXY_PORT; + if (envPort) { + const parsed = parseInt(envPort, 10); + if (!isNaN(parsed)) return parsed; + } + return DEFAULT_PORT; +} + +function getDefaultMaxPayment(): bigint { + const envMax = process.env.X402_MAX_PAYMENT; + if (envMax) { + try { + return BigInt(envMax); + } catch { + logger.warn(`Invalid X402_MAX_PAYMENT value "${envMax}", using default`); + } + } + return DEFAULT_MAX_PAYMENT; +} + +program + .name("x402-browser-proxy") + .description( + "MITM proxy that intercepts HTTP 402 responses and auto-signs x402 payments via CWP", + ) + .version("0.0.0"); + +program + .command("start") + .description("Start the x402 payment proxy") + .option( + "-p, --port ", + "Proxy port", + String(getDefaultPort()), + ) + .option( + "-m, --max-payment ", + "Maximum payment in smallest unit", + getDefaultMaxPayment().toString(), + ) + .option("--host
", "Host to bind to", "127.0.0.1") + .option("-w, --wallet ", "CWP provider name (auto-discovers if omitted)") + .option("-s, --session ", "Companion-wallet session ID for spending limits") + .action(async (options) => { + const port = parseInt(options.port, 10) || getDefaultPort(); + const maxPayment = BigInt(options.maxPayment || getDefaultMaxPayment()); + const host = options.host || "127.0.0.1"; + + // Discover or select CWP provider + let provider; + if (options.wallet) { + provider = await getProvider(options.wallet); + if (!provider) { + logger.error(`CWP provider "${options.wallet}" not found`); + logger.info("Run 'x402-browser-proxy list-wallets' to see available providers"); + process.exit(1); + } + } else { + provider = await getDefaultProvider(); + if (!provider) { + logger.error("No CWP wallet providers found on PATH"); + logger.info( + "Install a wallet provider (e.g., companion-wallet) and ensure wallet- is on PATH", + ); + process.exit(1); + } + } + + logger.info(`Using CWP provider: ${provider.info?.name || provider.shortName} (${provider.binary})`); + + // Get the wallet account address via CWP "accounts" operation + let account: `0x${string}`; + try { + const result = (await walletExec(provider.path, "accounts", undefined, 5000)) as { + accounts: Array<{ chain: string; address: `0x${string}` }>; + }; + if (!result.accounts || result.accounts.length === 0) { + logger.error("No accounts found in wallet. Run 'wallet-companion generate' first."); + process.exit(1); + } + account = result.accounts[0].address; + } catch (err) { + logger.error(`Failed to get wallet accounts: ${err}`); + process.exit(1); + } + + logger.info(`Wallet address: ${account}`); + + // Create the CWP signer and initialize x402 client + const signer = createCwpSigner({ + path: provider.path, + account, + sessionId: options.session, + }); + + const { httpClient, walletAddress } = initializeClient(signer); + + if (options.session) { + logger.info(`Using session: ${options.session}`); + } + + startProxy({ port, maxPayment, host, httpClient, walletAddress }); + }); + +program + .command("list-wallets") + .description("List available CWP wallet providers") + .action(async () => { + const providers = await discoverProviders(); + + if (providers.length === 0) { + console.log("No CWP wallet providers found on PATH."); + console.log( + "\nInstall a provider (e.g., companion-wallet) and ensure wallet- is on PATH.", + ); + return; + } + + console.log("Available CWP wallet providers:\n"); + for (const p of providers) { + const status = p.info + ? `${p.info.name} v${p.info.version}` + : `(error: ${p.error})`; + const chains = p.info?.chains?.join(", ") || "unknown"; + const caps = p.info?.capabilities?.join(", ") || "unknown"; + console.log(` ${p.shortName}`); + console.log(` Binary: ${p.path}`); + console.log(` Status: ${status}`); + console.log(` Chains: ${chains}`); + console.log(` Capabilities: ${caps}`); + console.log(""); + } + }); + +program.parse(); diff --git a/samples/x402-browser-proxy/src/logger.ts b/samples/x402-browser-proxy/src/logger.ts new file mode 100644 index 0000000..bdb7824 --- /dev/null +++ b/samples/x402-browser-proxy/src/logger.ts @@ -0,0 +1,59 @@ +const COLORS = { + reset: "\x1b[0m", + dim: "\x1b[2m", + cyan: "\x1b[36m", + yellow: "\x1b[33m", + red: "\x1b[31m", + green: "\x1b[32m", + magenta: "\x1b[35m", +}; + +function timestamp(): string { + return new Date().toISOString().replace("T", " ").substring(0, 19); +} + +export const logger = { + info(message: string, ...args: unknown[]): void { + console.log( + `${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.cyan}INFO${COLORS.reset} ${message}`, + ...args, + ); + }, + + warn(message: string, ...args: unknown[]): void { + console.log( + `${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.yellow}WARN${COLORS.reset} ${message}`, + ...args, + ); + }, + + error(message: string, ...args: unknown[]): void { + console.error( + `${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.red}ERROR${COLORS.reset} ${message}`, + ...args, + ); + }, + + success(message: string, ...args: unknown[]): void { + console.log( + `${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.green}OK${COLORS.reset} ${message}`, + ...args, + ); + }, + + payment(message: string, ...args: unknown[]): void { + console.log( + `${COLORS.dim}[${timestamp()}]${COLORS.reset} ${COLORS.magenta}PAY${COLORS.reset} ${message}`, + ...args, + ); + }, + + debug(message: string, ...args: unknown[]): void { + if (process.env.DEBUG) { + console.log( + `${COLORS.dim}[${timestamp()}] DEBUG ${message}${COLORS.reset}`, + ...args, + ); + } + }, +}; diff --git a/samples/x402-browser-proxy/src/proxy.ts b/samples/x402-browser-proxy/src/proxy.ts new file mode 100644 index 0000000..103c2b9 --- /dev/null +++ b/samples/x402-browser-proxy/src/proxy.ts @@ -0,0 +1,221 @@ +import { Proxy } from "http-mitm-proxy"; +import type { x402HTTPClient } from "@x402/core/client"; +import * as http from "http"; +import * as https from "https"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { logger } from "./logger.js"; +import { parsePaymentHeader, signPayment } from "./x402-client.js"; +import type { PaymentRequirements } from "@x402/core/types"; + +const CERT_DIR = path.join(os.homedir(), ".x402-browser-proxy", "certs"); + +export interface ProxyOptions { + port: number; + maxPayment: bigint; + host?: string; + httpClient: x402HTTPClient; + walletAddress: string; +} + +export function startProxy(options: ProxyOptions): void { + const { port, maxPayment, host = "127.0.0.1", httpClient, walletAddress } = options; + + if (!fs.existsSync(CERT_DIR)) { + fs.mkdirSync(CERT_DIR, { recursive: true }); + logger.info(`Created certificate directory: ${CERT_DIR}`); + } + + const proxy = new Proxy(); + proxy.sslCaDir = CERT_DIR; + + proxy.onError((_ctx, err) => { + if (err) { + logger.error(`Proxy error: ${err.message}`); + } + }); + + proxy.onRequest((ctx, callback) => { + if (process.env.DEBUG) { + const { clientToProxyRequest: req } = ctx; + const url = `${ctx.isSSL ? "https" : "http"}://${req.headers.host}${req.url}`; + logger.debug(`Request: ${req.method} ${url}`); + } + callback(); + }); + + proxy.onResponse((ctx, callback) => { + const { + clientToProxyRequest: req, + serverToProxyResponse: serverRes, + proxyToClientResponse: clientRes, + } = ctx; + + if (!serverRes) { + callback(); + return; + } + + const statusCode = serverRes.statusCode || 0; + const url = `${ctx.isSSL ? "https" : "http"}://${req.headers.host}${req.url}`; + + if (statusCode === 402) { + logger.payment(`402 Payment Required: ${url}`); + + const paymentHeader = (serverRes.headers["x-payment"] || + serverRes.headers["payment-required"]) as string | undefined; + + if (!paymentHeader) { + logger.warn( + "No X-Payment or Payment-Required header found in 402 response", + ); + callback(); + return; + } + + const paymentRequired = parsePaymentHeader(paymentHeader); + if (!paymentRequired) { + logger.error("Failed to parse payment requirements"); + callback(); + return; + } + + const accepts = paymentRequired.accepts || []; + if (accepts.length > 0) { + const paymentReq = accepts[0] as PaymentRequirements; + logger.payment( + `Payment details: scheme=${paymentReq.scheme}, network=${paymentReq.network}, amount=${paymentReq.amount}`, + ); + } + + signPayment(httpClient, paymentRequired, maxPayment) + .then((paymentHeaders) => { + if (!paymentHeaders) { + logger.warn("Payment rejected or signing failed"); + callback(); + return; + } + + logger.payment(`Payment signed, retrying request...`); + + retryWithPayment(ctx, paymentHeaders, url) + .then((retryRes) => { + if (retryRes) { + const retryStatus = retryRes.statusCode ?? 0; + if (retryStatus === 402) { + logger.warn( + `Retry still got 402 - payment may have been rejected: ${url}`, + ); + } else if (retryStatus >= 200 && retryStatus < 300) { + logger.success( + `Paid request successful: ${retryStatus} ${url}`, + ); + } else { + logger.info( + `Retry response: ${retryStatus} ${url}`, + ); + } + clientRes.writeHead(retryStatus, retryRes.headers); + retryRes.pipe(clientRes); + } else { + callback(); + } + }) + .catch((err) => { + logger.error(`Retry failed: ${err.message}`); + callback(); + }); + }) + .catch((err) => { + logger.error(`Payment signing failed: ${err.message}`); + callback(); + }); + } else { + if (process.env.DEBUG) { + logger.debug(`Response: ${statusCode} ${url}`); + } + callback(); + } + }); + + proxy.listen({ port, host }, () => { + logger.info(`x402 proxy started on ${host}:${port}`); + logger.info(`Wallet address: ${walletAddress}`); + logger.info(`Max payment: ${maxPayment.toString()}`); + logger.info( + `CA certificate: ${path.join(CERT_DIR, "certs", "ca.pem")}`, + ); + console.log(""); + console.log("Usage with agent-browser:"); + console.log(` export AGENT_BROWSER_PROXY="http://${host}:${port}"`); + console.log(" export AGENT_BROWSER_IGNORE_HTTPS_ERRORS=1"); + console.log(""); + }); +} + +type ProxyResponseContext = Parameters[0]>[0]; + +async function retryWithPayment( + ctx: ProxyResponseContext, + paymentHeaders: Record, + url: string, +): Promise { + return new Promise((resolve, reject) => { + const { clientToProxyRequest: originalReq } = ctx; + const parsedUrl = new URL(url); + + const isHttps = parsedUrl.protocol === "https:"; + const requestModule = isHttps ? https : http; + + const headers: Record = { + ...originalReq.headers, + }; + + for (const [key, value] of Object.entries(paymentHeaders)) { + headers[key.toLowerCase()] = value; + } + + // Remove hop-by-hop headers + delete headers["proxy-connection"]; + delete headers["connection"]; + delete headers["keep-alive"]; + delete headers["transfer-encoding"]; + + const options: http.RequestOptions = { + hostname: parsedUrl.hostname, + port: parsedUrl.port || (isHttps ? 443 : 80), + path: parsedUrl.pathname + parsedUrl.search, + method: originalReq.method, + headers, + }; + + if (process.env.DEBUG) { + logger.debug(`Retry request headers: ${JSON.stringify(headers)}`); + } + + const retryReq = requestModule.request(options, (res) => { + const paymentResponse = + res.headers["payment-response"] || res.headers["x-payment-response"]; + if (paymentResponse && process.env.DEBUG) { + try { + const decoded = Buffer.from( + paymentResponse as string, + "base64", + ).toString("utf-8"); + logger.debug(`Payment response: ${decoded}`); + } catch { + logger.debug(`Payment response (raw): ${paymentResponse}`); + } + } + resolve(res); + }); + + retryReq.on("error", (err) => { + reject(err); + }); + + // Note: body is not forwarded on retry (x402 payments are typically GET requests) + retryReq.end(); + }); +} diff --git a/samples/x402-browser-proxy/src/x402-client.ts b/samples/x402-browser-proxy/src/x402-client.ts new file mode 100644 index 0000000..12751cd --- /dev/null +++ b/samples/x402-browser-proxy/src/x402-client.ts @@ -0,0 +1,109 @@ +import { ExactEvmScheme } from "@x402/evm"; +import type { ClientEvmSigner } from "@x402/evm"; +import { x402Client, x402HTTPClient } from "@x402/core/client"; +import type { + PaymentRequirements, + PaymentRequired, +} from "@x402/core/types"; +import { logger } from "./logger.js"; + +export interface X402ClientHandle { + httpClient: x402HTTPClient; + walletAddress: `0x${string}`; +} + +/** + * Initialize the x402 client with a CWP-backed signer. + */ +export function initializeClient(signer: ClientEvmSigner): X402ClientHandle { + const client = new x402Client(); + const evmScheme = new ExactEvmScheme(signer); + client.register("eip155:*", evmScheme); + + const httpClient = new x402HTTPClient(client); + + logger.info(`Initialized x402 client with wallet: ${signer.address}`); + + return { httpClient, walletAddress: signer.address }; +} + +export function parsePaymentHeader( + headerValue: string, +): PaymentRequired | null { + try { + const decoded = Buffer.from(headerValue, "base64").toString("utf-8"); + const paymentRequired = JSON.parse(decoded) as PaymentRequired; + + // If resource is missing but is in accepts[0].extra, copy it up + if ( + !paymentRequired.resource && + paymentRequired.accepts?.length > 0 + ) { + const firstAccept = paymentRequired.accepts[0] as Record; + const extra = firstAccept.extra as Record | undefined; + if (extra?.resource) { + paymentRequired.resource = { + url: extra.resource, + description: extra.description || "", + mimeType: extra.mimeType || "application/json", + }; + } + } + + return paymentRequired; + } catch (error) { + logger.error("Failed to parse payment header:", error); + return null; + } +} + +export async function signPayment( + httpClient: x402HTTPClient, + paymentRequired: PaymentRequired, + maxPayment: bigint, +): Promise | null> { + const accepts = paymentRequired.accepts || []; + if (accepts.length === 0) { + logger.error("No payment options available in requirements"); + return null; + } + + const paymentReq = accepts[0] as PaymentRequirements; + const amount = BigInt(paymentReq.amount); + + if (amount > maxPayment) { + logger.warn( + `Payment amount ${amount} exceeds max allowed ${maxPayment}. Rejecting.`, + ); + return null; + } + + const resourceUrl = + paymentRequired.resource?.url || + (paymentReq.extra as Record | undefined)?.resource || + "unknown"; + logger.payment( + `Signing payment: ${paymentReq.amount} on ${paymentReq.network} for ${resourceUrl}`, + ); + + try { + const paymentPayload = await httpClient.createPaymentPayload(paymentRequired); + + if (process.env.DEBUG) { + logger.debug( + `Payment payload: ${JSON.stringify(paymentPayload, (_k, v) => (typeof v === "bigint" ? v.toString() : v))}`, + ); + } + + const headers = httpClient.encodePaymentSignatureHeader(paymentPayload); + + if (process.env.DEBUG) { + logger.debug(`Payment headers: ${JSON.stringify(headers)}`); + } + + return headers; + } catch (error) { + logger.error("Failed to sign payment:", error); + return null; + } +} diff --git a/samples/x402-browser-proxy/test-server.mjs b/samples/x402-browser-proxy/test-server.mjs new file mode 100644 index 0000000..115a7f4 --- /dev/null +++ b/samples/x402-browser-proxy/test-server.mjs @@ -0,0 +1,294 @@ +// x402 proxy test script +// Tests a local mock 402 server and optionally a real x402 endpoint +// +// Usage: +// node test-server.mjs # Run local mock server only +// node test-server.mjs --test # Run full test suite against running proxy +// node test-server.mjs --test-real # Test only the real endpoint against running proxy + +import http from "http"; + +const PORT = 3402; +const PROXY_PORT = 8402; +const REAL_ENDPOINT = "https://x402.payai.network/api/base/paid-content"; + +// --- Test runner --- + +const args = process.argv.slice(2); +const testMode = args.includes("--test"); +const testRealOnly = args.includes("--test-real"); + +if (testMode || testRealOnly) { + runTests(testRealOnly).then((passed) => { + process.exit(passed ? 0 : 1); + }); +} else { + startMockServer(); +} + +async function runTests(realOnly) { + console.log("=== x402 Proxy Test Suite ===\n"); + + let allPassed = true; + + if (!realOnly) { + console.log("--- Test 1: Local mock endpoint (HTTP) ---"); + const mockResult = await testEndpoint( + `http://127.0.0.1:${PORT}/paid-content`, + false, + ); + allPassed = allPassed && mockResult; + } + + console.log( + `--- Test ${realOnly ? "1" : "2"}: Real endpoint (${REAL_ENDPOINT}) ---`, + ); + const realResult = await testEndpoint(REAL_ENDPOINT, true); + allPassed = allPassed && realResult; + + console.log("\n=== Results ==="); + console.log(allPassed ? "All tests passed" : "Some tests failed"); + return allPassed; +} + +function testEndpoint(url, isHttps) { + return new Promise((resolve) => { + console.log(` [1/2] Direct request (expect 402)...`); + directRequest(url, isHttps) + .then((directStatus) => { + if (directStatus === 402) { + console.log(` OK: Got 402 Payment Required`); + } else { + console.log(` FAIL: Expected 402, got ${directStatus}`); + resolve(false); + return; + } + + console.log(` [2/2] Request through proxy (expect 200)...`); + proxyRequest(url) + .then(({ status, body }) => { + if (status === 200) { + console.log(` OK: Got 200 OK`); + try { + const data = JSON.parse(body); + if (data.premiumContent) { + console.log(` Content: "${data.premiumContent}"`); + } else if (data.message) { + console.log(` Content: "${data.message}"`); + } + if (data.payer) { + console.log(` Payer: ${data.payer}`); + } + } catch { + /* not JSON, that's ok */ + } + resolve(true); + } else { + console.log(` FAIL: Expected 200, got ${status}`); + console.log(` Body: ${body.substring(0, 200)}`); + resolve(false); + } + }) + .catch((err) => { + console.log(` FAIL: Proxy request failed: ${err.message}`); + resolve(false); + }); + }) + .catch((err) => { + console.log(` FAIL: Direct request failed: ${err.message}`); + resolve(false); + }); + }); +} + +async function directRequest(url, isHttps) { + const mod = isHttps ? (await import("https")).default : http; + return new Promise((resolve, reject) => { + const req = mod.request(url, (res) => { + res.resume(); + resolve(res.statusCode); + }); + req.on("error", reject); + req.end(); + }); +} + +function proxyRequest(targetUrl) { + return new Promise((resolve, reject) => { + const parsed = new URL(targetUrl); + const isHttps = parsed.protocol === "https:"; + + if (isHttps) { + const connectReq = http.request({ + hostname: "127.0.0.1", + port: PROXY_PORT, + method: "CONNECT", + path: `${parsed.hostname}:${parsed.port || 443}`, + }); + + connectReq.on("connect", (_res, socket) => { + import("https").then(({ default: httpsModule }) => { + const req = httpsModule.request( + { + hostname: parsed.hostname, + path: parsed.pathname + parsed.search, + method: "GET", + socket: socket, + agent: false, + rejectUnauthorized: false, + }, + (res) => { + let body = ""; + res.on("data", (chunk) => (body += chunk)); + res.on("end", () => + resolve({ status: res.statusCode, body }), + ); + }, + ); + req.on("error", reject); + req.end(); + }); + }); + + connectReq.on("error", reject); + connectReq.end(); + } else { + const req = http.request( + { + hostname: "127.0.0.1", + port: PROXY_PORT, + path: targetUrl, + method: "GET", + headers: { Host: parsed.host }, + }, + (res) => { + let body = ""; + res.on("data", (chunk) => (body += chunk)); + res.on("end", () => resolve({ status: res.statusCode, body })); + }, + ); + req.on("error", reject); + req.end(); + } + }); +} + +// --- Mock server --- + +const paymentRequired = { + x402Version: 2, + accepts: [ + { + scheme: "exact", + network: "eip155:8453", + amount: "1000", // 0.001 USDC + payTo: "0x0000000000000000000000000000000000000001", + maxTimeoutSeconds: 300, + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base + extra: { + name: "USD Coin", + version: "2", + description: "Test x402 endpoint", + mimeType: "application/json", + resource: `http://localhost:${PORT}/paid-content`, + }, + }, + ], + resource: { + url: `http://localhost:${PORT}/paid-content`, + description: "Test x402 endpoint", + mimeType: "application/json", + }, +}; + +function startMockServer() { + const server = http.createServer((req, res) => { + console.log(`${new Date().toISOString()} ${req.method} ${req.url}`); + + const paymentSig = + req.headers["payment-signature"] || req.headers["x-payment"]; + + if (req.url === "/paid-content") { + if (paymentSig) { + console.log(" Payment signature received, accepting payment..."); + try { + const decoded = JSON.parse( + Buffer.from(paymentSig, "base64").toString("utf-8"), + ); + console.log( + ` Payer: ${decoded.payload?.authorization?.from}`, + ); + console.log(` Amount: ${decoded.accepted?.amount}`); + + res.writeHead(200, { + "Content-Type": "application/json", + "Payment-Response": Buffer.from( + JSON.stringify({ + success: true, + message: + "Payment accepted (test mode - no actual payment processed)", + }), + ).toString("base64"), + }); + res.end( + JSON.stringify( + { + message: + "Payment successful! This is the paid content.", + payer: decoded.payload?.authorization?.from, + amount: decoded.accepted?.amount, + note: "This is a test server - no actual payment was processed", + }, + null, + 2, + ), + ); + return; + } catch { + console.log(" Invalid payment signature format"); + } + } + + console.log(" No payment, returning 402..."); + const paymentHeader = Buffer.from( + JSON.stringify(paymentRequired), + ).toString("base64"); + res.writeHead(402, { + "Content-Type": "application/json", + "Payment-Required": paymentHeader, + }); + res.end(JSON.stringify(paymentRequired, null, 2)); + return; + } + + if (req.url === "/health") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "ok", x402: true })); + return; + } + + res.writeHead(404, { "Content-Type": "text/plain" }); + res.end("Not found. Try /paid-content or /health"); + }); + + server.listen(PORT, "127.0.0.1", () => { + console.log(`x402 test server running on http://127.0.0.1:${PORT}`); + console.log(""); + console.log("Endpoints:"); + console.log( + ` GET http://127.0.0.1:${PORT}/paid-content - Returns 402, then 200 with payment`, + ); + console.log( + ` GET http://127.0.0.1:${PORT}/health - Health check`, + ); + console.log(""); + console.log("Test commands (with proxy running on :8402):"); + console.log( + " node test-server.mjs --test # Full test suite (mock + real)", + ); + console.log( + " node test-server.mjs --test-real # Real endpoint only", + ); + console.log(""); + }); +} diff --git a/samples/x402-browser-proxy/tsconfig.json b/samples/x402-browser-proxy/tsconfig.json new file mode 100644 index 0000000..5a24989 --- /dev/null +++ b/samples/x402-browser-proxy/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/samples/x402-browser-proxy/tsup.config.ts b/samples/x402-browser-proxy/tsup.config.ts new file mode 100644 index 0000000..30dae56 --- /dev/null +++ b/samples/x402-browser-proxy/tsup.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + banner: { js: "#!/usr/bin/env node" }, + clean: true, +});