diff --git a/examples/x402-client.ts b/examples/x402-client.ts new file mode 100644 index 0000000..dbb99bc --- /dev/null +++ b/examples/x402-client.ts @@ -0,0 +1,97 @@ +/** + * x402 Client Example + * + * Demonstrates both the automatic request() flow and a manual + * step-by-step payment flow against an x402-protected server. + * + * Environment variables: + * WALLET_ADDRESS – Sender's Solana wallet address + * SERVER_URL – Base URL of the x402 server (default: http://localhost:3000) + */ +import { ShadowWireClient, X402Client } from '@radr/shadowwire'; + +const WALLET_ADDRESS = process.env.WALLET_ADDRESS || 'YOUR_WALLET_ADDRESS'; +const SERVER_URL = process.env.SERVER_URL || 'http://localhost:3000'; + +const wallet = { + signMessage: async (_message: Uint8Array) => { + // Replace with real wallet adapter (e.g. Phantom, Solflare) + throw new Error('Implement wallet signing'); + }, +}; + +const client = new ShadowWireClient(); +const x402 = new X402Client({ + client, + wallet, + senderWallet: WALLET_ADDRESS, + defaultToken: 'USDC', + defaultTransferType: 'external', + requestTimeoutMs: 20_000, +}); + +/** + * Automatic flow — X402Client detects 402, pays, and retries in one call. + */ +async function payForData() { + const result = await x402.request(`${SERVER_URL}/api/data`); + + if (result.success) { + console.log('Data:', result.data); + if (result.payment) { + console.log('Paid via:', result.payment.transfer.tx_signature); + console.log('Amount hidden:', result.payment.transfer.amount_hidden); + } + } else { + console.error('Request failed:', result.error); + } +} + +/** + * Manual flow — probe, parse requirements, pay, then retry with the header. + */ +async function manualPayFlow() { + // 1. Probe the endpoint + const probe = await fetch(`${SERVER_URL}/api/data`); + + if (probe.status !== 402) { + console.log('No payment required, status:', probe.status); + return; + } + + // 2. Parse payment requirements + const body = await probe.json(); + const requirements = X402Client.parseRequirements(body); + + if (!requirements) { + console.error('Could not parse 402 response'); + return; + } + + console.log('Payment options:', requirements.accepts.length); + const requirement = requirements.accepts[0]; + console.log(`Scheme: ${requirement.scheme}, Amount: ${requirement.amount}, Asset: ${requirement.asset}`); + + // 3. Execute payment + const payment = await x402.pay(requirement); + if (!payment.success || !payment.paymentHeader) { + console.error('Payment failed:', payment.error); + return; + } + + console.log('Payment tx:', payment.transfer?.tx_signature); + + // 4. Retry with X-Payment header + const response = await fetch(`${SERVER_URL}/api/data`, { + headers: { 'X-Payment': payment.paymentHeader }, + }); + + if (response.ok) { + console.log('Unlocked:', await response.json()); + } else { + console.error('Retry failed:', response.status); + } +} + +payForData().catch(console.error); +// manualPayFlow().catch(console.error); diff --git a/examples/x402-server.ts b/examples/x402-server.ts new file mode 100644 index 0000000..fcb3d42 --- /dev/null +++ b/examples/x402-server.ts @@ -0,0 +1,58 @@ +/** + * x402 Server Example + * + * Express server with a paywall-protected route, a free route, + * and a .well-known/x402 discovery endpoint. + * + * Environment variables: + * MERCHANT_WALLET – Solana wallet that receives payments + * FACILITATOR_URL – x402 facilitator endpoint (default: https://x402.kamiyo.ai) + * API_KEY – API key for the facilitator + * PORT – Server port (default: 3000) + */ +import { x402Paywall, createDiscoveryDocument } from '@radr/shadowwire'; + +const express = require('express'); +const app = express(); + +const MERCHANT_WALLET = process.env.MERCHANT_WALLET || 'YOUR_MERCHANT_WALLET'; +const FACILITATOR_URL = process.env.FACILITATOR_URL || 'https://x402.kamiyo.ai'; +const API_KEY = process.env.API_KEY || ''; +const PORT = Number(process.env.PORT) || 3000; + +// Free route — no payment needed +app.get('/api/status', (_req: any, res: any) => { + res.json({ status: 'ok', timestamp: Date.now() }); +}); + +// Paywall-protected route +app.get( + '/api/data', + x402Paywall({ + payTo: MERCHANT_WALLET, + amount: 0.01, + asset: 'USDC', + description: 'Premium data endpoint', + facilitatorUrl: FACILITATOR_URL, + apiKey: API_KEY, + onPayment: (info) => { + console.log(`Payment received from ${info.payer}: ${info.signature} (${info.amount} USDC)`); + }, + }), + (req: any, res: any) => { + res.json({ data: 'premium content', payment: req.x402 }); + } +); + +// Discovery endpoint +app.get('/.well-known/x402', (_req: any, res: any) => { + res.json( + createDiscoveryDocument('My API', MERCHANT_WALLET, [ + { path: '/api/data', method: 'GET', price: 0.01, description: 'Premium data' }, + ], { facilitatorUrl: FACILITATOR_URL }) + ); +}); + +app.listen(PORT, () => { + console.log(`x402 server listening on http://localhost:${PORT}`); +}); diff --git a/package-lock.json b/package-lock.json index 69dc70b..1765e24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@radr/shadowwire", - "version": "1.1.1", + "version": "1.1.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@radr/shadowwire", - "version": "1.1.1", + "version": "1.1.15", "license": "MIT", "dependencies": { "@solana/web3.js": "^1.95.3", @@ -16,7 +16,8 @@ "@types/bs58": "^4.0.1", "@types/node": "^22.5.5", "ts-node": "^10.9.2", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "vitest": "^3.0.0" } }, "node_modules/@babel/runtime": { @@ -41,6 +42,448 @@ "node": ">=12" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -96,6 +539,356 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@solana/buffer-layout": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", @@ -238,6 +1031,17 @@ "base-x": "^3.0.6" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -247,6 +1051,20 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", @@ -271,6 +1089,121 @@ "@types/node": "*" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -316,6 +1249,16 @@ "dev": true, "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/base-x": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", @@ -424,6 +1367,33 @@ "node": ">=6.14.2" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -436,6 +1406,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/commander": { "version": "14.0.2", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", @@ -452,6 +1432,34 @@ "dev": true, "license": "MIT" }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/delay": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", @@ -474,6 +1482,13 @@ "node": ">=0.3.1" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -489,12 +1504,74 @@ "es6-promise": "^4.0.3" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", @@ -509,6 +1586,39 @@ "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", "license": "MIT" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -585,12 +1695,36 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "license": "ISC" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -604,6 +1738,25 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -636,6 +1789,117 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, "node_modules/rpc-websockets": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.1.tgz", @@ -709,6 +1973,37 @@ ], "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stream-chain": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", @@ -724,6 +2019,19 @@ "stream-chain": "^2.2.5" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/superstruct": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", @@ -738,6 +2046,67 @@ "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -843,6 +2212,177 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -859,6 +2399,23 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", diff --git a/package.json b/package.json index 230ed52..1af00fb 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "license": "MIT", "scripts": { "build": "tsc && node -e \"require('fs').cpSync('wasm', 'dist/wasm', {recursive:true})\"", + "test": "vitest run", + "test:watch": "vitest", "prepublish": "npm run build", "prepare": "npm run build" }, @@ -37,6 +39,7 @@ "@types/node": "^22.5.5", "@types/bs58": "^4.0.1", "ts-node": "^10.9.2", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "vitest": "^3.0.0" } } diff --git a/src/errors.ts b/src/errors.ts index 3eebb8e..4559697 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -78,3 +78,27 @@ export class ProofGenerationError extends ShadowWireError { } } +export class X402InvalidSchemeError extends ShadowWireError { + constructor(scheme: string) { + super(`Unsupported x402 payment scheme: ${scheme}`); + this.name = 'X402InvalidSchemeError'; + Object.setPrototypeOf(this, X402InvalidSchemeError.prototype); + } +} + +export class X402HeaderTooLargeError extends ShadowWireError { + constructor(size?: number) { + super(size ? `Payment header exceeds size limit (${size} bytes)` : 'Payment header exceeds size limit'); + this.name = 'X402HeaderTooLargeError'; + Object.setPrototypeOf(this, X402HeaderTooLargeError.prototype); + } +} + +export class X402FacilitatorError extends ShadowWireError { + constructor(message: string = 'Facilitator request failed') { + super(message); + this.name = 'X402FacilitatorError'; + Object.setPrototypeOf(this, X402FacilitatorError.prototype); + } +} + diff --git a/src/index.ts b/src/index.ts index 6bd79f2..84e056c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,9 @@ export { NetworkError, WASMNotSupportedError, ProofGenerationError, + X402InvalidSchemeError, + X402HeaderTooLargeError, + X402FacilitatorError, } from './errors'; export { @@ -61,3 +64,23 @@ export type { } from './types'; export { TOKEN_FEES, TOKEN_MINIMUMS, TOKEN_MINTS, TOKEN_DECIMALS } from './constants'; + +export { + X402Client, + x402Paywall, + createPaymentRequired, + verifyPayment, + settlePayment, + createDiscoveryDocument, +} from './x402'; +export type { + X402PaymentRequirement, + X402Response, + X402PaymentResult, + X402RequestResult, + X402ClientConfig, + X402VerifyResult, + X402MiddlewareConfig, + X402PaymentProof, + X402DiscoveryResource, +} from './x402'; diff --git a/src/x402.test.ts b/src/x402.test.ts new file mode 100644 index 0000000..7f549bb --- /dev/null +++ b/src/x402.test.ts @@ -0,0 +1,477 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + X402Client, + x402Paywall, + createPaymentRequired, + verifyPayment, + settlePayment, + createDiscoveryDocument, + X402PaymentProof, + X402PaymentRequirement, + X402MiddlewareConfig, +} from './x402'; + +function mockResponse(status: number, body: unknown, ok?: boolean): Response { + return { + status, + statusText: status === 200 ? 'OK' : 'Error', + ok: ok ?? (status >= 200 && status < 300), + json: () => Promise.resolve(body), + headers: new Headers(), + } as unknown as Response; +} + +function validProof(): X402PaymentProof { + return { + x402Version: 2, + scheme: 'shadowwire', + network: 'solana:mainnet', + payload: { + signature: 'abc123sig', + amountHidden: true, + resource: '/api/data', + payTo: 'merchant_wallet', + sender: 'sender_wallet', + }, + }; +} + +function baseRequirement(overrides?: Partial): X402PaymentRequirement { + return { + scheme: 'shadowwire', + network: 'solana:mainnet', + amount: '10000', + asset: 'USDC', + payTo: 'merchant_wallet', + resource: '/api/data', + ...overrides, + }; +} + +function middlewareConfig(overrides?: Partial): X402MiddlewareConfig { + return { + payTo: 'merchant_wallet', + amount: 0.01, + asset: 'USDC', + description: 'Test endpoint', + facilitatorUrl: 'https://facilitator.test', + apiKey: 'test-key', + ...overrides, + }; +} + +function mockReq(headers: Record = {}, path = '/api/data') { + return { headers, path, url: path }; +} + +function mockRes() { + const res: any = { + _status: 0, + _body: null, + _headers: {} as Record, + status(code: number) { res._status = code; return res; }, + json(body: unknown) { res._body = body; return res; }, + setHeader(k: string, v: string) { res._headers[k] = v; }, + }; + return res; +} + +describe('encodePaymentHeader / decodePaymentHeader', () => { + it('round-trips a valid proof', () => { + const proof = validProof(); + const encoded = X402Client.encodePaymentHeader(proof); + const decoded = X402Client.decodePaymentHeader(encoded); + expect(decoded).toEqual(proof); + }); + + it('returns null for malformed base64', () => { + expect(X402Client.decodePaymentHeader('not!valid!base64!!!')).toBeNull(); + }); + + it('returns null for valid base64 but invalid JSON', () => { + const encoded = Buffer.from('not json', 'utf-8').toString('base64'); + expect(X402Client.decodePaymentHeader(encoded)).toBeNull(); + }); + + it('returns null when required fields are missing', () => { + const partial = { x402Version: 2, network: 'solana:mainnet' }; + const encoded = Buffer.from(JSON.stringify(partial), 'utf-8').toString('base64'); + expect(X402Client.decodePaymentHeader(encoded)).toBeNull(); + }); + + it('returns null for wrong x402Version', () => { + const wrongVersion = { ...validProof(), x402Version: 1 }; + const encoded = Buffer.from(JSON.stringify(wrongVersion), 'utf-8').toString('base64'); + expect(X402Client.decodePaymentHeader(encoded)).toBeNull(); + }); + + it('returns null for oversized header', () => { + const huge = Buffer.from('x'.repeat(20_000), 'utf-8').toString('base64'); + expect(X402Client.decodePaymentHeader(huge)).toBeNull(); + }); +}); + +describe('X402Client.request()', () => { + let client: X402Client; + let mockTransfer: ReturnType; + + beforeEach(() => { + mockTransfer = vi.fn(); + const shadowClient = { transfer: mockTransfer, getBalance: vi.fn(), calculateFee: vi.fn() } as any; + client = new X402Client({ + client: shadowClient, + wallet: { signMessage: vi.fn() } as any, + senderWallet: 'sender_wallet', + }); + }); + + it('passes through non-402 successful responses', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(200, { ok: true }))); + const result = await client.request('https://api.test/data'); + expect(result.success).toBe(true); + expect(result.data).toEqual({ ok: true }); + expect(result.statusCode).toBe(200); + vi.unstubAllGlobals(); + }); + + it('returns error for non-402, non-ok responses', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(500, null, false))); + const result = await client.request('https://api.test/data'); + expect(result.success).toBe(false); + expect(result.statusCode).toBe(500); + vi.unstubAllGlobals(); + }); + + it('handles 402 → pay → retry flow', async () => { + const x402Body = { + x402Version: 2, + accepts: [baseRequirement()], + }; + + mockTransfer.mockResolvedValue({ + success: true, + tx_signature: 'sig123', + amount_hidden: true, + }); + + const fetchMock = vi.fn() + .mockResolvedValueOnce(mockResponse(402, x402Body, false)) + .mockResolvedValueOnce(mockResponse(200, { premium: true })); + + vi.stubGlobal('fetch', fetchMock); + const result = await client.request('https://api.test/data'); + expect(result.success).toBe(true); + expect(result.data).toEqual({ premium: true }); + expect(result.payment?.transfer.tx_signature).toBe('sig123'); + expect(fetchMock).toHaveBeenCalledTimes(2); + vi.unstubAllGlobals(); + }); + + it('returns error when no compatible scheme is found', async () => { + const x402Body = { + x402Version: 2, + accepts: [baseRequirement({ scheme: 'stripe' })], + }; + + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(402, x402Body, false))); + const result = await client.request('https://api.test/data'); + expect(result.success).toBe(false); + expect(result.error).toContain('No compatible'); + vi.unstubAllGlobals(); + }); + + it('returns error when transfer fails', async () => { + const x402Body = { + x402Version: 2, + accepts: [baseRequirement()], + }; + + mockTransfer.mockResolvedValue({ success: false }); + + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(402, x402Body, false))); + const result = await client.request('https://api.test/data'); + expect(result.success).toBe(false); + expect(result.error).toContain('transfer failed'); + vi.unstubAllGlobals(); + }); + + it('returns error for 402 with empty accepts array', async () => { + const x402Body = { x402Version: 2, accepts: [] }; + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(402, x402Body, false))); + const result = await client.request('https://api.test/data'); + expect(result.success).toBe(false); + expect(result.error).toContain('No accepted payment'); + vi.unstubAllGlobals(); + }); +}); + +describe('X402Client.pay()', () => { + let client: X402Client; + let mockTransfer: ReturnType; + + beforeEach(() => { + mockTransfer = vi.fn(); + const shadowClient = { transfer: mockTransfer, getBalance: vi.fn(), calculateFee: vi.fn() } as any; + client = new X402Client({ + client: shadowClient, + wallet: { signMessage: vi.fn() } as any, + senderWallet: 'sender_wallet', + }); + }); + + it('rejects unsupported payment schemes', async () => { + const result = await client.pay(baseRequirement({ scheme: 'stripe' })); + expect(result.success).toBe(false); + expect(result.error).toContain('Unsupported x402 payment scheme'); + expect(mockTransfer).not.toHaveBeenCalled(); + }); + + it('rejects invalid amount', async () => { + const result = await client.pay(baseRequirement({ amount: '0' })); + expect(result.success).toBe(false); + expect(result.error).toContain('Invalid payment amount'); + expect(mockTransfer).not.toHaveBeenCalled(); + }); + + it('catches transfer exceptions', async () => { + mockTransfer.mockRejectedValue(new Error('rpc down')); + const result = await client.pay(baseRequirement({ amount: '1000000' })); + expect(result.success).toBe(false); + expect(result.error).toContain('rpc down'); + }); +}); + +describe('X402Client static helpers', () => { + it('parseRequirements returns null for non-objects', () => { + expect(X402Client.parseRequirements(null)).toBeNull(); + expect(X402Client.parseRequirements('string')).toBeNull(); + expect(X402Client.parseRequirements(42)).toBeNull(); + }); + + it('parseRequirements returns null when accepts is not an array', () => { + expect(X402Client.parseRequirements({ accepts: 'not-array' })).toBeNull(); + expect(X402Client.parseRequirements({ x402Version: 2 })).toBeNull(); + }); + + it('is402 returns true only for 402 with valid body', () => { + const validBody = { x402Version: 2, accepts: [baseRequirement()] }; + expect(X402Client.is402(402, validBody)).toBe(true); + expect(X402Client.is402(200, validBody)).toBe(false); + expect(X402Client.is402(402, {})).toBe(false); + }); +}); + +describe('x402Paywall middleware', () => { + it('returns 402 when no payment header is present', async () => { + const mw = x402Paywall(middlewareConfig()); + const req = mockReq(); + const res = mockRes(); + const next = vi.fn(); + + await mw(req, res, next); + expect(res._status).toBe(402); + expect(res._body.accepts).toBeDefined(); + expect(res._body.accepts[0].scheme).toBe('shadowwire'); + expect(next).not.toHaveBeenCalled(); + }); + + it('returns 400 for oversized payment header', async () => { + const mw = x402Paywall(middlewareConfig()); + const hugeHeader = 'x'.repeat(20_000); + const req = mockReq({ 'x-payment': hugeHeader }); + const res = mockRes(); + const next = vi.fn(); + + await mw(req, res, next); + expect(res._status).toBe(400); + expect(res._body.error).toContain('exceeds size limit'); + expect(next).not.toHaveBeenCalled(); + }); + + it('returns 402 for resource mismatch in proof', async () => { + const mismatchProof = validProof(); + mismatchProof.payload.resource = '/api/other'; + const header = X402Client.encodePaymentHeader(mismatchProof); + const mw = x402Paywall(middlewareConfig()); + const req = mockReq({ 'x-payment': header }, '/api/data'); + const res = mockRes(); + const next = vi.fn(); + + await mw(req, res, next); + expect(res._status).toBe(402); + expect(res._body.verifyError).toContain('resource mismatch'); + expect(next).not.toHaveBeenCalled(); + }); + + it('returns 402 for invalid/unsupported scheme in proof', async () => { + const badProof = { ...validProof(), scheme: 'stripe' }; + const header = X402Client.encodePaymentHeader(badProof as X402PaymentProof); + const mw = x402Paywall(middlewareConfig()); + const req = mockReq({ 'x-payment': header }); + const res = mockRes(); + const next = vi.fn(); + + await mw(req, res, next); + expect(res._status).toBe(402); + expect(next).not.toHaveBeenCalled(); + }); + + it('calls next() on valid payment verification and settlement', async () => { + const header = X402Client.encodePaymentHeader(validProof()); + const mw = x402Paywall(middlewareConfig()); + const req = mockReq({ 'x-payment': header }); + const res = mockRes(); + const next = vi.fn(); + + vi.stubGlobal('fetch', vi.fn() + .mockResolvedValueOnce(mockResponse(200, { valid: true, payer: 'sender_wallet', amount: '10000', resource: '/api/data' })) + .mockResolvedValueOnce(mockResponse(200, { success: true, txHash: 'tx123', amount: 0.01, fee: 0.001, net: 0.009, network: 'solana:mainnet' }))); + + await mw(req, res, next); + expect(next).toHaveBeenCalled(); + expect(req.x402).toBeDefined(); + expect(req.x402.txHash).toBe('tx123'); + vi.unstubAllGlobals(); + }); + + it('calls next() even if onPayment callback throws', async () => { + const header = X402Client.encodePaymentHeader(validProof()); + const mw = x402Paywall(middlewareConfig({ + onPayment: () => { throw new Error('callback boom'); }, + })); + const req = mockReq({ 'x-payment': header }); + const res = mockRes(); + const next = vi.fn(); + + vi.stubGlobal('fetch', vi.fn() + .mockResolvedValueOnce(mockResponse(200, { valid: true, payer: 'sender_wallet', amount: '10000', resource: '/api/data' })) + .mockResolvedValueOnce(mockResponse(200, { success: true, txHash: 'tx123', amount: 0.01, fee: 0.001, net: 0.009, network: 'solana:mainnet' }))); + + await mw(req, res, next); + expect(next).toHaveBeenCalled(); + vi.unstubAllGlobals(); + }); +}); + +describe('createPaymentRequired', () => { + it('returns correct shape with default values', () => { + const body = createPaymentRequired('/api/data', middlewareConfig()); + expect(body.x402Version).toBe(2); + expect(body.accepts).toHaveLength(1); + expect(body.accepts[0].scheme).toBe('shadowwire'); + expect(body.accepts[0].asset).toBe('USDC'); + expect(body.accepts[0].payTo).toBe('merchant_wallet'); + expect(body.facilitator).toBe('https://facilitator.test'); + }); + + it('includes additional schemes when provided', () => { + const extra: X402PaymentRequirement = baseRequirement({ scheme: 'lightning', network: 'bitcoin' }); + const body = createPaymentRequired('/api/data', middlewareConfig({ additionalSchemes: [extra] })); + expect(body.accepts).toHaveLength(2); + expect(body.accepts[1].scheme).toBe('lightning'); + }); +}); + +describe('verifyPayment', () => { + it('returns valid result on facilitator success', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue( + mockResponse(200, { valid: true, payer: 'sender', amount: '10000', resource: '/api/data' }) + )); + + const result = await verifyPayment('header', baseRequirement(), 'https://facilitator.test', 'key'); + expect(result.valid).toBe(true); + expect(result.payer).toBe('sender'); + vi.unstubAllGlobals(); + }); + + it('returns invalid on facilitator rejection', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue( + mockResponse(403, { error: 'Invalid signature' }, false) + )); + + const result = await verifyPayment('header', baseRequirement(), 'https://facilitator.test'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid signature'); + vi.unstubAllGlobals(); + }); + + it('returns invalid on network failure', async () => { + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED'))); + + const result = await verifyPayment('header', baseRequirement(), 'https://facilitator.test'); + expect(result.valid).toBe(false); + expect(result.error).toContain('ECONNREFUSED'); + vi.unstubAllGlobals(); + }); + + it('short-circuits on oversized header', async () => { + const fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + + const result = await verifyPayment('x'.repeat(20_000), baseRequirement(), 'https://facilitator.test'); + expect(result.valid).toBe(false); + expect(result.error).toContain('exceeds size limit'); + expect(fetchMock).not.toHaveBeenCalled(); + vi.unstubAllGlobals(); + }); +}); + +describe('settlePayment', () => { + it('returns success on facilitator settlement', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue( + mockResponse(200, { success: true, txHash: 'tx456', amount: 0.01, fee: 0.001, net: 0.009, network: 'solana:mainnet' }) + )); + + const result = await settlePayment('header', baseRequirement(), 'https://facilitator.test', 'key'); + expect(result.success).toBe(true); + expect(result.txHash).toBe('tx456'); + vi.unstubAllGlobals(); + }); + + it('returns failure on facilitator rejection', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue( + mockResponse(400, { error: 'Already settled' }, false) + )); + + const result = await settlePayment('header', baseRequirement(), 'https://facilitator.test'); + expect(result.success).toBe(false); + expect(result.error).toContain('Already settled'); + vi.unstubAllGlobals(); + }); + + it('returns failure on network error', async () => { + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('timeout'))); + + const result = await settlePayment('header', baseRequirement(), 'https://facilitator.test'); + expect(result.success).toBe(false); + expect(result.error).toContain('timeout'); + vi.unstubAllGlobals(); + }); +}); + +describe('createDiscoveryDocument', () => { + it('returns correct shape with defaults', () => { + const doc = createDiscoveryDocument('My API', 'merchant_wallet', [ + { path: '/api/data', method: 'GET', price: 0.01, description: 'Premium data' }, + ]); + + expect(doc.version).toBe('2.0'); + expect(doc.name).toBe('My API'); + expect(doc.payTo).toBe('merchant_wallet'); + expect(doc.schemes).toEqual(['shadowwire']); + expect(doc.networks).toEqual(['solana:mainnet']); + expect((doc.resources as any[])).toHaveLength(1); + expect((doc.resources as any[])[0].asset).toBe('USDC'); + expect((doc.capabilities as any).privatePayments).toBe(true); + }); + + it('includes facilitatorUrl from options', () => { + const doc = createDiscoveryDocument('API', 'wallet', [], { + facilitatorUrl: 'https://facilitator.test', + description: 'Test API', + }); + + expect(doc.facilitator).toBe('https://facilitator.test'); + expect(doc.description).toBe('Test API'); + }); +}); diff --git a/src/x402.ts b/src/x402.ts new file mode 100644 index 0000000..5446115 --- /dev/null +++ b/src/x402.ts @@ -0,0 +1,595 @@ +import { ShadowWireClient } from './client'; +import { TokenSymbol, WalletAdapter, TransferResponse, PoolBalance } from './types'; +import { NetworkError, X402InvalidSchemeError, X402HeaderTooLargeError, X402FacilitatorError } from './errors'; + +const DEFAULT_TIMEOUT_MS = 15_000; +const MAX_PAYMENT_HEADER_BYTES = 16_384; +const PAYMENT_HEADER_NAME = 'X-Payment'; +const DEFAULT_FACILITATOR_URL = 'https://x402.kamiyo.ai'; + +/** Describes a single payment option a server will accept for a 402-protected resource. */ +export interface X402PaymentRequirement { + scheme: string; + network: string; + amount: string; + asset: string; + payTo: string; + resource: string; + description?: string; + maxTimeoutSeconds?: number; + extra?: Record; +} + +export interface X402Response { + x402Version: number; + accepts: X402PaymentRequirement[]; + error?: string; + facilitator?: string; + resource?: { + url: string; + description?: string; + mimeType?: string; + }; + [key: string]: unknown; +} + +export interface X402PaymentResult { + success: boolean; + transfer?: TransferResponse; + paymentHeader?: string; + error?: string; +} + +export interface X402RequestResult { + success: boolean; + data?: T; + payment?: { + transfer: TransferResponse; + requirement: X402PaymentRequirement; + }; + error?: string; + statusCode: number; +} + +/** Configuration for creating an X402Client instance. */ +export interface X402ClientConfig { + client: ShadowWireClient; + wallet: WalletAdapter; + senderWallet: string; + defaultToken?: TokenSymbol; + defaultTransferType?: 'internal' | 'external'; + maxRetries?: number; + headers?: Record; + requestTimeoutMs?: number; +} + +export interface X402VerifyResult { + valid: boolean; + payer?: string; + amount?: string; + resource?: string; + balance?: number; + sufficient?: boolean; + error?: string; +} + +/** Configuration for the Express x402Paywall middleware. */ +export interface X402MiddlewareConfig { + payTo: string; + amount: number; + asset?: TokenSymbol; + description?: string; + maxTimeoutSeconds?: number; + facilitatorUrl?: string; + apiKey: string; + additionalSchemes?: X402PaymentRequirement[]; + onPayment?: (info: { payer: string; amount: number; signature: string; resource: string }) => void; +} + +export interface X402PaymentProof { + x402Version: number; + scheme: string; + network: string; + payload: { + signature: string; + amountHidden: boolean; + resource: string; + payTo: string; + sender?: string; + }; +} + +function toBase64(input: string): string { + if (typeof Buffer !== 'undefined') return Buffer.from(input, 'utf-8').toString('base64'); + const bytes = new TextEncoder().encode(input); + let bin = ''; + bytes.forEach((b) => (bin += String.fromCharCode(b))); + return btoa(bin); +} + +function fromBase64(encoded: string): string { + if (typeof Buffer !== 'undefined') return Buffer.from(encoded, 'base64').toString('utf-8'); + const bin = atob(encoded); + const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0)); + return new TextDecoder().decode(bytes); +} + +function byteLength(str: string): number { + if (typeof Buffer !== 'undefined') return Buffer.byteLength(str, 'utf-8'); + return new TextEncoder().encode(str).length; +} + +function isShadowwire(scheme: string): boolean { + return scheme === 'shadowwire' || scheme === 'shadow'; +} + +function sleep(ms: number) { + return new Promise((r) => setTimeout(r, ms)); +} + +/** HTTP client that automatically handles x402 payment flows via ShadowWire transfers. */ +export class X402Client { + private client: ShadowWireClient; + private wallet: WalletAdapter; + private senderWallet: string; + private defaultToken: TokenSymbol; + private defaultTransferType: 'internal' | 'external'; + private maxRetries: number; + private headers: Record; + private timeoutMs: number; + + /** Creates a new X402 client bound to a ShadowWire instance and wallet. */ + constructor(config: X402ClientConfig) { + this.client = config.client; + this.wallet = config.wallet; + this.senderWallet = config.senderWallet; + this.defaultToken = config.defaultToken || 'USDC'; + this.defaultTransferType = config.defaultTransferType || 'external'; + this.maxRetries = config.maxRetries ?? 1; + this.headers = config.headers || {}; + this.timeoutMs = config.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS; + } + + /** Fetches a URL, automatically paying the 402 requirement if one is returned. */ + async request(url: string, options?: RequestInit): Promise> { + const mergedHeaders: Record = { + ...this.headers, + ...((options?.headers as Record) || {}), + }; + + const initial = await this.doFetch(url, { ...options, headers: mergedHeaders }); + + if (initial.status !== 402) { + if (!initial.ok) { + return { success: false, error: `HTTP ${initial.status}: ${initial.statusText}`, statusCode: initial.status }; + } + const parsed = await safeParseBody(initial); + return { success: true, data: parsed, statusCode: initial.status }; + } + + const x402Body = await safeParseBody(initial); + if (!x402Body?.accepts?.length) { + return { success: false, error: 'No accepted payment methods in 402 response', statusCode: 402 }; + } + + const requirement = this.findCompatibleRequirement(x402Body.accepts); + if (!requirement) { + return { success: false, error: 'No compatible payment option (need ShadowWire)', statusCode: 402 }; + } + + const payResult = await this.pay(requirement); + if (!payResult.success || !payResult.transfer || !payResult.paymentHeader) { + return { success: false, error: payResult.error || 'Payment failed', statusCode: 402 }; + } + + for (let attempt = 0; attempt <= this.maxRetries; attempt++) { + const res = await this.doFetch(url, { + ...options, + headers: { ...mergedHeaders, [PAYMENT_HEADER_NAME]: payResult.paymentHeader }, + }); + + if (res.ok) { + const parsed = await safeParseBody(res); + return { success: true, data: parsed, payment: { transfer: payResult.transfer, requirement }, statusCode: res.status }; + } + + if (res.status === 402) { + return { success: false, error: 'Payment not accepted by server', statusCode: 402 }; + } + + if (attempt < this.maxRetries && res.status >= 500) { + await sleep(Math.min(250 * 2 ** attempt, 1000)); + continue; + } + + return { success: false, error: `HTTP ${res.status} after payment`, statusCode: res.status }; + } + + return { success: false, error: 'Unexpected error', statusCode: 500 }; + } + + /** Executes a ShadowWire transfer to fulfil a single payment requirement. */ + async pay(requirement: X402PaymentRequirement): Promise { + if (!isShadowwire(requirement.scheme)) return { success: false, error: new X402InvalidSchemeError(requirement.scheme).message }; + + const amount = this.parseAmount(requirement.amount, requirement.asset); + if (amount <= 0) return { success: false, error: 'Invalid payment amount' }; + + try { + const transfer = await this.client.transfer({ + sender: this.senderWallet, + recipient: requirement.payTo, + amount, + token: this.resolveToken(requirement.asset), + type: this.defaultTransferType, + wallet: this.wallet, + }); + + if (!transfer.success) return { success: false, error: 'ShadowWire transfer failed' }; + + const paymentHeader = X402Client.encodePaymentHeader({ + x402Version: 2, + scheme: 'shadowwire', + network: 'solana:mainnet', + payload: { + signature: transfer.tx_signature, + amountHidden: transfer.amount_hidden, + resource: requirement.resource, + payTo: requirement.payTo, + sender: this.senderWallet, + }, + }); + + return { success: true, transfer, paymentHeader }; + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown error'; + return { success: false, error: message }; + } + } + + async getBalance(token?: TokenSymbol): Promise { + return this.client.getBalance(this.senderWallet, token || this.defaultToken); + } + + estimateFee(amount: number, token?: TokenSymbol): { fee: number; feePercentage: number; netAmount: number } { + return this.client.calculateFee(amount, token || this.defaultToken); + } + + /** Parses a 402 response body into structured payment requirements, or null if invalid. */ + static parseRequirements(body: unknown): X402Response | null { + if (!body || typeof body !== 'object') return null; + const obj = body as Record; + if (!Array.isArray(obj.accepts)) return null; + return obj as unknown as X402Response; + } + + /** Returns true if the status is 402 and the body contains valid payment requirements. */ + static is402(status: number, body: unknown): boolean { + return status === 402 && X402Client.parseRequirements(body) !== null; + } + + /** Encodes a payment proof into a base64 string suitable for the X-Payment header. */ + static encodePaymentHeader(proof: X402PaymentProof): string { + return toBase64(JSON.stringify(proof)); + } + + /** Decodes a base64 X-Payment header into a payment proof, or null if malformed. */ + static decodePaymentHeader(header: string): X402PaymentProof | null { + try { + if (byteLength(header) > MAX_PAYMENT_HEADER_BYTES) return null; + const decoded = JSON.parse(fromBase64(header)); + if (decoded.x402Version !== 2) return null; + if (!decoded.scheme || !decoded.payload?.signature) return null; + return decoded as X402PaymentProof; + } catch { + return null; + } + } + + private findCompatibleRequirement(accepts: X402PaymentRequirement[]): X402PaymentRequirement | null { + return accepts.find((r) => isShadowwire(r.scheme)) || null; + } + + private parseAmount(amountStr: string, asset: string): number { + const raw = parseInt(amountStr, 10); + if (isNaN(raw) || raw <= 0) return 0; + const token = this.resolveToken(asset); + try { + const { TokenUtils } = require('./tokens'); + return TokenUtils.fromSmallestUnit(raw, token); + } catch { + return raw / 1_000_000; + } + } + + private resolveToken(asset: string): TokenSymbol { + const upper = asset?.toUpperCase?.() || ''; + try { + const { TokenUtils } = require('./tokens'); + if (upper && TokenUtils.isValidToken(upper)) return upper as TokenSymbol; + } catch {} + return this.defaultToken; + } + + private async doFetch(url: string, options?: RequestInit): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), this.timeoutMs); + try { + return await (globalThis as any).fetch(url, { ...options, signal: controller.signal }); + } catch (err: any) { + if (err && (err.name === 'AbortError')) { + throw new NetworkError(`x402 request timed out after ${this.timeoutMs}ms`); + } + throw new NetworkError(err instanceof Error ? `x402 request failed: ${err.message}` : 'x402 request failed'); + } finally { + clearTimeout(timer); + } + } +} + +async function safeParseBody(response: Response): Promise { + try { + return (await response.json()) as T; + } catch { + return undefined; + } +} + +async function timedFetch(url: string, init: RequestInit, timeoutMs: number = DEFAULT_TIMEOUT_MS): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + return await (globalThis as any).fetch(url, { ...init, signal: controller.signal }); + } finally { + clearTimeout(timer); + } +} + +/** Builds a 402 response body for the given resource and middleware config. */ +export function createPaymentRequired(resource: string, config: X402MiddlewareConfig): X402Response { + const amount = Math.floor(config.amount * 1_000_000).toString(); + + const accepts: X402PaymentRequirement[] = [ + { + scheme: 'shadowwire', + network: 'solana:mainnet', + amount, + asset: config.asset || 'USDC', + payTo: config.payTo, + resource, + description: config.description, + maxTimeoutSeconds: config.maxTimeoutSeconds || 60, + extra: { transferTypes: ['internal', 'external'], amountHidden: true }, + }, + ]; + + if (config.additionalSchemes) accepts.push(...config.additionalSchemes); + + return { + x402Version: 2, + accepts, + error: 'Payment Required', + facilitator: config.facilitatorUrl || DEFAULT_FACILITATOR_URL, + resource: { url: resource, description: config.description, mimeType: 'application/json' }, + }; +} + +/** Sends a payment proof to the facilitator for verification. */ +export async function verifyPayment( + paymentHeader: string, + requirement: X402PaymentRequirement, + facilitatorUrl: string, + apiKey?: string +): Promise { + if (byteLength(paymentHeader) > MAX_PAYMENT_HEADER_BYTES) return { valid: false, error: new X402HeaderTooLargeError(byteLength(paymentHeader)).message }; + + try { + const headers: Record = { 'Content-Type': 'application/json' }; + if (apiKey) headers['X-API-Key'] = apiKey; + + const verifyUrl = new URL('/verify', facilitatorUrl).toString(); + const res = await timedFetch(verifyUrl, { + method: 'POST', + headers, + body: JSON.stringify({ + paymentHeader, + resource: requirement.resource, + maxAmount: requirement.amount ? parseInt(requirement.amount, 10) / 1_000_000 : undefined, + }), + }); + + if (!res.ok) { + const errBody = await safeParseBody<{ error?: string }>(res); + return { valid: false, error: new X402FacilitatorError(errBody?.error || `Facilitator returned ${res.status}`).message }; + } + + const data = (await safeParseBody(res)) || {}; + return { + valid: !!data.valid, + payer: data.payer, + amount: data.amount, + resource: data.resource, + balance: typeof data.balance === 'number' ? data.balance : undefined, + sufficient: typeof data.sufficient === 'boolean' ? data.sufficient : undefined, + error: data.error, + }; + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown error'; + return { valid: false, error: new X402FacilitatorError(message).message }; + } +} + +/** Settles a verified payment via the facilitator, finalising the transfer. */ +export async function settlePayment( + paymentHeader: string, + requirement: X402PaymentRequirement, + facilitatorUrl: string, + apiKey?: string +): Promise<{ success: boolean; txHash?: string; amount?: number; fee?: number; net?: number; network?: string; error?: string }> { + try { + const headers: Record = { 'Content-Type': 'application/json' }; + if (apiKey) headers['X-API-Key'] = apiKey; + + const amount = parseInt(requirement.amount, 10) / 1_000_000; + + const settleUrl = new URL('/settle', facilitatorUrl).toString(); + const res = await timedFetch(settleUrl, { + method: 'POST', + headers, + body: JSON.stringify({ paymentHeader, merchantWallet: requirement.payTo, amount, asset: requirement.asset || 'USDC' }), + }); + + if (!res.ok) { + const errBody = await safeParseBody<{ error?: string }>(res); + return { success: false, error: new X402FacilitatorError(errBody?.error || `Settlement returned ${res.status}`).message }; + } + + const data = (await safeParseBody(res)) || {}; + return { + success: !!data.success, + txHash: data.txHash, + amount: data.amount, + fee: data.fee, + net: data.net, + network: data.network, + error: data.error, + }; + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown error'; + return { success: false, error: new X402FacilitatorError(message).message }; + } +} + +/** Express middleware that gates a route behind an x402 payment wall. */ +export function x402Paywall(config: X402MiddlewareConfig) { + return async (req: any, res: any, next: any) => { + const resource = req.path || req.url || '/'; + const paymentHeader = (req.headers && (req.headers['x-payment'] || req.headers['X-Payment'])) as string | undefined; + + if (!paymentHeader) { + const body = createPaymentRequired(resource, config); + res.setHeader?.('WWW-Authenticate', 'X402'); + res.setHeader?.('X-Payment-Schemes', 'shadowwire'); + res.setHeader?.('Vary', PAYMENT_HEADER_NAME); + res.setHeader?.('Cache-Control', 'no-store'); + return res.status(402).json(body); + } + + if (byteLength(paymentHeader) > MAX_PAYMENT_HEADER_BYTES) { + return res.status(400).json({ error: new X402HeaderTooLargeError(byteLength(paymentHeader)).message }); + } + + const proof = X402Client.decodePaymentHeader(paymentHeader); + if (!proof || !isShadowwire(proof.scheme)) { + const body = createPaymentRequired(resource, config); + (body as any).verifyError = 'Invalid or unsupported payment proof'; + res.setHeader?.('WWW-Authenticate', 'X402'); + res.setHeader?.('Vary', PAYMENT_HEADER_NAME); + res.setHeader?.('Cache-Control', 'no-store'); + return res.status(402).json(body); + } + + const amount = Math.floor(config.amount * 1_000_000).toString(); + const requirement: X402PaymentRequirement = { + scheme: 'shadowwire', + network: 'solana:mainnet', + amount, + asset: config.asset || 'USDC', + payTo: config.payTo, + resource, + description: config.description, + maxTimeoutSeconds: config.maxTimeoutSeconds || 60, + }; + + if (proof.payload?.resource && proof.payload.resource !== resource) { + const body = createPaymentRequired(resource, config); + (body as any).verifyError = 'Payment proof resource mismatch'; + res.setHeader?.('WWW-Authenticate', 'X402'); + res.setHeader?.('Vary', PAYMENT_HEADER_NAME); + res.setHeader?.('Cache-Control', 'no-store'); + return res.status(402).json(body); + } + + const facilitator = config.facilitatorUrl || DEFAULT_FACILITATOR_URL; + + const verifyResult = await verifyPayment(paymentHeader, requirement, facilitator, config.apiKey); + + if (!verifyResult.valid) { + const body = createPaymentRequired(resource, config); + (body as any).verifyError = verifyResult.error; + res.setHeader?.('WWW-Authenticate', 'X402'); + res.setHeader?.('Vary', PAYMENT_HEADER_NAME); + res.setHeader?.('Cache-Control', 'no-store'); + return res.status(402).json(body); + } + + const settleResult = await settlePayment(paymentHeader, requirement, facilitator, config.apiKey); + if (!settleResult.success) { + const body = createPaymentRequired(resource, config); + (body as any).settleError = settleResult.error; + res.setHeader?.('WWW-Authenticate', 'X402'); + res.setHeader?.('Vary', PAYMENT_HEADER_NAME); + res.setHeader?.('Cache-Control', 'no-store'); + return res.status(402).json(body); + } + + req.x402 = { + payer: verifyResult.payer, + txHash: settleResult.txHash, + amount: settleResult.amount, + fee: settleResult.fee, + net: settleResult.net, + network: settleResult.network, + scheme: 'shadowwire', + }; + + if (config.onPayment && verifyResult.payer && settleResult.txHash) { + try { + config.onPayment({ payer: verifyResult.payer, amount: config.amount, signature: settleResult.txHash, resource }); + } catch { + // Don't block the request — callback errors are non-fatal + } + } + + next(); + }; +} + +export interface X402DiscoveryResource { + path: string; + method: string; + price: number; + asset?: string; + description?: string; +} + +/** Generates a `.well-known/x402` discovery document listing payable resources. */ +export function createDiscoveryDocument( + name: string, + payTo: string, + resources: X402DiscoveryResource[], + options?: { description?: string; facilitatorUrl?: string } +): Record { + return { + version: '2.0', + name, + description: options?.description, + payTo, + schemes: ['shadowwire'], + networks: ['solana:mainnet'], + facilitator: options?.facilitatorUrl || DEFAULT_FACILITATOR_URL, + resources: resources.map((r) => ({ + path: r.path, + method: r.method, + price: r.price, + asset: r.asset || 'USDC', + description: r.description, + schemes: ['shadowwire'], + })), + capabilities: { + privatePayments: true, + amountHiding: true, + bulletproofs: true, + }, + }; +} diff --git a/tsconfig.json b/tsconfig.json index 800c65c..c934861 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,6 @@ "sourceMap": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "examples"] + "exclude": ["node_modules", "dist", "examples", "src/**/*.test.ts"] }