From 2a540b6279d42a8b90457735696642fa64fe5a77 Mon Sep 17 00:00:00 2001 From: azf20 <9612972+azf20@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:19:13 +0000 Subject: [PATCH] sdk: accept payments from any HTTP stack via radius-sdk/server New `radius-sdk/server` entry point holding the seller core independently of how requests arrive: - `radiusPayments()` is a web-standard handler (Request in, Response out) for Workers without a framework, Bun, Deno, Node 18+, and Next.js / SvelteKit / Remix route handlers. Paid handlers receive the settled receipt; `wrap()` gives a single fetch function; `requiresPayment()` is a no-I/O route check. - `createRadiusServer()` exposes the Radius x402 resource server and a `routes()` builder for the upstream adapters: `paymentMiddleware(radius.routes({...}), radius.server)` with `@x402/express`, `@x402/next` or `@x402/hono`. - `onSettled` is registered via x402 core's after-settle hook so it fires for every adapter. Errors it throws are logged by core instead of failing the request. - `radius-sdk/hono` becomes a thin wrapper over the handler with unchanged options and tests; `RadiusHonoAdapter` is removed. facilitator.ts and scheme.ts move to src/server/. - `@x402/core` and `@x402/evm` 2.25.0 -> 2.27.0 (the line the upstream adapters require). - Examples: `examples/worker-plain` (no framework) and `examples/express-seller` (`@x402/express`), same API as the Hono worker. Verified: 123 unit tests + import guard (new test/server.test.ts covers the handler and Express through the real upstream adapter); 17 e2e tests on testnet including a real settlement through the plain handler; stock `radius-cli wallet x402` paid worker-plain under `wrangler dev` (0xb9e874a455fec5ad1bb39b35ff9614b56a1a2e93fbd859b77cb7f8bc2c62289a) and express-seller on Node (0xcd44add4aa1f43c5a7613efa23b27ad52efae7843e28a5188d508341cc80615e). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PtERNnagj1qYiSHvfLpdtu --- .changeset/sdk-framework-agnostic-server.md | 12 + README.md | 6 +- packages/sdk/README.md | 102 ++- .../sdk/examples/express-seller/package.json | 19 + .../sdk/examples/express-seller/src/index.ts | 44 + .../sdk/examples/express-seller/tsconfig.json | 14 + .../sdk/examples/worker-plain/package.json | 18 + .../sdk/examples/worker-plain/src/index.ts | 41 + .../sdk/examples/worker-plain/tsconfig.json | 12 + .../sdk/examples/worker-plain/wrangler.toml | 8 + packages/sdk/package.json | 17 +- packages/sdk/scripts/imports.test.mjs | 24 + packages/sdk/src/hono/index.ts | 326 +------ packages/sdk/src/server/adapter.ts | 85 ++ .../sdk/src/{hono => server}/facilitator.ts | 0 packages/sdk/src/server/index.ts | 379 ++++++++ packages/sdk/src/{hono => server}/scheme.ts | 0 packages/sdk/test/e2e/server.test.ts | 48 + packages/sdk/test/server.test.ts | 294 +++++++ pnpm-lock.yaml | 828 +++++++++++++++++- 20 files changed, 1923 insertions(+), 354 deletions(-) create mode 100644 .changeset/sdk-framework-agnostic-server.md create mode 100644 packages/sdk/examples/express-seller/package.json create mode 100644 packages/sdk/examples/express-seller/src/index.ts create mode 100644 packages/sdk/examples/express-seller/tsconfig.json create mode 100644 packages/sdk/examples/worker-plain/package.json create mode 100644 packages/sdk/examples/worker-plain/src/index.ts create mode 100644 packages/sdk/examples/worker-plain/tsconfig.json create mode 100644 packages/sdk/examples/worker-plain/wrangler.toml create mode 100644 packages/sdk/src/server/adapter.ts rename packages/sdk/src/{hono => server}/facilitator.ts (100%) create mode 100644 packages/sdk/src/server/index.ts rename packages/sdk/src/{hono => server}/scheme.ts (100%) create mode 100644 packages/sdk/test/e2e/server.test.ts create mode 100644 packages/sdk/test/server.test.ts diff --git a/.changeset/sdk-framework-agnostic-server.md b/.changeset/sdk-framework-agnostic-server.md new file mode 100644 index 0000000..83e91b3 --- /dev/null +++ b/.changeset/sdk-framework-agnostic-server.md @@ -0,0 +1,12 @@ +--- +"radius-sdk": minor +--- + +Accept payments from any HTTP stack, not just Hono. New `radius-sdk/server` entry point: + +- `radiusPayments()` is a web-standard handler (`Request` in, `Response` out) for Cloudflare Workers without a framework, Bun, Deno, Node 18+, and Next.js / SvelteKit / Remix route handlers. Paid handlers receive the settled receipt as a second argument. +- `createRadiusServer()` exposes the Radius x402 resource server and a `routes()` builder that plug straight into the upstream adapters: `paymentMiddleware(radius.routes({ … }), radius.server)` with `@x402/express`, `@x402/next` or `@x402/hono`. +- `onSettled` is registered on the resource server, so it fires whichever adapter served the request (it receives the x402 request context; `requestOf(context)` returns the `Request` for the SDK's own adapters). Errors thrown by `onSettled` are logged by x402 core instead of failing the request. +- `radius-sdk/hono` is now a thin wrapper over the web-standard handler with the same options; `RadiusHonoAdapter` is gone. +- `@x402/core` / `@x402/evm` bumped to 2.27.0 (the version line the upstream adapters require). +- New examples: `examples/worker-plain` (no framework) and `examples/express-seller` (`@x402/express`). diff --git a/README.md b/README.md index b0a27b1..cceff48 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,15 @@ Tools for the [Radius Network](https://radiustech.xyz), managed as one pnpm work | Package | What | | --- | --- | | [`packages/cli`](./packages/cli) | [`radius-cli`](https://www.npmjs.com/package/radius-cli) — CLI wallet for Radius, modeled on Foundry's `cast`; `wallet x402` pays through `radius-sdk` | -| [`packages/sdk`](./packages/sdk) | [`radius-sdk`](https://www.npmjs.com/package/radius-sdk) — accept and make Radius payments over x402 v2 (Hono / Cloudflare Workers first), plus balance and settlement helpers | +| [`packages/sdk`](./packages/sdk) | [`radius-sdk`](https://www.npmjs.com/package/radius-sdk) — accept and make Radius payments over x402 v2 from any web-standard runtime, Hono, or the upstream x402 framework adapters (Express, Next.js), plus balance and settlement helpers | ```bash npx radius-cli wallet balance # the CLI -pnpm add radius-sdk hono # SDK, seller side +pnpm add radius-sdk # SDK, seller side (add hono, or @x402/express + express, for those stacks) pnpm add radius-sdk viem # SDK, buyer / agent side ``` -Runnable SDK examples (seller worker, agent buyer, browser demo dapp) are in [`packages/sdk/examples`](./packages/sdk/examples). +Runnable SDK examples (seller workers with and without Hono, an Express seller, agent buyer, browser demo dapp) are in [`packages/sdk/examples`](./packages/sdk/examples). ## Agent skills diff --git a/packages/sdk/README.md b/packages/sdk/README.md index ca9d5f5..2aaeb9d 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -1,14 +1,17 @@ # radius-sdk Accept and make [Radius](https://radiustech.xyz) payments over standard [x402 v2](https://x402.org). -Hono and Cloudflare Workers first. SBC is the default currency, mainnet the default network. +Sellers run on any stack that speaks web-standard `Request`/`Response` (Cloudflare Workers, Bun, +Deno, Node, Next.js and SvelteKit route handlers), on Hono, or on Express / Next.js through the +upstream x402 adapters. SBC is the default currency, mainnet the default network. Pre-1.0: minor versions may change the API. Release notes are in [CHANGELOG.md](./CHANGELOG.md). | Entry point | What | Needs | | --- | --- | --- | | `radius-sdk` | networks, amounts, receipts, errors, `radiusEnv` (no viem at runtime) | — | -| `radius-sdk/hono` | `radiusPayments()` seller middleware | `hono` | +| `radius-sdk/server` | `radiusPayments()` web-standard seller handler; `createRadiusServer()` for the upstream `@x402/*` adapters | — | +| `radius-sdk/hono` | `radiusPayments()` Hono middleware (wraps `radius-sdk/server`) | `hono` | | `radius-sdk/client` | `createRadiusFetch()` paying fetch, balance and settlement actions | `viem` | ## Install @@ -16,16 +19,22 @@ Pre-1.0: minor versions may change the API. Release notes are in [CHANGELOG.md]( Install the peer dependencies for the entry point you use: ```sh -# Hono seller +# Seller on Workers / Bun / Deno / Node / route handlers +pnpm add radius-sdk + +# Seller on Hono pnpm add radius-sdk hono +# Seller on Express (or Next.js with @x402/next) +pnpm add radius-sdk @x402/express express + # Buyer / agent (including applications that also accept payments) pnpm add radius-sdk viem ``` Hono and viem are optional peer dependencies. The `radius-sdk/hono` entry point requires Hono; -`radius-sdk/client` requires viem `^2.48.11`. Root and Hono entry points do not load viem at -runtime. Network definitions remain compatible with viem's `Chain` type; TypeScript consumers +`radius-sdk/client` requires viem `^2.48.11`. Root, server and Hono entry points do not load +viem at runtime. Network definitions remain compatible with viem's `Chain` type; TypeScript consumers that resolve those declarations may also need viem installed for its types. Applications already using a compatible viem version can use that installation for the SDK's @@ -39,27 +48,73 @@ across the full dependency tree depends on compatible ranges and the package man ## Accept payments (seller) -```ts -import { Hono } from 'hono'; -import { radiusPayments, type RadiusPaymentVariables } from 'radius-sdk/hono'; +The payment configuration is the same everywhere; only the wrapper changes. -type Env = { Bindings: { PAY_TO: `0x${string}` }; Variables: RadiusPaymentVariables }; -const app = new Hono(); +**Any web-standard runtime** (Cloudflare Workers without a framework, Bun, Deno, Node 18+, +Next.js / SvelteKit / Remix route handlers): a handler that takes a `Request` and returns a +`Response`. Paid handlers receive the settled receipt. -app.use('/api/*', radiusPayments({ +```ts +import { radiusPayments } from 'radius-sdk/server'; + +const pay = radiusPayments({ network: 'testnet', // default 'mainnet'; or a custom instance, see below - payTo: (c) => c.env.PAY_TO, // or a literal address + payTo: '0xYourWallet', // or (request) => … routes: { 'GET /api/lookup': { price: '$0.001', description: 'One lookup' }, 'POST /api/query': '$0.01', // shorthand 'GET /api/raw': { price: { amount: '100' } }, // atomic units (6 decimals for SBC) }, +}); + +export default { + fetch: pay.wrap((request, payment) => Response.json({ ok: true, paidBy: payment?.payer })), +}; +// or, with your own router: `pay(request, (request, payment) => router.handle(request))` +``` + +**Hono** (`pnpm add hono`): the same options as middleware; dynamic `payTo`/`price` and +`onSettled` receive the Hono context and paid handlers read `c.get('radiusPayment')`. + +```ts +import { Hono } from 'hono'; +import { radiusPayments, type RadiusPaymentVariables } from 'radius-sdk/hono'; + +type Env = { Bindings: { PAY_TO: `0x${string}` }; Variables: RadiusPaymentVariables }; +const app = new Hono(); + +app.use('/api/*', radiusPayments({ + network: 'testnet', + payTo: (c) => c.env.PAY_TO, + routes: { 'GET /api/lookup': { price: '$0.001', description: 'One lookup' } }, })); app.get('/api/lookup', (c) => c.json({ ok: true, paidBy: c.get('radiusPayment')?.payer })); export default app; ``` +**Express, Next.js, or any other framework with an upstream x402 adapter**: the SDK provides +the Radius resource server and routes, the adapter provides the middleware. + +```ts +import express from 'express'; +import { paymentMiddleware } from '@x402/express'; // or `paymentProxy` from '@x402/next' +import { createRadiusServer } from 'radius-sdk/server'; + +const radius = createRadiusServer({ network: 'testnet' }); +const app = express(); +app.use(paymentMiddleware( + radius.routes({ payTo: '0xYourWallet', routes: { 'GET /api/lookup': '$0.001' } }), + radius.server, +)); +app.get('/api/lookup', (_req, res) => res.json({ ok: true })); +``` + +`radius.routes()` accepts the same route specs; dynamic `payTo`/`price` receive the x402 request +context. `radius.http(...)` returns an `x402HTTPResourceServer` for adapters' `…FromHTTPServer` +variants, and `radius.server` can be used with any `HTTPAdapter` of your own for stacks nobody +has an adapter for yet. + What you get, on the wire, with no Radius-specific client knowledge required: - Unpaid request → `402` with a `PAYMENT-REQUIRED` header: `exact` scheme, SBC via Permit2, @@ -67,14 +122,21 @@ What you get, on the wire, with no Radius-specific client knowledge required: - Paid request (`PAYMENT-SIGNATURE`) → settled on Radius through the Radius facilitator **before** your handler runs (`settle: 'after'` switches to the x402 default flow), then a `PAYMENT-RESPONSE` header with the transaction hash. -- `c.get('radiusPayment')` in the handler, and `onSettled(receipt, c)` for logging. +- The receipt in the handler (second argument / `c.get('radiusPayment')`), and `onSettled` for + logging with every adapter: it receives the `Request` (web-standard handler), the Hono context, + or the x402 request context (`createRadiusServer`). - `eip2612GasSponsoring` is declared only when the facilitator's `/supported` lists it (`gasSponsoring: true | false` overrides), so clients never send a permit nobody will honour. -- No I/O at module scope (Workers-safe): the facilitator's `/supported` is fetched lazily on the - first paid request after each cold start. Server bundle is ~65 KiB gzipped, no viem. +- No I/O at module scope (Workers-safe): the SDK's handlers fetch the facilitator's `/supported` + lazily on the first paid request after each cold start. The upstream adapters fetch it at + construction by default (fine on Node; pass their `syncFacilitatorOnStart: false` and call + `radius.server.initialize()` yourself where module-scope I/O is forbidden). Server bundle is + ~65 KiB gzipped, no viem. -Any x402 v2 client can pay it: verified with the pre-SDK `radius-cli wallet x402` 0.1.5 as well as -`createRadiusFetch` (which `radius-cli` uses from 0.2.0) paying a local `wrangler dev` worker on testnet. +Any x402 v2 client can pay it: verified with `radius-cli wallet x402` (which uses +`createRadiusFetch` from 0.2.0, and paid the SDK's 402s with its hand-rolled client before that) +against `examples/worker-plain` under `wrangler dev`, `examples/express-seller` on Node, and the +Hono `examples/worker-seller`, all on testnet. ## Make payments (buyer / agent) @@ -323,8 +385,10 @@ self-hosted facilitator with your own auth or routing. | Path | What | | --- | --- | -| `src/` | `networks`, `balances`, `erc20`, `permit2`, `amounts`, `receipt`, `settlement`, `schemes`, `env`, `errors`; `hono/` (server); `client/` (buyer) | -| `examples/worker-seller` | Hono worker: free `/`, paid `/api/lookup` and `/api/query` (`pnpm --filter radius-worker-seller dev`) | +| `src/` | `networks`, `balances`, `erc20`, `permit2`, `amounts`, `receipt`, `settlement`, `schemes`, `env`, `errors`; `server/` (seller core: web-standard handler, x402 resource server, facilitator, scheme); `hono/` (Hono wrapper); `client/` (buyer) | +| `examples/worker-plain` | Worker with no framework, the web-standard handler: free `/`, paid `/api/lookup` and `/api/query` (`pnpm --filter radius-worker-plain dev`, port 8788) | +| `examples/worker-seller` | Same API on Hono (`pnpm --filter radius-worker-seller dev`) | +| `examples/express-seller` | Same API on Express through `@x402/express` (`pnpm --filter radius-express-seller start`, port 8789) | | `examples/agent-buyer` | `buy.mjs` (pay a URL), `fresh-wallet.mjs` (gasless proof from a new wallet), `permit2-pull.mjs` (sign a Permit2 transfer off-chain, pull it from another account) | | `examples/demo-dapp` | Test-dapp style page exercising both sides in the browser (burner wallet or MetaMask) | | `test/` | unit tests (facilitator and RPC mocked; `client-parity.test.ts` pins the wire format against radius-cli's; `balances.test.ts` runs the native-balance init code in a real EVM; `erc20.semantics.test.ts` runs the ERC-20 actions against `evmNode.ts`, a JSON-RPC node backed by @ethereumjs/evm executing the forge-compiled `fixtures/TestToken` (rebuild with `fixtures/build.sh` after editing the .sol; the artifact is committed because CI has no forge)); `test/e2e` real settlement, balance reconciliation and ERC-20 round trips on testnet or mainnet (`RADIUS_E2E=1 RADIUS_PRIVATE_KEY=… [RADIUS_NETWORK=mainnet] pnpm test:e2e`) | diff --git a/packages/sdk/examples/express-seller/package.json b/packages/sdk/examples/express-seller/package.json new file mode 100644 index 0000000..025da19 --- /dev/null +++ b/packages/sdk/examples/express-seller/package.json @@ -0,0 +1,19 @@ +{ + "name": "radius-express-seller", + "private": true, + "type": "module", + "scripts": { + "start": "node --experimental-strip-types src/index.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@x402/express": "2.27.0", + "express": "^5.2.1", + "radius-sdk": "workspace:*" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "@types/node": "^24.0.0", + "typescript": "^5.9.0" + } +} diff --git a/packages/sdk/examples/express-seller/src/index.ts b/packages/sdk/examples/express-seller/src/index.ts new file mode 100644 index 0000000..9275e64 --- /dev/null +++ b/packages/sdk/examples/express-seller/src/index.ts @@ -0,0 +1,44 @@ +// A paid API on Express using the upstream x402 adapter. The SDK supplies the Radius +// resource server (facilitator, SBC pricing, gas sponsoring); `@x402/express` supplies +// the middleware. `@x402/next` and `@x402/hono` take the same two arguments. +import express from 'express'; +import { paymentMiddleware } from '@x402/express'; +import { createRadiusServer } from 'radius-sdk/server'; + +const PAY_TO = (process.env.PAY_TO ?? '0x1eF420190c299D4d133fE9227F780D7d5cE91BeE') as `0x${string}`; +const NETWORK = (process.env.RADIUS_NETWORK ?? 'testnet') as 'mainnet' | 'testnet'; +const PORT = Number(process.env.PORT ?? 8789); + +const radius = createRadiusServer({ + network: NETWORK, + onSettled: (receipt) => console.log('settled', receipt.transaction, receipt.payer), +}); + +const app = express(); +app.get('/', (_req, res) => { + res.json({ ok: true, paid: ['GET /api/lookup?ip=… $0.001', 'POST /api/query $0.01'] }); +}); + +app.use( + paymentMiddleware( + radius.routes({ + payTo: PAY_TO, + routes: { + 'GET /api/lookup': { price: '$0.001', description: 'Synthetic threat-intel lookup for one IP' }, + 'POST /api/query': { price: '$0.01', description: 'Batch query' }, + }, + }), + radius.server, + ), +); + +// Handlers only run after the payment has settled on Radius (the SDK defaults to settling first). +app.get('/api/lookup', (req, res) => { + const ip = typeof req.query.ip === 'string' ? req.query.ip : '0.0.0.0'; + res.json({ ip, reputation: ip.startsWith('10.') ? 'private' : 'clean', score: 7 }); +}); +app.post('/api/query', express.json(), (req, res) => { + res.json({ received: req.body ?? {}, results: [] }); +}); + +app.listen(PORT, () => console.log(`radius-express-seller on http://localhost:${PORT} (${NETWORK}, pay to ${PAY_TO})`)); diff --git a/packages/sdk/examples/express-seller/tsconfig.json b/packages/sdk/examples/express-seller/tsconfig.json new file mode 100644 index 0000000..40f044d --- /dev/null +++ b/packages/sdk/examples/express-seller/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "erasableSyntaxOnly": true, + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/sdk/examples/worker-plain/package.json b/packages/sdk/examples/worker-plain/package.json new file mode 100644 index 0000000..f00843d --- /dev/null +++ b/packages/sdk/examples/worker-plain/package.json @@ -0,0 +1,18 @@ +{ + "name": "radius-worker-plain", + "private": true, + "type": "module", + "scripts": { + "dev": "wrangler dev --port 8788", + "deploy": "wrangler deploy", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "radius-sdk": "workspace:*" + }, + "devDependencies": { + "@cloudflare/workers-types": "^5.20260910.1", + "typescript": "^5.9.0", + "wrangler": "^4.131.0" + } +} diff --git a/packages/sdk/examples/worker-plain/src/index.ts b/packages/sdk/examples/worker-plain/src/index.ts new file mode 100644 index 0000000..b821089 --- /dev/null +++ b/packages/sdk/examples/worker-plain/src/index.ts @@ -0,0 +1,41 @@ +// A paid API on Cloudflare Workers with no framework: the SDK's web-standard handler +// takes a `Request` and returns a `Response`. The same code runs on Bun, Deno, Node 18+ +// (`Bun.serve({ fetch })`, `Deno.serve(fetch)`) and in Next.js / SvelteKit route handlers. +import { radiusPayments, type PaymentHandler } from 'radius-sdk/server'; + +type Env = { PAY_TO: `0x${string}`; RADIUS_NETWORK: 'mainnet' | 'testnet' }; + +let pay: PaymentHandler | undefined; + +// Built on the first request so config can come from bindings (Workers forbid I/O at +// module scope; the handler does none, but env is only available per request). +function payments(env: Env): PaymentHandler { + return (pay ??= radiusPayments({ + network: env.RADIUS_NETWORK, + payTo: env.PAY_TO, + routes: { + 'GET /api/lookup': { price: '$0.001', description: 'Synthetic threat-intel lookup for one IP' }, + 'POST /api/query': { price: '$0.01', description: 'Batch query' }, + }, + onSettled: (receipt, request) => console.log('settled', receipt.transaction, receipt.payer, new URL(request.url).pathname), + })); +} + +export default { + fetch(request: Request, env: Env): Promise { + // `payment` is the settled receipt: handlers only run for money already received. + return payments(env)(request, async (request, payment) => { + const url = new URL(request.url); + if (url.pathname === '/') return Response.json({ ok: true, paid: ['GET /api/lookup?ip=… $0.001', 'POST /api/query $0.01'] }); + if (url.pathname === '/api/lookup') { + const ip = url.searchParams.get('ip') ?? '0.0.0.0'; + return Response.json({ ip, reputation: ip.startsWith('10.') ? 'private' : 'clean', score: 7, paidBy: payment?.payer, tx: payment?.transaction }); + } + if (url.pathname === '/api/query' && request.method === 'POST') { + const body = await request.json().catch(() => ({})); + return Response.json({ received: body, results: [] }); + } + return Response.json({ error: 'not_found' }, { status: 404 }); + }); + }, +}; diff --git a/packages/sdk/examples/worker-plain/tsconfig.json b/packages/sdk/examples/worker-plain/tsconfig.json new file mode 100644 index 0000000..a0d7de0 --- /dev/null +++ b/packages/sdk/examples/worker-plain/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "types": ["@cloudflare/workers-types"] + }, + "include": ["src"] +} diff --git a/packages/sdk/examples/worker-plain/wrangler.toml b/packages/sdk/examples/worker-plain/wrangler.toml new file mode 100644 index 0000000..5536d05 --- /dev/null +++ b/packages/sdk/examples/worker-plain/wrangler.toml @@ -0,0 +1,8 @@ +name = "radius-worker-plain" +main = "src/index.ts" +compatibility_date = "2026-09-01" + +[vars] +RADIUS_NETWORK = "testnet" +# Set PAY_TO to your wallet address (or put it in .dev.vars locally / `wrangler secret put` in prod). +PAY_TO = "0x1eF420190c299D4d133fE9227F780D7d5cE91BeE" diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 092bc99..cc64149 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,7 +1,7 @@ { "name": "radius-sdk", "version": "0.3.0", - "description": "Accept and make Radius payments over x402 v2 \u2014 Hono / Cloudflare Workers first", + "description": "Accept and make Radius payments over x402 v2 \u2014 any web-standard runtime, Hono, or the upstream x402 framework adapters", "type": "module", "license": "MIT", "engines": { @@ -17,6 +17,10 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./server": { + "types": "./dist/server/index.d.ts", + "default": "./dist/server/index.js" + }, "./hono": { "types": "./dist/hono/index.d.ts", "default": "./dist/hono/index.js" @@ -35,8 +39,8 @@ "test:imports": "pnpm run build && node --test scripts/imports.test.mjs" }, "dependencies": { - "@x402/core": "2.25.0", - "@x402/evm": "2.25.0" + "@x402/core": "2.27.0", + "@x402/evm": "2.27.0" }, "peerDependencies": { "hono": "^4.0.0", @@ -55,11 +59,14 @@ "@ethereumjs/evm": "10.1.3", "@ethereumjs/statemanager": "10.1.3", "@ethereumjs/util": "10.1.3", + "@types/express": "^5.0.6", + "@x402/express": "2.27.0", + "express": "^5.2.1", "hono": "^4.13.7", "typescript": "^5.9.0", + "viem": "^2.48.11", "vitest": "^3.2.0", - "wrangler": "^4.131.0", - "viem": "^2.48.11" + "wrangler": "^4.131.0" }, "repository": { "type": "git", diff --git a/packages/sdk/scripts/imports.test.mjs b/packages/sdk/scripts/imports.test.mjs index 205dcaa..e377fe8 100644 --- a/packages/sdk/scripts/imports.test.mjs +++ b/packages/sdk/scripts/imports.test.mjs @@ -53,6 +53,30 @@ test('root and Hono seller work with buyer dependency imports blocked', () => { `, true); }); +test('web-standard server handler works with buyer dependency imports blocked', () => { + run(` + import assert from 'node:assert/strict'; + import { radiusPayments, createRadiusServer } from 'radius-sdk/server'; + + const pay = radiusPayments({ + network: 'testnet', + payTo: '0x1111111111111111111111111111111111111111', + routes: { 'GET /paid': '0.001 SBC' }, + facilitator: { live: false }, + }); + let called = false; + const response = await pay(new Request('http://localhost/paid'), () => { called = true; return new Response('paid'); }); + assert.equal(response.status, 402); + assert.equal(called, false); + const challenge = JSON.parse(Buffer.from(response.headers.get('PAYMENT-REQUIRED'), 'base64')); + assert.equal(challenge.accepts[0].network, 'eip155:72344'); + assert.equal(challenge.accepts[0].amount, '1000'); + const radius = createRadiusServer({ network: 'testnet', facilitator: { live: false } }); + assert.equal(typeof radius.server.initialize, 'function'); + assert.ok('GET /paid' in radius.routes({ payTo: '0x1111111111111111111111111111111111111111', routes: { 'GET /paid': '$1' } })); + `, true); +}); + test('buyer initializes with the installed viem peer', () => { run(` import assert from 'node:assert/strict'; diff --git a/packages/sdk/src/hono/index.ts b/packages/sdk/src/hono/index.ts index 39e03d4..660443f 100644 --- a/packages/sdk/src/hono/index.ts +++ b/packages/sdk/src/hono/index.ts @@ -1,69 +1,23 @@ import type { Context, Env, MiddlewareHandler } from 'hono'; -import { - FacilitatorResponseError, - type FacilitatorClient, - SETTLEMENT_OVERRIDES_HEADER, - getFacilitatorResponseError, - withPrivateCacheControl, - x402HTTPResourceServer, - x402ResourceServer, - type HTTPAdapter, - type HTTPRequestContext, - type HTTPResponseInstructions, - type RouteConfig, - type RoutesConfig, -} from '@x402/core/server'; -import type { SettleResponse } from '@x402/core/types'; -import { resolveNetwork, type Address, type NetworkInput, type NetworkOverrides, type RadiusNetwork } from '../networks.js'; -import { resolvePrice, type Price } from '../amounts.js'; -import { explorerTxUrl } from '../networks.js'; +import type { HTTPRequestContext } from '@x402/core/server'; import type { PaymentReceipt } from '../receipt.js'; -import { RadiusFacilitatorClient, withUnknownOutcomes, type FacilitatorOptions } from './facilitator.js'; -import { RadiusExactScheme, type GasSponsoringMode, type SettleMode } from './scheme.js'; +import { RadiusServer, createPaymentHandler, requestOf, type PayTo as ServerPayTo, type RadiusServerOptions, type RouteSpec as ServerRouteSpec, type RouteSpecs } from '../server/index.js'; -export { RadiusFacilitatorClient, staticSupported, type FacilitatorOptions } from './facilitator.js'; -export { RadiusExactScheme, type GasSponsoringMode, type SettleMode } from './scheme.js'; +export { RadiusFacilitatorClient, staticSupported, withUnknownOutcomes, type FacilitatorOptions } from '../server/facilitator.js'; +export { RadiusExactScheme, type GasSponsoringMode, type SettleMode } from '../server/scheme.js'; +export { RadiusServer, createRadiusServer, RequestAdapter, toReceipt, type RadiusServerOptions } from '../server/index.js'; -export type PayTo = Address | ((c: Context) => Address | Promise
); +export type PayTo = ServerPayTo>; +export type RouteSpec = ServerRouteSpec>; -export interface RouteSpec { - /** "$0.01", "0.01", 0.01 (USD == SBC), or { amount: "10000" } atomic units. */ - price: Price | ((c: Context) => Price | Promise); - description?: string; - mimeType?: string; - /** Seconds the signed payment stays valid. Default 300. */ - maxTimeoutSeconds?: number; - /** Per-route recipient override. */ - payTo?: PayTo; -} - -export interface RadiusPaymentsOptions extends NetworkOverrides { - /** 'mainnet' (default), 'testnet', a preset, or a custom instance. */ - network?: NetworkInput; +export interface RadiusPaymentsOptions extends Omit { /** Recipient of every payment (unless a route overrides it). May read `c.env`. */ payTo: PayTo; /** * Protected routes keyed "METHOD /path" (Hono-style `*` wildcards allowed, e.g. * "GET /api/*"). A bare price is shorthand for `{ price }`. */ - routes: Record | Price>; - /** - * Which facilitator verifies and settles. Defaults to the Radius facilitator for - * the network. Pass `{ url, apiKey }` for another hosted facilitator, or your own - * `FacilitatorClient` (from `@x402/core/server`) for a self-hosted one. - */ - facilitator?: FacilitatorOptions | FacilitatorClient; - /** - * 'before' (default): verify and settle on-chain, then run the handler — the - * handler only ever runs for money already received. - * 'after': verify, run the handler, settle if it succeeded (x402 default flow). - */ - settle?: SettleMode; - /** - * Whether 402s declare `eip2612GasSponsoring` (lets first-time wallets pay without an - * approval transaction). 'auto' (default): only when the facilitator supports it. - */ - gasSponsoring?: GasSponsoringMode; + routes: RouteSpecs>; /** Called once per settled payment. */ onSettled?: (receipt: PaymentReceipt, c: Context) => void | Promise; } @@ -71,253 +25,31 @@ export interface RadiusPaymentsOptions extends NetworkOverr /** Hono context variable set for paid requests: `c.get('radiusPayment')`. */ export type RadiusPaymentVariables = { radiusPayment?: PaymentReceipt }; -/** Adapter over Hono's context that also carries the context for dynamic payTo/price. */ -export class RadiusHonoAdapter implements HTTPAdapter { - constructor(readonly c: Context) {} - getHeader(name: string): string | undefined { - return this.c.req.header(name); - } - getMethod(): string { - return this.c.req.method; - } - getPath(): string { - return this.c.req.path; - } - getUrl(): string { - return this.c.req.url; - } - getAcceptHeader(): string { - return this.c.req.header('accept') ?? ''; - } - getUserAgent(): string { - return this.c.req.header('user-agent') ?? ''; - } - getQueryParams(): Record { - return this.c.req.queries() as Record; - } - getQueryParam(name: string): string | string[] | undefined { - const all = this.c.req.queries(name); - if (!all || all.length === 0) return undefined; - return all.length === 1 ? all[0] : all; - } - async getBody(): Promise { - try { - return await this.c.req.raw.clone().json(); - } catch { - return undefined; - } - } -} - -const GAS_SPONSORING_DECLARATION = { - eip2612GasSponsoring: { - info: { - description: 'The facilitator accepts EIP-2612 gasless Permit to `Permit2` canonical contract.', - version: '1', - }, - schema: { - $schema: 'https://json-schema.org/draft/2020-12/schema', - type: 'object', - properties: { - from: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' }, - asset: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' }, - spender: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' }, - amount: { type: 'string', pattern: '^[0-9]+$' }, - nonce: { type: 'string', pattern: '^[0-9]+$' }, - deadline: { type: 'string', pattern: '^[0-9]+$' }, - signature: { type: 'string', pattern: '^0x[a-fA-F0-9]+$' }, - version: { type: 'string', pattern: '^[0-9]+(\\.[0-9]+)*$' }, - }, - required: ['from', 'asset', 'spender', 'amount', 'nonce', 'deadline', 'signature', 'version'], - }, - }, -} as const; - -function isFacilitatorClient(v: unknown): v is FacilitatorClient { - return typeof v === 'object' && v !== null && typeof (v as FacilitatorClient).settle === 'function' && typeof (v as FacilitatorClient).getSupported === 'function'; -} - -function contextOf(ctx: HTTPRequestContext): Context { - const adapter = ctx.adapter as RadiusHonoAdapter; - if (!adapter.c) throw new Error('radius-sdk: expected RadiusHonoAdapter'); - return adapter.c; -} - -function toReceipt(r: SettleResponse, network: RadiusNetwork, requirements: { amount: string }): PaymentReceipt { - const transaction = r.transaction && r.transaction.length > 0 ? r.transaction : undefined; - return { - success: r.success, - transaction, - network: r.network, - payer: r.payer, - // `exact` settles precisely the requested amount; facilitators only report `amount` - // for schemes (like `upto`) where it can differ. - amount: r.amount ?? (r.success ? requirements.amount : undefined), - errorReason: r.errorReason, - errorMessage: r.errorMessage, - explorerUrl: transaction ? explorerTxUrl(network, transaction) : undefined, - }; -} - -function applyInstructions(c: Context, r: HTTPResponseInstructions): Response { - for (const [k, v] of Object.entries(r.headers)) c.header(k, v); - if (r.isHtml) return c.html(String(r.body ?? ''), r.status as 402); - return c.json((r.body ?? {}) as object, r.status as 402); -} - -function facilitatorErrorResponse(c: Context, error: FacilitatorResponseError): Response { - return c.json({ error: 'facilitator_error', message: error.message }, 502); -} - -function internalErrorResponse(c: Context, error: unknown): Response { - const message = error instanceof Error ? error.message : String(error); - return c.json({ error: 'payment_processing_error', message }, 500); -} - /** - * Hono middleware that charges for routes with Radius x402 payments. - * - * Standard x402 v2 on the wire: unpaid requests get a 402 with a `PAYMENT-REQUIRED` - * header; paid requests carry `PAYMENT-SIGNATURE`, are verified and settled through - * the Radius facilitator, and get a `PAYMENT-RESPONSE` header back. + * Hono middleware that charges for routes with Radius x402 payments. A thin wrapper + * over the web-standard handler in `radius-sdk/server`: dynamic `payTo`/`price` and + * `onSettled` receive the Hono context, and paid handlers can read the receipt with + * `c.get('radiusPayment')` (when `settle` is 'before', the default). */ export function radiusPayments(options: RadiusPaymentsOptions): MiddlewareHandler { - const network = resolveNetwork(options.network, options); - const settle: SettleMode = options.settle ?? 'before'; - const facilitator = withUnknownOutcomes( - isFacilitatorClient(options.facilitator) ? options.facilitator : new RadiusFacilitatorClient(network, options.facilitator), - ); - const resourceServer = new x402ResourceServer(facilitator).register(network.network, new RadiusExactScheme(network, settle, options.gasSponsoring ?? 'auto')); - - const resolvePayTo = (spec: PayTo) => - typeof spec === 'function' ? (ctx: HTTPRequestContext) => spec(contextOf(ctx)) : spec; - - const routes: RoutesConfig = {}; - for (const [pattern, specOrPrice] of Object.entries(options.routes)) { - const spec: RouteSpec = - typeof specOrPrice === 'object' && specOrPrice !== null && 'price' in specOrPrice - ? (specOrPrice as RouteSpec) - : { price: specOrPrice as Price }; - const price = spec.price; - const normalise = (p: Price) => resolvePrice(p, network.asset); - const route: RouteConfig & { extensions?: Record } = { - accepts: { - scheme: 'exact', - network: network.network, - payTo: resolvePayTo(spec.payTo ?? options.payTo), - price: typeof price === 'function' ? async (ctx: HTTPRequestContext) => normalise(await price(contextOf(ctx))) : normalise(price), - maxTimeoutSeconds: spec.maxTimeoutSeconds ?? 300, - }, - description: spec.description, - mimeType: spec.mimeType, - extensions: { ...GAS_SPONSORING_DECLARATION }, - }; - routes[pattern] = route; - } - - const httpServer = new x402HTTPResourceServer(resourceServer, routes); - - // Lazy, request-time initialisation: Workers forbid I/O at module scope, and the - // static facilitator answer means this is normally just bookkeeping anyway. - let initPromise: Promise | undefined; - const ensureInitialized = () => - (initPromise ??= httpServer.initialize().catch((e) => { - initPromise = undefined; - throw e; - })); - + const { payTo, routes, onSettled, ...serverOptions } = options; + const contexts = new WeakMap>(); + const contextOf = (ctx: HTTPRequestContext | undefined): Context => { + const c = contexts.get(requestOf(ctx) as Request); + if (!c) throw new Error('radius-sdk/hono: request was not served through this middleware'); + return c; + }; + const radius = new RadiusServer({ + ...serverOptions, + onSettled: onSettled && ((receipt, ctx) => onSettled(receipt, contextOf(ctx))), + }); + const handler = createPaymentHandler(radius, radius.buildRoutes({ payTo, routes }, contextOf)); return async (c, next) => { - const adapter = new RadiusHonoAdapter(c); - const context: HTTPRequestContext = { - adapter, - path: c.req.path, - method: c.req.method, - paymentHeader: adapter.getHeader('payment-signature') ?? adapter.getHeader('x-payment'), - }; - if (!httpServer.requiresPayment(context)) return next(); - - try { - await ensureInitialized(); - } catch (error) { - const fe = getFacilitatorResponseError(error); - return fe ? facilitatorErrorResponse(c, fe) : internalErrorResponse(c, error); - } - - let result: Awaited>; - try { - result = await httpServer.processHTTPRequest(context); - } catch (error) { - if (error instanceof FacilitatorResponseError) return facilitatorErrorResponse(c, error); - return internalErrorResponse(c, error); - } - - if (result.type === 'no-payment-required') return next(); - if (result.type === 'payment-error') return applyInstructions(c, result.response); - - const { cancellationDispatcher, beforeHandlerSettlement, paymentPayload, paymentRequirements, declaredExtensions } = result; - let notified = false; - const notify = async (r: SettleResponse) => { - if (notified) return; - notified = true; - const receipt = toReceipt(r, network, paymentRequirements); - c.set('radiusPayment' as never, receipt as never); - if (options.onSettled) await options.onSettled(receipt, c); - }; - if (beforeHandlerSettlement) await notify(beforeHandlerSettlement.result); - - try { + contexts.set(c.req.raw, c); + c.res = await handler(c.req.raw, async (_request, payment) => { + if (payment) c.set('radiusPayment' as never, payment as never); await next(); - } catch (error) { - const cancelSettlement = await cancellationDispatcher.cancel({ reason: 'handler_threw', error: error as Error }); - if (!beforeHandlerSettlement && !cancelSettlement) throw error; - const res = internalErrorResponse(c, error); - const headers = httpServer.createFailurePathSettlementHeaders(cancelSettlement, beforeHandlerSettlement, paymentPayload, res.headers.get('Cache-Control')); - if (headers) for (const [k, v] of Object.entries(headers)) res.headers.set(k, v); - c.res = res; - return; - } - - let res = c.res; - if (res.status >= 400) { - const cancelSettlement = await cancellationDispatcher.cancel({ reason: 'handler_failed', responseStatus: res.status }); - res.headers.delete(SETTLEMENT_OVERRIDES_HEADER); - const headers = httpServer.createFailurePathSettlementHeaders(cancelSettlement, beforeHandlerSettlement, paymentPayload, res.headers.get('Cache-Control')); - if (headers) for (const [k, v] of Object.entries(headers)) res.headers.set(k, v); - return; - } - - const responseBody = new Uint8Array(await res.clone().arrayBuffer()); - const responseHeaders: Record = {}; - res.headers.forEach((v, k) => { - responseHeaders[k] = v; + return c.res; }); - c.res = undefined; - try { - const settleResult = await httpServer.processSettlement( - paymentPayload, - paymentRequirements, - declaredExtensions, - { request: context, responseBody, responseHeaders }, - undefined, - beforeHandlerSettlement, - ); - if (!settleResult.success) { - const r = settleResult.response; - res = new Response(r.isHtml ? String(r.body ?? '') : JSON.stringify(r.body ?? {}), { status: r.status, headers: r.headers }); - } else { - for (const [k, v] of Object.entries(settleResult.headers)) res.headers.set(k, v); - res.headers.set('Cache-Control', withPrivateCacheControl(res.headers.get('Cache-Control'))); - res.headers.delete(SETTLEMENT_OVERRIDES_HEADER); - await notify(settleResult); - } - } catch (error) { - if (error instanceof FacilitatorResponseError) { - c.res = facilitatorErrorResponse(c, error); - return; - } - console.error(error); - res = c.json({ error: 'settlement_error' }, 402); - } - c.res = res; }; } diff --git a/packages/sdk/src/server/adapter.ts b/packages/sdk/src/server/adapter.ts new file mode 100644 index 0000000..fed2e8b --- /dev/null +++ b/packages/sdk/src/server/adapter.ts @@ -0,0 +1,85 @@ +import type { HTTPAdapter, HTTPRequestContext } from '@x402/core/server'; + +/** + * x402 `HTTPAdapter` over a web-standard `Request`. Works anywhere `Request`/`Response` + * exist: Cloudflare Workers, Bun, Deno, Node 18+, Next.js route handlers, and + * frameworks built on them (Hono, SvelteKit, Remix, Astro). + */ +export class RequestAdapter implements HTTPAdapter { + readonly url: URL; + + constructor(readonly request: Request) { + this.url = new URL(request.url); + } + + getHeader(name: string): string | undefined { + return this.request.headers.get(name) ?? undefined; + } + getMethod(): string { + return this.request.method; + } + getPath(): string { + return this.url.pathname; + } + getUrl(): string { + return this.request.url; + } + getAcceptHeader(): string { + return this.request.headers.get('accept') ?? ''; + } + getUserAgent(): string { + return this.request.headers.get('user-agent') ?? ''; + } + getQueryParams(): Record { + const out: Record = {}; + for (const [key, value] of this.url.searchParams) { + const existing = out[key]; + if (existing === undefined) out[key] = value; + else if (Array.isArray(existing)) existing.push(value); + else out[key] = [existing, value]; + } + return out; + } + getQueryParam(name: string): string | string[] | undefined { + const all = this.url.searchParams.getAll(name); + if (all.length === 0) return undefined; + return all.length === 1 ? all[0] : all; + } + async getBody(): Promise { + try { + return await this.request.clone().json(); + } catch { + return undefined; + } + } +} + +function decodePath(pathname: string): string | undefined { + try { + return decodeURIComponent(pathname); + } catch { + return undefined; + } +} + +/** Build the x402 request context for a web-standard `Request`. */ +export function requestContext(request: Request): HTTPRequestContext { + const adapter = new RequestAdapter(request); + return { + adapter, + path: adapter.getPath(), + decodedPath: decodePath(adapter.getPath()), + method: request.method, + paymentHeader: adapter.getHeader('payment-signature') ?? adapter.getHeader('x-payment'), + }; +} + +/** + * The `Request` behind an x402 request context, when it was produced by this SDK's + * adapter (the web-standard handler and the Hono middleware). `undefined` for + * requests served through a third-party adapter such as `@x402/express`. + */ +export function requestOf(context: HTTPRequestContext | undefined): Request | undefined { + const adapter = context?.adapter; + return adapter instanceof RequestAdapter ? adapter.request : undefined; +} diff --git a/packages/sdk/src/hono/facilitator.ts b/packages/sdk/src/server/facilitator.ts similarity index 100% rename from packages/sdk/src/hono/facilitator.ts rename to packages/sdk/src/server/facilitator.ts diff --git a/packages/sdk/src/server/index.ts b/packages/sdk/src/server/index.ts new file mode 100644 index 0000000..a8f4d30 --- /dev/null +++ b/packages/sdk/src/server/index.ts @@ -0,0 +1,379 @@ +import { + FacilitatorResponseError, + type FacilitatorClient, + SETTLEMENT_OVERRIDES_HEADER, + getFacilitatorResponseError, + withPrivateCacheControl, + x402HTTPResourceServer, + x402ResourceServer, + type HTTPRequestContext, + type HTTPResponseInstructions, + type RouteConfig, + type RoutesConfig, +} from '@x402/core/server'; +import type { SettleResponse } from '@x402/core/types'; +import { resolveNetwork, explorerTxUrl, type Address, type NetworkInput, type NetworkOverrides, type RadiusNetwork } from '../networks.js'; +import { resolvePrice, type Price } from '../amounts.js'; +import type { PaymentReceipt } from '../receipt.js'; +import { RadiusFacilitatorClient, withUnknownOutcomes, type FacilitatorOptions } from './facilitator.js'; +import { RadiusExactScheme, type GasSponsoringMode, type SettleMode } from './scheme.js'; +import { requestContext, requestOf } from './adapter.js'; + +export { RadiusFacilitatorClient, staticSupported, withUnknownOutcomes, type FacilitatorOptions } from './facilitator.js'; +export { RadiusExactScheme, type GasSponsoringMode, type SettleMode } from './scheme.js'; +export { RequestAdapter, requestContext, requestOf } from './adapter.js'; +export type { HTTPRequestContext, RoutesConfig, FacilitatorClient } from '@x402/core/server'; + +/** Recipient address, or a function of the request. */ +export type PayTo = Address | ((ctx: Ctx) => Address | Promise
); + +export interface RouteSpec { + /** "$0.01", "0.01", 0.01 (USD == SBC), or { amount: "10000" } atomic units. */ + price: Price | ((ctx: Ctx) => Price | Promise); + description?: string; + mimeType?: string; + /** Seconds the signed payment stays valid. Default 300. */ + maxTimeoutSeconds?: number; + /** Per-route recipient override. */ + payTo?: PayTo; +} + +/** + * Protected routes keyed "METHOD /path" (`*` wildcards allowed, e.g. "GET /api/*"). + * A bare price is shorthand for `{ price }`. + */ +export type RouteSpecs = Record | Price>; + +export interface RoutesOptions { + /** Recipient of every payment (unless a route overrides it). */ + payTo: PayTo; + routes: RouteSpecs; +} + +export interface RadiusServerOptions extends NetworkOverrides { + /** 'mainnet' (default), 'testnet', a preset, or a custom instance. */ + network?: NetworkInput; + /** + * Which facilitator verifies and settles. Defaults to the Radius facilitator for + * the network. Pass `{ url, apiKey }` for another hosted facilitator, or your own + * `FacilitatorClient` (from `@x402/core/server`) for a self-hosted one. + */ + facilitator?: FacilitatorOptions | FacilitatorClient; + /** + * 'before' (default): verify and settle on-chain, then run the handler — the + * handler only ever runs for money already received. + * 'after': verify, run the handler, settle if it succeeded (x402 default flow). + */ + settle?: SettleMode; + /** + * Whether 402s declare `eip2612GasSponsoring` (lets first-time wallets pay without an + * approval transaction). 'auto' (default): only when the facilitator supports it. + */ + gasSponsoring?: GasSponsoringMode; + /** + * Called once per settled payment, whichever adapter served the request. `context` + * is the x402 request context; `requestOf(context)` gives the `Request` when the + * SDK's own adapter handled it. + */ + onSettled?: (receipt: PaymentReceipt, context: HTTPRequestContext | undefined) => void | Promise; +} + +const GAS_SPONSORING_DECLARATION = { + eip2612GasSponsoring: { + info: { + description: 'The facilitator accepts EIP-2612 gasless Permit to `Permit2` canonical contract.', + version: '1', + }, + schema: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { + from: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' }, + asset: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' }, + spender: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' }, + amount: { type: 'string', pattern: '^[0-9]+$' }, + nonce: { type: 'string', pattern: '^[0-9]+$' }, + deadline: { type: 'string', pattern: '^[0-9]+$' }, + signature: { type: 'string', pattern: '^0x[a-fA-F0-9]+$' }, + version: { type: 'string', pattern: '^[0-9]+(\\.[0-9]+)*$' }, + }, + required: ['from', 'asset', 'spender', 'amount', 'nonce', 'deadline', 'signature', 'version'], + }, + }, +} as const; + +function isFacilitatorClient(v: unknown): v is FacilitatorClient { + return typeof v === 'object' && v !== null && typeof (v as FacilitatorClient).settle === 'function' && typeof (v as FacilitatorClient).getSupported === 'function'; +} + +/** Turn a facilitator settle result into the receipt handed to application code. */ +export function toReceipt(r: SettleResponse, network: RadiusNetwork, requirements: { amount: string }): PaymentReceipt { + const transaction = r.transaction && r.transaction.length > 0 ? r.transaction : undefined; + return { + success: r.success, + transaction, + network: r.network, + payer: r.payer, + // `exact` settles precisely the requested amount; facilitators only report `amount` + // for schemes (like `upto`) where it can differ. + amount: r.amount ?? (r.success ? requirements.amount : undefined), + errorReason: r.errorReason, + errorMessage: r.errorMessage, + explorerUrl: transaction ? explorerTxUrl(network, transaction) : undefined, + }; +} + +/** + * Radius payments for any HTTP stack. Holds the x402 resource server (Radius scheme, + * facilitator, gas-sponsoring rules) independently of how requests arrive: + * + * - `server` + `routes()` plug into the upstream adapters (`@x402/express`, + * `@x402/next`, `@x402/hono`): `paymentMiddleware(radius.routes({...}), radius.server)`. + * - `handler()` serves web-standard `Request` → `Response` directly (Workers, Bun, + * Deno, Node, route handlers). `radius-sdk/hono` wraps it for Hono. + */ +export class RadiusServer { + readonly network: RadiusNetwork; + /** The facilitator, wrapped so calls that fail without its own answer surface as 502 (`withUnknownOutcomes`). */ + readonly facilitator: FacilitatorClient; + readonly scheme: RadiusExactScheme; + /** The x402 resource server, for upstream framework adapters. */ + readonly server: x402ResourceServer; + + constructor(options: RadiusServerOptions = {}) { + this.network = resolveNetwork(options.network, options); + this.facilitator = withUnknownOutcomes(isFacilitatorClient(options.facilitator) ? options.facilitator : new RadiusFacilitatorClient(this.network, options.facilitator)); + this.scheme = new RadiusExactScheme(this.network, options.settle ?? 'before', options.gasSponsoring ?? 'auto'); + this.server = new x402ResourceServer(this.facilitator).register(this.network.network, this.scheme); + if (options.onSettled) this.onSettled(options.onSettled); + } + + /** Register a settlement listener (see `RadiusServerOptions.onSettled`). */ + onSettled(hook: NonNullable): this { + this.server.onAfterSettle(async (ctx) => { + if (!ctx.result.success) return; + const transport = ctx.transportContext as { request?: HTTPRequestContext } | undefined; + await hook(toReceipt(ctx.result as SettleResponse, this.network, ctx.requirements), transport?.request); + }); + return this; + } + + /** + * x402 `RoutesConfig` with Radius pricing (USD/SBC display units or atomic amounts) + * and the gas-sponsoring declaration. Dynamic `payTo`/`price` receive the x402 + * request context. + */ + routes(options: RoutesOptions): RoutesConfig { + return this.buildRoutes(options, (ctx) => ctx); + } + + /** `routes()` with dynamic functions receiving a framework-specific context. @internal */ + buildRoutes(options: RoutesOptions, contextOf: (ctx: HTTPRequestContext) => Ctx): RoutesConfig { + const network = this.network; + const resolvePayTo = (spec: PayTo) => (typeof spec === 'function' ? (ctx: HTTPRequestContext) => spec(contextOf(ctx)) : spec); + const normalise = (p: Price) => resolvePrice(p, network.asset); + const routes: RoutesConfig = {}; + for (const [pattern, specOrPrice] of Object.entries(options.routes)) { + const spec: RouteSpec = + typeof specOrPrice === 'object' && specOrPrice !== null && 'price' in specOrPrice ? (specOrPrice as RouteSpec) : { price: specOrPrice as Price }; + const price = spec.price; + const route: RouteConfig & { extensions?: Record } = { + accepts: { + scheme: 'exact', + network: network.network, + payTo: resolvePayTo(spec.payTo ?? options.payTo), + price: typeof price === 'function' ? async (ctx: HTTPRequestContext) => normalise(await price(contextOf(ctx))) : normalise(price), + maxTimeoutSeconds: spec.maxTimeoutSeconds ?? 300, + }, + description: spec.description, + mimeType: spec.mimeType, + extensions: { ...GAS_SPONSORING_DECLARATION }, + }; + routes[pattern] = route; + } + return routes; + } + + /** An `x402HTTPResourceServer` for these routes (`paymentMiddlewareFromHTTPServer` in upstream adapters). */ + http(options: RoutesOptions): x402HTTPResourceServer { + return new x402HTTPResourceServer(this.server, this.routes(options)); + } + + /** Web-standard handler for these routes; dynamic `payTo`/`price` receive the `Request`. */ + handler(options: RoutesOptions): PaymentHandler { + return createPaymentHandler(this, this.buildRoutes(options, (ctx) => requestOf(ctx) ?? missingRequest())); + } +} + +function missingRequest(): never { + throw new Error('radius-sdk: request context was not produced by RequestAdapter'); +} + +export function createRadiusServer(options: RadiusServerOptions = {}): RadiusServer { + return new RadiusServer(options); +} + +/** The application handler behind a paid route. `payment` is set when settlement ran before the handler (`settle: 'before'`, the default). */ +export type NextHandler = (request: Request, payment?: PaymentReceipt) => Response | Promise; + +export interface PaymentHandler { + /** Charge for `request` if it matches a paid route, then call `next` (or answer 402 / errors). */ + (request: Request, next: NextHandler): Promise; + /** `handler => request => Response`, for runtimes that take a single fetch function. */ + wrap(next: NextHandler): (request: Request) => Promise; + /** True when `request` matches a paid route (cheap, no network). */ + requiresPayment(request: Request): boolean; + readonly radius: RadiusServer; +} + +export interface RadiusPaymentsOptions extends Omit, RoutesOptions { + /** Called once per settled payment with the `Request` that paid. */ + onSettled?: (receipt: PaymentReceipt, request: Request) => void | Promise; +} + +/** + * Charge for routes with Radius x402 payments, for any runtime that speaks + * web-standard `Request`/`Response`: + * + * ```ts + * const pay = radiusPayments({ network: 'testnet', payTo: '0x…', routes: { 'GET /api/lookup': '$0.001' } }); + * export default { fetch: pay.wrap((request, payment) => Response.json({ paidBy: payment?.payer })) }; + * ``` + * + * Standard x402 v2 on the wire: unpaid requests get a 402 with a `PAYMENT-REQUIRED` + * header; paid requests carry `PAYMENT-SIGNATURE`, are verified and settled through + * the facilitator, and get a `PAYMENT-RESPONSE` header back. + */ +export function radiusPayments(options: RadiusPaymentsOptions): PaymentHandler { + const { payTo, routes, onSettled, ...serverOptions } = options; + const radius = new RadiusServer({ + ...serverOptions, + onSettled: onSettled && ((receipt, ctx) => onSettled(receipt, requestOf(ctx) ?? missingRequest())), + }); + return radius.handler({ payTo, routes }); +} + +function jsonResponse(body: unknown, status: number): Response { + return Response.json(body, { status }); +} + +function instructionsToResponse(r: HTTPResponseInstructions): Response { + const res = r.isHtml ? new Response(String(r.body ?? ''), { status: r.status, headers: { 'Content-Type': 'text/html; charset=UTF-8' } }) : jsonResponse(r.body ?? {}, r.status); + for (const [k, v] of Object.entries(r.headers)) res.headers.set(k, v); + return res; +} + +function facilitatorErrorResponse(error: FacilitatorResponseError): Response { + return jsonResponse({ error: 'facilitator_error', message: error.message }, 502); +} + +function internalErrorResponse(error: unknown): Response { + const message = error instanceof Error ? error.message : String(error); + return jsonResponse({ error: 'payment_processing_error', message }, 500); +} + +function errorResponse(error: unknown): Response { + const fe = getFacilitatorResponseError(error); + return fe ? facilitatorErrorResponse(fe) : internalErrorResponse(error); +} + +function setHeaders(res: Response, headers: Record | undefined): void { + if (headers) for (const [k, v] of Object.entries(headers)) res.headers.set(k, v); +} + +/** A copy of `res` whose headers are mutable (responses from `fetch()` have immutable headers). */ +function mutable(res: Response, body: BodyInit | null = res.body): Response { + return new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers }); +} + +/** Web-standard handler for an already-built x402 `RoutesConfig` (see `RadiusServer.buildRoutes`). @internal */ +export function createPaymentHandler(radius: RadiusServer, routes: RoutesConfig): PaymentHandler { + const { network } = radius; + const httpServer = new x402HTTPResourceServer(radius.server, routes); + + // Lazy, request-time initialisation: Workers forbid I/O at module scope, and the + // static facilitator answer means this is normally just bookkeeping anyway. + let initPromise: Promise | undefined; + const ensureInitialized = () => + (initPromise ??= httpServer.initialize().catch((e) => { + initPromise = undefined; + throw e; + })); + + const handle = async (request: Request, next: NextHandler): Promise => { + const context = requestContext(request); + if (!httpServer.requiresPayment(context)) return next(request); + + try { + await ensureInitialized(); + } catch (error) { + return errorResponse(error); + } + + let result: Awaited>; + try { + result = await httpServer.processHTTPRequest(context); + } catch (error) { + return errorResponse(error); + } + + if (result.type === 'no-payment-required') return next(request); + if (result.type === 'payment-error') return instructionsToResponse(result.response); + + const { cancellationDispatcher, beforeHandlerSettlement, paymentPayload, paymentRequirements, declaredExtensions } = result; + const payment = beforeHandlerSettlement ? toReceipt(beforeHandlerSettlement.result, network, paymentRequirements) : undefined; + + let res: Response; + try { + res = await next(request, payment); + } catch (error) { + const cancelSettlement = await cancellationDispatcher.cancel({ reason: 'handler_threw', error: error as Error }); + if (!beforeHandlerSettlement && !cancelSettlement) throw error; + const failure = internalErrorResponse(error); + setHeaders(failure, httpServer.createFailurePathSettlementHeaders(cancelSettlement, beforeHandlerSettlement, paymentPayload, failure.headers.get('Cache-Control'))); + return failure; + } + + if (res.status >= 400) { + const cancelSettlement = await cancellationDispatcher.cancel({ reason: 'handler_failed', responseStatus: res.status }); + const failure = mutable(res); + failure.headers.delete(SETTLEMENT_OVERRIDES_HEADER); + setHeaders(failure, httpServer.createFailurePathSettlementHeaders(cancelSettlement, beforeHandlerSettlement, paymentPayload, failure.headers.get('Cache-Control'))); + return failure; + } + + const responseBody = new Uint8Array(await res.arrayBuffer()); + const responseHeaders: Record = {}; + res.headers.forEach((v, k) => { + responseHeaders[k] = v; + }); + try { + const settleResult = await httpServer.processSettlement( + paymentPayload, + paymentRequirements, + declaredExtensions, + { request: context, responseBody, responseHeaders }, + undefined, + beforeHandlerSettlement, + ); + if (!settleResult.success) return instructionsToResponse(settleResult.response); + const out = mutable(res, responseBody); + setHeaders(out, settleResult.headers); + out.headers.set('Cache-Control', withPrivateCacheControl(out.headers.get('Cache-Control'))); + out.headers.delete(SETTLEMENT_OVERRIDES_HEADER); + return out; + } catch (error) { + if (error instanceof FacilitatorResponseError) return facilitatorErrorResponse(error); + console.error(error); + return jsonResponse({ error: 'settlement_error' }, 402); + } + }; + + const handler = handle as PaymentHandler; + Object.defineProperties(handler, { + radius: { value: radius, enumerable: true }, + wrap: { value: (next: NextHandler) => (request: Request) => handle(request, next), enumerable: true }, + requiresPayment: { value: (request: Request) => httpServer.requiresPayment(requestContext(request)), enumerable: true }, + }); + return handler; +} diff --git a/packages/sdk/src/hono/scheme.ts b/packages/sdk/src/server/scheme.ts similarity index 100% rename from packages/sdk/src/hono/scheme.ts rename to packages/sdk/src/server/scheme.ts diff --git a/packages/sdk/test/e2e/server.test.ts b/packages/sdk/test/e2e/server.test.ts new file mode 100644 index 0000000..260d10d --- /dev/null +++ b/packages/sdk/test/e2e/server.test.ts @@ -0,0 +1,48 @@ +/** + * End-to-end for the web-standard handler against the real Radius facilitator: the + * seller is `radiusPayments()` from `radius-sdk/server` called directly with a + * `Request`, the buyer is `createRadiusFetch`. Same env as settlement.test.ts. + */ +import { beforeAll, describe, expect, it } from 'vitest'; +import { privateKeyToAccount } from 'viem/accounts'; +import { radiusPayments } from '../../src/server/index.js'; +import { createRadiusFetch } from '../../src/client/index.js'; +import { getPaymentReceipt } from '../../src/receipt.js'; +import { resolveNetwork } from '../../src/networks.js'; + +const NET = (process.env.RADIUS_NETWORK ?? 'testnet') as 'mainnet' | 'testnet'; +const network = resolveNetwork(NET); +const KEY = process.env.RADIUS_PRIVATE_KEY as `0x${string}` | undefined; +const run = process.env.RADIUS_E2E && KEY ? describe : describe.skip; + +run(`${NET} e2e (web-standard handler)`, () => { + const payer = privateKeyToAccount(KEY!); + const PAY_TO = (process.env.PAY_TO as `0x${string}`) ?? payer.address; + const settled: { tx?: string; path: string }[] = []; + const pay = radiusPayments({ + network: NET, + payTo: PAY_TO, + routes: { 'GET /api/lookup': '$0.001' }, + onSettled: (r, request) => { settled.push({ tx: r.transaction, path: new URL(request.url).pathname }); }, + }); + const app = pay.wrap((_request, payment) => Response.json({ ok: true, payer: payment?.payer, tx: payment?.transaction })); + const serverFetch: typeof fetch = (input, init) => app(new Request(input, init)); + const buyer = createRadiusFetch({ network: NET, signer: KEY!, maxPerRequest: '$0.01', fetch: serverFetch }); + + beforeAll(async () => { + const { atomic } = await buyer.balance(); + expect(atomic, `payer needs at least 0.001 SBC on ${NET}`).toBeGreaterThanOrEqual(1000n); + }); + + it('pays a lookup with real settlement; the handler and onSettled both see the receipt', async () => { + const res = await buyer('http://seller.test/api/lookup'); + expect(res.status).toBe(200); + const body = (await res.json()) as { ok: boolean; payer: string; tx: string }; + expect(body.ok).toBe(true); + expect(body.payer).toBe(payer.address); + expect(body.tx).toMatch(/^0x[0-9a-f]{64}$/); + const receipt = getPaymentReceipt(res, buyer.network)!; + expect(receipt).toMatchObject({ success: true, transaction: body.tx, network: network.network }); + expect(settled).toEqual([{ tx: body.tx, path: '/api/lookup' }]); + }, 60_000); +}); diff --git a/packages/sdk/test/server.test.ts b/packages/sdk/test/server.test.ts new file mode 100644 index 0000000..b1195cd --- /dev/null +++ b/packages/sdk/test/server.test.ts @@ -0,0 +1,294 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { decodePaymentRequiredHeader, encodePaymentSignatureHeader } from '@x402/core/http'; +import { createRadiusServer, radiusPayments, requestOf, type PaymentHandler } from '../src/server/index.js'; +import { getPaymentReceipt } from '../src/receipt.js'; + +const PAY_TO = '0x1eF420190c299D4d133fE9227F780D7d5cE91BeE'; + +function makeHandler(opts: Partial[0]> = {}) { + return radiusPayments({ + network: 'testnet', + payTo: PAY_TO, + facilitator: { live: false }, + routes: { + 'GET /api/lookup': { price: '$0.001', description: 'Lookup' }, + 'POST /api/query': '0.01', + 'GET /api/atomic': { price: { amount: '42' } }, + }, + ...opts, + }); +} + +/** A minimal app: free `/health`, paid `/api/*`, echoing the receipt the handler received. */ +function makeApp(pay: PaymentHandler) { + return pay.wrap((request, payment) => { + const { pathname } = new URL(request.url); + if (pathname === '/health') return Response.json({ ok: true }); + if (pathname === '/api/lookup') return Response.json({ data: 'secret', payment }); + if (pathname === '/api/fail') return Response.json({ error: 'nope' }, { status: 500 }); + if (pathname === '/api/throw') throw new Error('boom'); + return Response.json({ data: pathname }); + }); +} + +afterEach(() => vi.restoreAllMocks()); + +async function payloadFor(app: (r: Request) => Promise, url = 'http://seller.test/api/lookup') { + const challenge = await app(new Request(url)); + const pr = decodePaymentRequiredHeader(challenge.headers.get('payment-required')!); + const accepted = pr.accepts[0]; + const { paymentFlow: _pf, ...extra } = accepted.extra as Record; + return encodePaymentSignatureHeader({ + x402Version: 2, + resource: pr.resource, + accepted: { ...accepted, extra }, + payload: { signature: '0xsig', permit2Authorization: {} }, + }); +} + +function mockFacilitator(settle: { success: boolean; transaction?: string; errorReason?: string }) { + const calls: string[] = []; + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const url = String(input instanceof Request ? input.url : input); + calls.push(url); + if (url.endsWith('/verify')) return Response.json({ isValid: true, payer: '0xabc' }); + if (url.endsWith('/settle')) return Response.json({ success: settle.success, transaction: settle.transaction ?? '', network: 'eip155:72344', payer: '0xabc', errorReason: settle.errorReason }); + throw new Error(`unexpected fetch ${url}`); + }); + return calls; +} + +describe('radiusPayments (web-standard handler): 402 challenge', () => { + it('leaves unprotected routes alone and reports requiresPayment without I/O', async () => { + const pay = makeHandler(); + const res = await makeApp(pay)(new Request('http://seller.test/health')); + expect(res.status).toBe(200); + expect(pay.requiresPayment(new Request('http://seller.test/health'))).toBe(false); + expect(pay.requiresPayment(new Request('http://seller.test/api/lookup'))).toBe(true); + expect(pay.requiresPayment(new Request('http://seller.test/api/lookup', { method: 'POST' }))).toBe(false); + }); + + it('returns a standard x402 v2 challenge for SBC via Permit2 with gas sponsoring declared', async () => { + const res = await makeHandler()(new Request('http://seller.test/api/lookup?q=1'), () => Response.json({ leaked: true })); + expect(res.status).toBe(402); + expect(res.headers.get('content-type')).toContain('application/json'); + const pr = decodePaymentRequiredHeader(res.headers.get('payment-required')!); + expect(pr.x402Version).toBe(2); + expect(pr.resource.url).toBe('http://seller.test/api/lookup?q=1'); + expect(pr.resource.description).toBe('Lookup'); + expect(pr.accepts[0]).toMatchObject({ + scheme: 'exact', + network: 'eip155:72344', + amount: '1000', + asset: '0x33ad9e4BD16B69B5BFdED37D8B5D9fF9aba014Fb', + payTo: PAY_TO, + maxTimeoutSeconds: 300, + extra: { assetTransferMethod: 'permit2', name: 'Stable Coin', version: '1', paymentFlow: 'upfront' }, + }); + expect(pr.extensions).toHaveProperty('eip2612GasSponsoring'); + expect(await res.text()).not.toContain('leaked'); + }); + + it('supports shorthand and atomic prices', async () => { + const app = makeApp(makeHandler()); + const post = await app(new Request('http://seller.test/api/query', { method: 'POST' })); + expect(decodePaymentRequiredHeader(post.headers.get('payment-required')!).accepts[0].amount).toBe('10000'); + const atomic = await app(new Request('http://seller.test/api/atomic')); + expect(decodePaymentRequiredHeader(atomic.headers.get('payment-required')!).accepts[0].amount).toBe('42'); + }); + + it('resolves payTo and price dynamically from the Request', async () => { + const pay = radiusPayments({ + network: 'testnet', + facilitator: { live: false }, + payTo: (request) => request.headers.get('x-pay-to') as `0x${string}`, + routes: { 'GET /p': { price: (request) => `$${new URL(request.url).searchParams.get('n')}` } }, + }); + const res = await pay(new Request('http://seller.test/p?n=2', { headers: { 'x-pay-to': PAY_TO } }), () => new Response('x')); + const accepted = decodePaymentRequiredHeader(res.headers.get('payment-required')!).accepts[0]; + expect(accepted.payTo).toBe(PAY_TO); + expect(accepted.amount).toBe('2000000'); + }); + + it('serves browsers an HTML paywall page', async () => { + const res = await makeHandler()(new Request('http://seller.test/api/lookup', { headers: { accept: 'text/html', 'user-agent': 'Mozilla/5.0' } }), () => new Response('x')); + expect(res.status).toBe(402); + expect(res.headers.get('content-type')).toContain('text/html'); + }); +}); + +describe('radiusPayments (web-standard handler): paid flow (facilitator mocked)', () => { + it('settles before the handler, passes the receipt to it, and attaches PAYMENT-RESPONSE', async () => { + const calls = mockFacilitator({ success: true, transaction: '0xdeadbeef' }); + const settled: { receipt: unknown; url: string }[] = []; + const app = makeApp(makeHandler({ onSettled: (receipt, request) => { settled.push({ receipt, url: request.url }); } })); + const sig = await payloadFor(app); + const res = await app(new Request('http://seller.test/api/lookup', { headers: { 'PAYMENT-SIGNATURE': sig } })); + expect(res.status).toBe(200); + const body = (await res.json()) as { data: string; payment: { transaction: string; amount: string } }; + expect(body.data).toBe('secret'); + expect(body.payment).toMatchObject({ success: true, transaction: '0xdeadbeef', amount: '1000', payer: '0xabc' }); + expect(calls.filter((u) => u.endsWith('/verify'))).toHaveLength(0); + expect(calls.filter((u) => u.endsWith('/settle'))).toHaveLength(1); + expect(getPaymentReceipt(res)).toMatchObject({ success: true, transaction: '0xdeadbeef', network: 'eip155:72344' }); + expect(res.headers.get('cache-control')).toContain('private'); + expect(settled).toEqual([{ receipt: expect.objectContaining({ transaction: '0xdeadbeef' }), url: 'http://seller.test/api/lookup' }]); + }); + + it('settle: "after" runs the handler first, then settles; the handler sees no receipt', async () => { + const calls = mockFacilitator({ success: true, transaction: '0xafter' }); + const seen: unknown[] = []; + const app = makeHandler({ settle: 'after' }).wrap((_r, payment) => { + seen.push(payment); + return Response.json({ ok: true }); + }); + const sig = await payloadFor(app); + const res = await app(new Request('http://seller.test/api/lookup', { headers: { 'PAYMENT-SIGNATURE': sig } })); + expect(res.status).toBe(200); + expect(seen).toEqual([undefined]); + expect(calls.filter((u) => u.endsWith('/verify'))).toHaveLength(1); + expect(calls.filter((u) => u.endsWith('/settle'))).toHaveLength(1); + expect(getPaymentReceipt(res)?.transaction).toBe('0xafter'); + }); + + it('withholds the resource when settlement fails and does not call onSettled', async () => { + mockFacilitator({ success: false, errorReason: 'insufficient_funds' }); + const settled: unknown[] = []; + const app = makeApp(makeHandler({ onSettled: (r) => { settled.push(r); } })); + const sig = await payloadFor(app); + const res = await app(new Request('http://seller.test/api/lookup', { headers: { 'PAYMENT-SIGNATURE': sig } })); + expect(res.status).toBe(402); + expect(await res.text()).not.toContain('secret'); + expect(settled).toHaveLength(0); + }); + + it('rejects a malformed payment header without calling the facilitator', async () => { + const calls = mockFacilitator({ success: true }); + const res = await makeApp(makeHandler())(new Request('http://seller.test/api/lookup', { headers: { 'PAYMENT-SIGNATURE': 'not-base64-json' } })); + expect(res.status).toBe(402); + expect(calls).toHaveLength(0); + }); + + it('handles responses with immutable headers (e.g. proxied from fetch)', async () => { + mockFacilitator({ success: true, transaction: '0x1' }); + const pay = makeHandler(); + const app = pay.wrap(async () => { + const upstream = new Response('proxied', { headers: { 'x-upstream': '1' } }); + // Simulate `fetch()`'s immutable headers guard. + Object.defineProperty(upstream, 'headers', { value: new Proxy(upstream.headers, { get: (t, k) => (k === 'set' || k === 'delete' ? () => { throw new TypeError('immutable'); } : Reflect.get(t, k).bind(t)) }) }); + return upstream; + }); + const sig = await payloadFor(app); + const res = await app(new Request('http://seller.test/api/lookup', { headers: { 'PAYMENT-SIGNATURE': sig } })); + expect(res.status).toBe(200); + expect(await res.text()).toBe('proxied'); + expect(res.headers.get('x-upstream')).toBe('1'); + expect(getPaymentReceipt(res)?.transaction).toBe('0x1'); + }); + + it('returns the handler error with the settlement receipt when a paid handler fails', async () => { + mockFacilitator({ success: true, transaction: '0x2' }); + const app = makeApp(makeHandler({ routes: { 'GET /api/fail': '$0.001', 'GET /api/throw': '$0.001' } })); + const sig = await payloadFor(app, 'http://seller.test/api/fail'); + const failed = await app(new Request('http://seller.test/api/fail', { headers: { 'PAYMENT-SIGNATURE': sig } })); + expect(failed.status).toBe(500); + expect(getPaymentReceipt(failed)?.transaction).toBe('0x2'); + const sig2 = await payloadFor(app, 'http://seller.test/api/throw'); + const threw = await app(new Request('http://seller.test/api/throw', { headers: { 'PAYMENT-SIGNATURE': sig2 } })); + expect(threw.status).toBe(500); + expect(await threw.json()).toMatchObject({ error: 'payment_processing_error', message: 'boom' }); + expect(getPaymentReceipt(threw)?.transaction).toBe('0x2'); + }); +}); + +describe('radiusPayments (web-standard handler) when the facilitator gives no answer', () => { + function mockFacilitator(respond: (path: 'verify' | 'settle') => Response | Promise) { + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const url = String(input instanceof Request ? input.url : input); + if (url.endsWith('/verify')) return respond('verify'); + if (url.endsWith('/settle')) return respond('settle'); + throw new Error(`unexpected fetch ${url}`); + }); + } + const gatewayTimeout = () => new Response('504 Gateway Time-out', { status: 504, headers: { 'content-type': 'text/html' } }); + const verified = () => Response.json({ isValid: true, payer: '0xabc' }); + + async function pay(app: (r: Request) => Promise) { + const sig = await payloadFor(app); + return app(new Request('http://seller.test/api/lookup', { headers: { 'PAYMENT-SIGNATURE': sig } })); + } + + it('answers 502 when settle returns an error page', async () => { + mockFacilitator(gatewayTimeout); + const res = await pay(makeApp(makeHandler())); + expect(res.status).toBe(502); + expect(await res.json()).toMatchObject({ error: 'facilitator_error' }); + }); + + it('answers 502 when the settle connection fails', async () => { + mockFacilitator(() => { + throw new TypeError('fetch failed'); + }); + const res = await pay(makeApp(makeHandler())); + expect(res.status).toBe(502); + expect(await res.text()).not.toContain('secret'); + }); + + it('answers 502 when settle fails after the handler (settle: "after")', async () => { + mockFacilitator((path) => (path === 'verify' ? verified() : gatewayTimeout())); + const res = await pay(makeApp(makeHandler({ settle: 'after' }))); + expect(res.status).toBe(502); + expect(await res.text()).not.toContain('secret'); + }); + + it('answers 502 when verify returns an error page (settle: "after")', async () => { + mockFacilitator(gatewayTimeout); + const res = await pay(makeApp(makeHandler({ settle: 'after' }))); + expect(res.status).toBe(502); + }); + + it("keeps 402 for the facilitator's own settle rejection", async () => { + mockFacilitator(() => Response.json({ success: false, errorReason: 'insufficient_funds', transaction: '', network: 'eip155:72344' }, { status: 400 })); + const res = await pay(makeApp(makeHandler())); + expect(res.status).toBe(402); + }); +}); + +describe('createRadiusServer', () => { + it('builds upstream-compatible routes and a resource server that answers 402s through @x402/express', async () => { + const { paymentMiddleware } = await import('@x402/express'); + const express = (await import('express')).default; + const seen: string[] = []; + const radius = createRadiusServer({ network: 'testnet', facilitator: { live: false }, onSettled: (r, ctx) => { seen.push(`${r.transaction}:${requestOf(ctx) ? 'request' : 'express'}`); } }); + const app = express(); + app.use(paymentMiddleware(radius.routes({ payTo: PAY_TO, routes: { 'GET /api/lookup': '$0.001' } }), radius.server)); + app.get('/api/lookup', (_req, res) => { res.json({ data: 'secret' }); }); + const server = app.listen(0); + const port = (server.address() as { port: number }).port; + const base = `http://127.0.0.1:${port}`; + try { + const challenge = await fetch(`${base}/api/lookup`); + expect(challenge.status).toBe(402); + const pr = decodePaymentRequiredHeader(challenge.headers.get('payment-required')!); + expect(pr.accepts[0]).toMatchObject({ scheme: 'exact', network: 'eip155:72344', amount: '1000', payTo: PAY_TO, extra: { assetTransferMethod: 'permit2', paymentFlow: 'upfront' } }); + expect(pr.extensions).toHaveProperty('eip2612GasSponsoring'); + + const realFetch = globalThis.fetch; + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { + const url = String(input instanceof Request ? input.url : input); + if (url.endsWith('/settle')) return Response.json({ success: true, transaction: '0xexpress', network: 'eip155:72344', payer: '0xabc' }); + return realFetch(input, init); + }); + const accepted = pr.accepts[0]; + const { paymentFlow: _pf, ...extra } = accepted.extra as Record; + const sig = encodePaymentSignatureHeader({ x402Version: 2, resource: pr.resource, accepted: { ...accepted, extra }, payload: { signature: '0xsig', permit2Authorization: {} } }); + const paid = await fetch(`${base}/api/lookup`, { headers: { 'PAYMENT-SIGNATURE': sig } }); + expect(paid.status).toBe(200); + expect(await paid.json()).toEqual({ data: 'secret' }); + expect(getPaymentReceipt(paid)?.transaction).toBe('0xexpress'); + expect(seen).toEqual(['0xexpress:express']); + } finally { + server.close(); + } + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6dcd1ce..66c8945 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,11 +43,11 @@ importers: packages/sdk: dependencies: '@x402/core': - specifier: 2.25.0 - version: 2.25.0 + specifier: 2.27.0 + version: 2.27.0 '@x402/evm': - specifier: 2.25.0 - version: 2.25.0(typescript@5.9.3) + specifier: 2.27.0 + version: 2.27.0(typescript@5.9.3) devDependencies: '@cloudflare/workers-types': specifier: ^5.20260910.1 @@ -61,6 +61,15 @@ importers: '@ethereumjs/util': specifier: 10.1.3 version: 10.1.3 + '@types/express': + specifier: ^5.0.6 + version: 5.0.6 + '@x402/express': + specifier: 2.27.0 + version: 2.27.0(ethers@6.17.0)(express@5.2.1)(typescript@5.9.3) + express: + specifier: ^5.2.1 + version: 5.2.1 hono: specifier: ^4.13.7 version: 4.13.8 @@ -72,10 +81,10 @@ importers: version: 2.56.5(typescript@5.9.3)(zod@3.25.76) vitest: specifier: ^3.2.0 - version: 3.2.7(@types/node@22.20.3) + version: 3.2.7(@types/node@24.19.0) wrangler: specifier: ^4.131.0 - version: 4.132.0(@cloudflare/workers-types@5.20260915.1)(@types/node@22.20.3) + version: 4.132.0(@cloudflare/workers-types@5.20260915.1)(@types/node@24.19.0) packages/sdk/examples/agent-buyer: dependencies: @@ -112,7 +121,45 @@ importers: version: 5.9.3 wrangler: specifier: ^4.131.0 - version: 4.132.0(@cloudflare/workers-types@5.20260915.1)(@types/node@22.20.3) + version: 4.132.0(@cloudflare/workers-types@5.20260915.1)(@types/node@24.19.0) + + packages/sdk/examples/express-seller: + dependencies: + '@x402/express': + specifier: 2.27.0 + version: 2.27.0(ethers@6.17.0)(express@5.2.1)(typescript@5.9.3) + express: + specifier: ^5.2.1 + version: 5.2.1 + radius-sdk: + specifier: workspace:* + version: link:../.. + devDependencies: + '@types/express': + specifier: ^5.0.6 + version: 5.0.6 + '@types/node': + specifier: ^24.0.0 + version: 24.19.0 + typescript: + specifier: ^5.9.0 + version: 5.9.3 + + packages/sdk/examples/worker-plain: + dependencies: + radius-sdk: + specifier: workspace:* + version: link:../.. + devDependencies: + '@cloudflare/workers-types': + specifier: ^5.20260910.1 + version: 5.20260915.1 + typescript: + specifier: ^5.9.0 + version: 5.9.3 + wrangler: + specifier: ^4.131.0 + version: 4.132.0(@cloudflare/workers-types@5.20260915.1)(@types/node@24.19.0) packages/sdk/examples/worker-seller: dependencies: @@ -131,7 +178,7 @@ importers: version: 5.9.3 wrangler: specifier: ^4.131.0 - version: 4.132.0(@cloudflare/workers-types@5.20260915.1)(@types/node@22.20.3) + version: 4.132.0(@cloudflare/workers-types@5.20260915.1)(@types/node@24.19.0) packages: @@ -1252,6 +1299,20 @@ packages: '@scure/bip39@1.6.0': resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@signinwithethereum/siwe-parser@4.2.1': + resolution: {integrity: sha512-GAdwNiBaRpUMTiFfPrbQ3e/RBhPbEhRRN1sZmDMLgLEZuKqKwO7V0ksVzCerE9l8HFKTNw5R/q3kPDCWJRz4zA==} + + '@signinwithethereum/siwe@4.2.1': + resolution: {integrity: sha512-CQEQhCIgN9blil1YpaGR+wvGPdsBVK6zed5DBu03s1IdbSO3kA/n7Zp+SAkZGiHy3HY6OJm1Su0/Bd33vKK6nQ==} + peerDependencies: + ethers: ^5.7.0 || ^6.13.0 + viem: ^2.7.0 + peerDependenciesMeta: + ethers: + optional: true + viem: + optional: true + '@sindresorhus/is@7.2.0': resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} @@ -1259,21 +1320,51 @@ packages: '@speed-highlight/core@1.2.24': resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/express-serve-static-core@5.1.3': + resolution: {integrity: sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/node@22.20.3': resolution: {integrity: sha512-DZmzkmwHzXrLPAXPyKNDzlIwMMUZCVacoD25ywdy5YTKGbOx/2ld+Q38Im2zJ0vBuZP5Prd3VZutKZyXwkOS8A==} '@types/node@22.7.5': resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/node@24.19.0': + resolution: {integrity: sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==} + + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -1332,11 +1423,23 @@ packages: '@vitest/utils@3.2.7': resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} - '@x402/core@2.25.0': - resolution: {integrity: sha512-5Ys0XYz3FKutxVKoXC46R/XPT/oaAbuj7ahzrlVHwQxZJPH9u6u91IOh0ztz+7G/zYGsaXR8l3ety205QtEnUw==} + '@x402/core@2.27.0': + resolution: {integrity: sha512-UbxCKnoTsg5nQX9fYER3g9tt5l80jHm1IZOmDczQRqE0cwskls5r+VAAj0iUQfHVshnpv0qo/rLFI7UntqL96g==} + + '@x402/evm@2.27.0': + resolution: {integrity: sha512-WKVFq5VaSrDJfo6lHNG7/4hT0iZdpsmd53W/a+gZqmE1CiVT8wyRpwIPue4f/YKqNfgHHhZ00AcfPho0xLr2eQ==} + + '@x402/express@2.27.0': + resolution: {integrity: sha512-73eGihL7BLHrFOH48pJdVPhHTvzRaFKcimzcRYR/0+VVfPLTfYyLwxCu414aoDokKGTU5u8AIYwf2LpcpaYX7w==} + peerDependencies: + '@x402/paywall': ^2.27.0 + express: ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + '@x402/paywall': + optional: true - '@x402/evm@2.25.0': - resolution: {integrity: sha512-EmnL4MyGW8weEUlq0qC1uJUeoBmtp9GWGwyPl+qkC0CYItRHn748puhYAxK8GsrofscQrt63HsFCB0Gp3iEROA==} + '@x402/extensions@2.27.0': + resolution: {integrity: sha512-eGBZZCTNtJsbzYoOe72Mfo/HN7Ir4i8+gmBUQ3ScrTiO91Q0bscO6LjGg2d3oJiFQ6g+YVXn+5fLxCEwoqgFsA==} abitype@1.2.3: resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} @@ -1349,9 +1452,16 @@ packages: zod: optional: true + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + aes-js@4.0.0-beta.5: resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1360,6 +1470,9 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + apg-js@4.4.0: + resolution: {integrity: sha512-fefmXFknJmtgtNEXfPwZKYkMFX4Fyeyz+fNF6JWp87biGOPslJbCBVU158zvKRZfHBKnJDy8CMM40oLFGkXT8Q==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1367,6 +1480,14 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1375,6 +1496,14 @@ packages: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1401,6 +1530,26 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} + engines: {node: '>=18'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -1418,19 +1567,46 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -1446,9 +1622,16 @@ packages: engines: {node: '>=18'} hasBin: true + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + ethers@6.17.0: resolution: {integrity: sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==} engines: {node: '>=14.0.0'} @@ -1460,12 +1643,22 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-string-truncated-width@3.0.3: resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} + fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -1478,15 +1671,54 @@ packages: picomatch: optional: true + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hono@4.13.8: resolution: {integrity: sha512-/Gng7NfoykZl2pjukW5Z6+8Yxm3BPRf86GTbQnt0SbySkvax4fyL4H3HhY1cCpBGmiW9XDRFzRV+CXK2W8QudQ==} engines: {node: '>=16.9.0'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + human-id@4.2.1: resolution: {integrity: sha512-zPGsiS+dWoTZtZ4AtpA9Y+BdSFSNWvnouNlWNoUFyAM6xHOHmdCvqO3k8AIbdamCOv4gUFUVNPf6rJFfc4UiJw==} hasBin: true @@ -1498,10 +1730,20 @@ packages: import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + isows@1.0.7: resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} peerDependencies: @@ -1510,9 +1752,15 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} @@ -1533,6 +1781,26 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + miniflare@5.20260915.0-alpha: resolution: {integrity: sha512-ApLsq9X2jbpkLAPXx89afGvuNxAFzd1rUK+nzfBSLu+34YWc2Q2YlgH6f3kSEEQYZ2gAlC5Q5IP6tDNNg/1KqQ==} engines: {node: '>=22.0.0'} @@ -1549,6 +1817,21 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@1.1.0: + resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} + engines: {node: '>=18'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + ox@0.14.44: resolution: {integrity: sha512-O54qXXHEk4ySamMSyBpI0AeBegS5frxU6+LA6Jb9Sf6kMOxcTNaxYmwZfpOu22DIQsdKfgQxHq8EsAGaoBQScA==} peerDependencies: @@ -1560,9 +1843,16 @@ packages: package-manager-detector@1.8.0: resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -1584,11 +1874,35 @@ packages: resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} + proxy-addr@2.0.8: + resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} + engines: {node: '>= 0.10'} + + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} + engines: {node: '>=0.6'} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + rollup@4.63.3: resolution: {integrity: sha512-1i2XreiAoMMXuPGD6Msj2xWrMMkHojNRKivInxGQcg7/1KuPuYlfUutLyh4drnOxUTHX9cHI4wFoat8D/NKaBw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1597,6 +1911,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.35.4: resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} engines: {node: '>=20.9.0'} @@ -1610,6 +1935,22 @@ packages: resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} engines: {node: '>= 0.4'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1627,6 +1968,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -1679,9 +2024,20 @@ packages: resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} engines: {node: '>=14.0.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tslib@2.7.0: resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1693,6 +2049,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + undici@7.29.0: resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} @@ -1700,6 +2059,14 @@ packages: unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + viem@2.56.5: resolution: {integrity: sha512-GuEf/oee0PHgy4D6JIA8u0rY1g+sM5nF+p7HyMHO3vjlHC5G/ljfrgnUOnrES9m5Ny/P3RwWCHjDuN2T6HUWzQ==} peerDependencies: @@ -1826,6 +2193,9 @@ packages: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -2676,19 +3046,55 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 + '@signinwithethereum/siwe-parser@4.2.1': + dependencies: + '@noble/hashes': 1.8.0 + apg-js: 4.4.0 + + '@signinwithethereum/siwe@4.2.1(ethers@6.17.0)(viem@2.56.5(typescript@5.9.3)(zod@3.25.76))': + dependencies: + '@signinwithethereum/siwe-parser': 4.2.1 + optionalDependencies: + ethers: 6.17.0 + viem: 2.56.5(typescript@5.9.3)(zod@3.25.76) + '@sindresorhus/is@7.2.0': {} '@speed-highlight/core@1.2.24': {} + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 24.19.0 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.19.0 + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.9': {} + '@types/express-serve-static-core@5.1.3': + dependencies: + '@types/node': 24.19.0 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.3 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + '@types/node@22.20.3': dependencies: undici-types: 6.21.0 @@ -2697,6 +3103,23 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/node@24.19.0': + dependencies: + undici-types: 7.24.6 + + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@1.2.1': + dependencies: + '@types/node': 24.19.0 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.19.0 + '@vitest/expect@2.1.9': dependencies: '@vitest/spy': 2.1.9 @@ -2720,13 +3143,13 @@ snapshots: optionalDependencies: vite: 5.4.21(@types/node@22.20.3) - '@vitest/mocker@3.2.7(vite@5.4.21(@types/node@22.20.3))': + '@vitest/mocker@3.2.7(vite@5.4.21(@types/node@24.19.0))': dependencies: '@vitest/spy': 3.2.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 5.4.21(@types/node@22.20.3) + vite: 5.4.21(@types/node@24.19.0) '@vitest/pretty-format@2.1.9': dependencies: @@ -2779,13 +3202,13 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@x402/core@2.25.0': + '@x402/core@2.27.0': dependencies: zod: 3.25.76 - '@x402/evm@2.25.0(typescript@5.9.3)': + '@x402/evm@2.27.0(typescript@5.9.3)': dependencies: - '@x402/core': 2.25.0 + '@x402/core': 2.27.0 viem: 2.56.5(typescript@5.9.3)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: @@ -2793,27 +3216,95 @@ snapshots: - typescript - utf-8-validate + '@x402/express@2.27.0(ethers@6.17.0)(express@5.2.1)(typescript@5.9.3)': + dependencies: + '@x402/core': 2.27.0 + '@x402/extensions': 2.27.0(ethers@6.17.0)(typescript@5.9.3) + express: 5.2.1 + transitivePeerDependencies: + - bufferutil + - ethers + - typescript + - utf-8-validate + + '@x402/extensions@2.27.0(ethers@6.17.0)(typescript@5.9.3)': + dependencies: + '@noble/curves': 1.9.1 + '@scure/base': 1.2.6 + '@signinwithethereum/siwe': 4.2.1(ethers@6.17.0)(viem@2.56.5(typescript@5.9.3)(zod@3.25.76)) + '@x402/core': 2.27.0 + ajv: 8.20.0 + jose: 5.10.0 + tweetnacl: 1.0.3 + viem: 2.56.5(typescript@5.9.3)(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - bufferutil + - ethers + - typescript + - utf-8-validate + abitype@1.2.3(typescript@5.9.3)(zod@3.25.76): optionalDependencies: typescript: 5.9.3 zod: 3.25.76 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.1.0 + aes-js@4.0.0-beta.5: {} + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.8 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-regex@5.0.1: {} ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 + apg-js@4.4.0: {} + assertion-error@2.0.1: {} blake3-wasm@2.1.5: {} + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.1.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.16.0 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + cac@6.7.14: {} cac@7.0.0: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -2836,6 +3327,16 @@ snapshots: commander@12.1.0: {} + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.1.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + cookie@1.1.1: {} debug@4.4.3: @@ -2844,14 +3345,34 @@ snapshots: deep-eql@5.0.2: {} + depd@2.0.0: {} + detect-libc@2.1.2: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + emoji-regex@8.0.0: {} + encodeurl@2.0.0: {} + error-stack-parser-es@1.0.5: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -2936,10 +3457,14 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + escape-html@1.0.3: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 + etag@1.8.1: {} + ethers@6.17.0: dependencies: '@adraffy/ens-normalize': 1.11.1 @@ -2957,12 +3482,49 @@ snapshots: expect-type@1.4.0: {} + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.8 + qs: 6.16.0 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + fast-string-truncated-width@3.0.3: {} fast-string-width@3.0.2: dependencies: fast-string-truncated-width: 3.0.3 + fast-uri@3.1.8: {} + fast-wrap-ansi@0.2.2: dependencies: fast-string-width: 3.0.2 @@ -2971,11 +3533,62 @@ snapshots: optionalDependencies: picomatch: 4.0.7 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + hono@4.13.8: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + human-id@4.2.1: {} iconv-lite@0.7.3: @@ -2984,16 +3597,26 @@ snapshots: import-meta-resolve@4.2.0: {} + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + is-fullwidth-code-point@3.0.0: {} + is-promise@4.0.0: {} + isows@1.0.7(ws@8.21.0): dependencies: ws: 8.21.0 jju@1.4.0: {} + jose@5.10.0: {} + js-tokens@9.0.1: {} + json-schema-traverse@1.0.0: {} + jsonc-parser@3.3.1: {} kleur@4.1.5: {} @@ -3011,10 +3634,22 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 - miniflare@5.20260915.0-alpha(@types/node@22.20.3): + math-intrinsics@1.1.0: {} + + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + miniflare@5.20260915.0-alpha(@types/node@24.19.0): dependencies: '@cspotcode/source-map-support': 0.8.1 - sharp: 0.35.4(@types/node@22.20.3) + sharp: 0.35.4(@types/node@24.19.0) undici: 7.29.0 workerd: 1.20260915.1 ws: 8.21.0 @@ -3030,6 +3665,20 @@ snapshots: nanoid@3.3.19: {} + negotiator@1.1.0: + dependencies: + content-type: 2.1.0 + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + ox@0.14.44(typescript@5.9.3)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.11.1 @@ -3047,8 +3696,12 @@ snapshots: package-manager-detector@1.8.0: {} + parseurl@1.3.3: {} + path-to-regexp@6.3.0: {} + path-to-regexp@8.4.2: {} + pathe@1.1.2: {} pathe@2.0.3: {} @@ -3065,6 +3718,27 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + proxy-addr@2.0.8: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.16.0: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + + require-from-string@2.0.2: {} + rollup@4.63.3: dependencies: '@types/estree': 1.0.9 @@ -3097,11 +3771,48 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.63.3 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + safer-buffer@2.1.2: {} semver@7.8.5: {} - sharp@0.35.4(@types/node@22.20.3): + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + sharp@0.35.4(@types/node@24.19.0): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 @@ -3132,10 +3843,38 @@ snapshots: '@img/sharp-win32-arm64': 0.35.4 '@img/sharp-win32-ia32': 0.35.4 '@img/sharp-win32-x64': 0.35.4 - '@types/node': 22.20.3 + '@types/node': 24.19.0 shell-quote@1.10.0: {} + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -3146,6 +3885,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} string-width@4.2.3: @@ -3185,20 +3926,36 @@ snapshots: tinyspy@4.0.6: {} + toidentifier@1.0.1: {} + tslib@2.7.0: {} + tweetnacl@1.0.3: {} + + type-is@2.1.0: + dependencies: + content-type: 2.1.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + typescript@5.9.3: {} undici-types@6.19.8: {} undici-types@6.21.0: {} + undici-types@7.24.6: {} + undici@7.29.0: {} unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 + unpipe@1.0.0: {} + + vary@1.1.2: {} + viem@2.56.5(typescript@5.9.3)(zod@3.25.76): dependencies: '@noble/curves': 1.9.1 @@ -3234,13 +3991,13 @@ snapshots: - supports-color - terser - vite-node@3.2.4(@types/node@22.20.3): + vite-node@3.2.4(@types/node@24.19.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.21(@types/node@22.20.3) + vite: 5.4.21(@types/node@24.19.0) transitivePeerDependencies: - '@types/node' - less @@ -3261,6 +4018,15 @@ snapshots: '@types/node': 22.20.3 fsevents: 2.3.3 + vite@5.4.21(@types/node@24.19.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.28 + rollup: 4.63.3 + optionalDependencies: + '@types/node': 24.19.0 + fsevents: 2.3.3 + vitest@2.1.9(@types/node@22.20.3): dependencies: '@vitest/expect': 2.1.9 @@ -3296,11 +4062,11 @@ snapshots: - supports-color - terser - vitest@3.2.7(@types/node@22.20.3): + vitest@3.2.7(@types/node@24.19.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 - '@vitest/mocker': 3.2.7(vite@5.4.21(@types/node@22.20.3)) + '@vitest/mocker': 3.2.7(vite@5.4.21(@types/node@24.19.0)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.7 '@vitest/snapshot': 3.2.7 @@ -3318,11 +4084,11 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.21(@types/node@22.20.3) - vite-node: 3.2.4(@types/node@22.20.3) + vite: 5.4.21(@types/node@24.19.0) + vite-node: 3.2.4(@types/node@24.19.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.20.3 + '@types/node': 24.19.0 transitivePeerDependencies: - less - lightningcss @@ -3347,13 +4113,13 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260915.1 '@cloudflare/workerd-windows-64': 1.20260915.1 - wrangler@4.132.0(@cloudflare/workers-types@5.20260915.1)(@types/node@22.20.3): + wrangler@4.132.0(@cloudflare/workers-types@5.20260915.1)(@types/node@24.19.0): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260915.1) blake3-wasm: 2.1.5 esbuild: 0.28.1 - miniflare: 5.20260915.0-alpha(@types/node@22.20.3) + miniflare: 5.20260915.0-alpha(@types/node@24.19.0) path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 workerd: 1.20260915.1 @@ -3371,6 +4137,8 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrappy@1.0.2: {} + ws@8.21.0: {} yaml@2.9.1: {}