diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d992b0f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +.git \ No newline at end of file diff --git a/.github/workflows/deploy-server-og-evm.yml b/.github/workflows/deploy-server-og-evm.yml new file mode 100644 index 0000000..882104a --- /dev/null +++ b/.github/workflows/deploy-server-og-evm.yml @@ -0,0 +1,55 @@ +name: Deploy to AWS + +on: + workflow_dispatch: + +jobs: + build-and-deploy: + name: Build and Deploy + runs-on: ubuntu-latest + environment: production + + env: + ECS_CLUSTER: MemChat + ECS_SERVICE: memchat-facilitator-x402-og-evm + ECR_REPOSITORY: memchat/facilitator-x402-og-evm + IMAGE_TAG: latest + AWS_REGION: us-east-2 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, tag, and push image to Amazon ECR + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + echo "Docker image successfully pushed to ECR!" + + - name: Force new deployment on Amazon ECS + run: | + aws ecs update-service --cluster $ECS_CLUSTER --service $ECS_SERVICE --force-new-deployment > /dev/null + echo "New ECS deployment started!" + + - name: AWS LINKS TO VIEW DEPLOYMENT + run: | + echo "View deployment progress here - https://us-east-2.console.aws.amazon.com/ecs/v2/clusters/MemChat/services/facilitator-x402-og-evm/tasks?region=us-east-2" + + - name: Wait for ECS service to stabilize + run: | + echo "Waiting for ECS service to stabilize..." + aws ecs wait services-stable --cluster $ECS_CLUSTER --services $ECS_SERVICE + echo "✅ ECS service is now stable and deployment is complete!" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3109ac0..2c112b2 100644 --- a/.gitignore +++ b/.gitignore @@ -141,4 +141,8 @@ vite.config.ts.timestamp-* .vite/ # walrus config -walrus_config \ No newline at end of file +walrus_config + +.DS_Store + +*.turbo/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f6065c7..308c687 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,49 +1,61 @@ +# Build stage FROM node:20-alpine AS builder ARG PNPM_VERSION=10.7.0 - - RUN npm install -g pnpm@${PNPM_VERSION} && \ apk add --no-cache python3 make g++ WORKDIR /app -ARG NODE_OPTIONS=--max-old-space-size=3048 -ARG NODE_ENV=production +# Copy root workspace files +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +# Copy all package.json files to optimize layer caching for pnpm install COPY typescript/package.json ./typescript/ -COPY typescript/packages/x402/package.json ./typescript/packages/x402/ - +COPY typescript/packages/core/package.json ./typescript/packages/core/ +COPY typescript/packages/extensions/package.json ./typescript/packages/extensions/ +COPY typescript/packages/mcp/package.json ./typescript/packages/mcp/ +COPY typescript/packages/mechanisms/evm/package.json ./typescript/packages/mechanisms/evm/ +COPY typescript/packages/mechanisms/svm/package.json ./typescript/packages/mechanisms/svm/ +COPY typescript/packages/http/next/package.json ./typescript/packages/http/next/ +COPY typescript/packages/http/express/package.json ./typescript/packages/http/express/ +COPY typescript/packages/http/fetch/package.json ./typescript/packages/http/fetch/ +COPY typescript/packages/http/hono/package.json ./typescript/packages/http/hono/ +COPY typescript/packages/http/axios/package.json ./typescript/packages/http/axios/ +COPY typescript/packages/http/paywall/package.json ./typescript/packages/http/paywall/ + +# Install dependencies RUN pnpm install --frozen-lockfile +# Copy source code COPY . . -WORKDIR /app/typescript/packages/x402 -RUN pnpm build - -WORKDIR /app -RUN pnpm build - -FROM node:20-alpine +# Build all packages +# We assume 'pnpm build' at root builds all workspace packages +RUN pnpm --filter @x402/core build && \ + pnpm --filter @x402/evm build && \ + pnpm --filter @x402/svm build && \ + pnpm --filter @x402/extensions build && \ + pnpm build -ARG PNPM_VERSION=10.7.0 +# Remove development dependencies +RUN pnpm prune --prod -RUN npm install -g pnpm@${PNPM_VERSION} +# Production stage +FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production -COPY --from=builder /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml ./ -COPY --from=builder /app/typescript/package.json ./typescript/ -COPY --from=builder /app/typescript/packages/x402/package.json ./typescript/packages/x402/ - -RUN pnpm install --prod --frozen-lockfile - +# Copy built files and necessary node_modules +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist -COPY --from=builder /app/typescript/packages/x402/dist ./typescript/packages/x402/dist +# Include workspace packages if needed at runtime +COPY --from=builder /app/typescript/packages ./typescript/packages EXPOSE 3002 -CMD ["pnpm", "start"] +CMD ["node", "dist/all_networks.js"] + diff --git a/all_networks.ts b/all_networks.ts new file mode 100644 index 0000000..8f97ef1 --- /dev/null +++ b/all_networks.ts @@ -0,0 +1,589 @@ +import { base58 } from "@scure/base"; +import { createKeyPairSignerFromBytes } from "@solana/kit"; +import { x402Facilitator } from "@x402/core/facilitator"; +import { + PaymentPayload, + PaymentRequirements, + SettleResponse, + VerifyResponse, +} from "@x402/core/types"; +import { toFacilitatorEvmSigner } from "@x402/evm"; +import { ExactEvmScheme } from "@x402/evm/exact/facilitator"; +import { UptoEvmScheme } from "@x402/evm/upto/facilitator"; +import { toFacilitatorSvmSigner } from "@x402/svm"; +import { ExactSvmScheme } from "@x402/svm/exact/facilitator"; +import dotenv from "dotenv"; +import express from "express"; +import { randomUUID } from "node:crypto"; +import { type Server } from "node:http"; +import { createClient } from "redis"; +import { createWalletClient, http, publicActions, parseGwei } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { defineChain } from "viem"; +import { baseSepolia as defaultBaseSepolia } from "viem/chains"; + +const ogEvm = defineChain({ + id: 10740, + name: 'OG EVM', + nativeCurrency: { + decimals: 18, + name: 'OG', + symbol: 'OG', + }, + rpcUrls: { + default: { http: ['https://ogevmdevnet.opengradient.ai/'] }, + }, + blockExplorers: { + default: { + name: 'OG EVM Explorer', + url: 'https://explorer.og.artela.io', // TODO: update + }, + }, + contracts: { + multicall3: { + address: '0x4200000000000000000000000000000000000006', + blockCreated: 1, + }, + }, +}) + +dotenv.config(); + +// Configuration +const PORT = process.env.PORT || "4022"; +const REDIS_URL = process.env.REDIS_URL || "redis://127.0.0.1:6379"; +const BASE_SEPOLIA_RPC_URL = + process.env.BASE_SEPOLIA_RPC_URL || defaultBaseSepolia.rpcUrls.default.http[0]; +const SETTLE_QUEUE_KEY = process.env.SETTLE_QUEUE_KEY || "x402:settle:queue"; +const SETTLE_JOB_KEY_PREFIX = process.env.SETTLE_JOB_KEY_PREFIX || "x402:settle:job:"; +const SETTLE_JOB_TTL_SECONDS = Number(process.env.SETTLE_JOB_TTL_SECONDS || 60 * 60 * 24); +const SETTLE_WORKER_POLL_SECONDS = Number(process.env.SETTLE_WORKER_POLL_SECONDS || 1); +const SHUTDOWN_TIMEOUT_MS = Number(process.env.SHUTDOWN_TIMEOUT_MS || 10_000); + +const baseSepolia = defineChain({ + ...defaultBaseSepolia, + rpcUrls: { + ...defaultBaseSepolia.rpcUrls, + default: { http: [BASE_SEPOLIA_RPC_URL] }, + public: { http: [BASE_SEPOLIA_RPC_URL] }, + }, +}); + + +type SettleJobStatus = "queued" | "processing" | "succeeded" | "failed"; + +type SettleJob = { + id: string; + status: SettleJobStatus; + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + createdAt: string; + updatedAt: string; + result?: SettleResponse; + error?: string; +}; + +type SettleJobResult = { + jobId: string; + status: SettleJobStatus; + createdAt: string; + updatedAt: string; + result?: SettleResponse; + error?: string; +}; + +type SerializedBigInt = { + __type: "bigint"; + value: string; +}; + +function serializeJson(value: unknown): string { + return JSON.stringify(value, (_key, item) => { + if (typeof item === "bigint") { + const wrapped: SerializedBigInt = { + __type: "bigint", + value: item.toString(), + }; + return wrapped; + } + return item; + }); +} + +function parseJson(value: string): T { + return JSON.parse(value, (_key, item) => { + if ( + item && + typeof item === "object" && + "__type" in item && + (item as SerializedBigInt).__type === "bigint" + ) { + return BigInt((item as SerializedBigInt).value); + } + return item; + }) as T; +} + +function settleJobKey(jobId: string): string { + return `${SETTLE_JOB_KEY_PREFIX}${jobId}`; +} + +function toSettleJobResult(job: SettleJob): SettleJobResult { + return { + jobId: job.id, + status: job.status, + createdAt: job.createdAt, + updatedAt: job.updatedAt, + result: job.result, + error: job.error, + }; +} + +let isShuttingDown = false; +let httpServer: Server | null = null; + +// Configuration - optional per network +const evmPrivateKey = process.env.EVM_PRIVATE_KEY as `0x${string}` | undefined; +const svmPrivateKey = process.env.SVM_PRIVATE_KEY as string | undefined; + +// Validate at least one private key is provided +if (!evmPrivateKey && !svmPrivateKey) { + console.error( + "❌ At least one of EVM_PRIVATE_KEY or SVM_PRIVATE_KEY is required", + ); + process.exit(1); +} + +// Network configuration +const EVM_NETWORK = "eip155:10740"; // OG EVM +const BASE_TESTNET_NETWORK = "eip155:84532"; // Base Testnet +const SVM_NETWORK = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"; // Solana Devnet + +// Initialize the x402 Facilitator +const facilitator = new x402Facilitator() + .onBeforeVerify(async context => { + console.log("Before verify", context); + }) + .onAfterVerify(async context => { + console.log("After verify", context); + }) + .onVerifyFailure(async context => { + console.log("Verify failure", context); + }) + .onBeforeSettle(async context => { + console.log("Before settle", context); + }) + .onAfterSettle(async context => { + console.log("After settle", context); + }) + .onSettleFailure(async context => { + console.log("Settle failure", context); + }); + +const redisClient = createClient({ + url: REDIS_URL, +}); + +const settleWorkerRedis = redisClient.duplicate(); + +redisClient.on("error", (error: unknown) => { + console.error("Redis client error:", error); +}); + +settleWorkerRedis.on("error", (error: unknown) => { + if (!isShuttingDown) { + console.error("Redis settle worker error:", error); + } +}); + +async function saveSettleJob(job: SettleJob): Promise { + console.log("Saving settle job", job); + await redisClient.set(settleJobKey(job.id), serializeJson(job), { + EX: SETTLE_JOB_TTL_SECONDS, + }); +} + +async function getSettleJob(jobId: string): Promise { + + const raw = await redisClient.get(settleJobKey(jobId)); + if (!raw) { + return null; + } + return parseJson(raw); +} + +async function enqueueSettleJob( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, +): Promise { + console.log("Enqueuing settle job", paymentPayload, paymentRequirements); + const now = new Date().toISOString(); + const job: SettleJob = { + id: randomUUID(), + status: "queued", + paymentPayload, + paymentRequirements, + createdAt: now, + updatedAt: now, + }; + + await saveSettleJob(job); + await redisClient.lPush(SETTLE_QUEUE_KEY, job.id); + + return toSettleJobResult(job); +} + +async function settleQueuedJob(jobId: string): Promise { + const job = await getSettleJob(jobId); + if (!job) { + return; + } + + console.log("Processing settle job", job); + + const processingJob: SettleJob = { + ...job, + status: "processing", + updatedAt: new Date().toISOString(), + error: undefined, + }; + await saveSettleJob(processingJob); + + try { + const result = await facilitator.settle(job.paymentPayload, job.paymentRequirements); + const completedJob: SettleJob = { + ...processingJob, + status: "succeeded", + updatedAt: new Date().toISOString(), + result, + error: undefined, + }; + await saveSettleJob(completedJob); + } catch (error) { + const failedJob: SettleJob = { + ...processingJob, + status: "failed", + updatedAt: new Date().toISOString(), + result: undefined, + error: error instanceof Error ? error.message : "Unknown error", + }; + await saveSettleJob(failedJob); + } +} + +async function settleWorkerLoop(): Promise { + while (!isShuttingDown) { + try { + const popped = await settleWorkerRedis.brPop( + SETTLE_QUEUE_KEY, + SETTLE_WORKER_POLL_SECONDS, + ); + if (!popped) { + continue; + } + + await settleQueuedJob(popped.element); + } catch (error) { + if (isShuttingDown) { + return; + } + console.error("Settle worker loop error:", error); + } + } +} + +// Register EVM scheme if private key is provided +if (evmPrivateKey) { + const evmAccount = privateKeyToAccount(evmPrivateKey); + console.info(`EVM Facilitator account: ${evmAccount.address}`); + + // Create a Viem client with both wallet and public capabilities + const viemClient = createWalletClient({ + account: evmAccount, + chain: ogEvm, + transport: http(), + }).extend(publicActions); + + const baseViemClient = createWalletClient({ + account: evmAccount, + chain: baseSepolia, + transport: http(BASE_SEPOLIA_RPC_URL), + }).extend(publicActions); + + const evmSigner = toFacilitatorEvmSigner({ + getCode: (args: { address: `0x${string}` }) => viemClient.getCode(args), + address: evmAccount.address, + readContract: (args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; + }) => + viemClient.readContract({ + ...args, + args: args.args || [], + }), + verifyTypedData: (args: { + address: `0x${string}`; + domain: Record; + types: Record; + primaryType: string; + message: Record; + signature: `0x${string}`; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) => viemClient.verifyTypedData(args as any), + writeContract: (args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args: readonly unknown[]; + }) => + viemClient.writeContract({ + ...args, + args: args.args || [], + gas: 5000000n, // Set a high gas limit (5M) to prevent OOG + maxFeePerGas: parseGwei('0.002'), // Example: Set specific gas price if needed + maxPriorityFeePerGas: parseGwei('0.001'), + }), + sendTransaction: (args: { to: `0x${string}`; data: `0x${string}` }) => + viemClient.sendTransaction({ + ...args, + gas: 5000000n, // Set a high gas limit (5M) to prevent OOG + }), + waitForTransactionReceipt: (args: { hash: `0x${string}` }) => + viemClient.waitForTransactionReceipt(args), + }); + + const baseEvmSigner = toFacilitatorEvmSigner({ + getCode: (args: { address: `0x${string}` }) => baseViemClient.getCode(args), + address: evmAccount.address, + readContract: (args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; + }) => + baseViemClient.readContract({ + ...args, + args: args.args || [], + }), + verifyTypedData: (args: { + address: `0x${string}`; + domain: Record; + types: Record; + primaryType: string; + message: Record; + signature: `0x${string}`; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) => baseViemClient.verifyTypedData(args as any), + writeContract: (args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args: readonly unknown[]; + }) => + baseViemClient.writeContract({ + ...args, + args: args.args || [], + gas: 5000000n, // Set a high gas limit (5M) to prevent OOG + maxFeePerGas: parseGwei('0.006'), + maxPriorityFeePerGas: parseGwei('0.005'), + }), + sendTransaction: (args: { to: `0x${string}`; data: `0x${string}` }) => + baseViemClient.sendTransaction({ + ...args, + gas: 5000000n, // Set a high gas limit (5M) to prevent OOG + }), + waitForTransactionReceipt: (args: { hash: `0x${string}` }) => + baseViemClient.waitForTransactionReceipt(args), + }); + + + facilitator.register( + EVM_NETWORK, + new ExactEvmScheme(evmSigner, { deployERC4337WithEIP6492: true }), + ); + + facilitator.register( + EVM_NETWORK, + new UptoEvmScheme(evmSigner, { deployERC4337WithEIP6492: true }), + ); + + facilitator.register( + BASE_TESTNET_NETWORK, + new ExactEvmScheme(baseEvmSigner, { deployERC4337WithEIP6492: true }), + ); + + facilitator.register( + BASE_TESTNET_NETWORK, + new UptoEvmScheme(baseEvmSigner, { deployERC4337WithEIP6492: true }), + ); +} + +// Register SVM scheme if private key is provided +if (svmPrivateKey) { + const svmAccount = await createKeyPairSignerFromBytes( + base58.decode(svmPrivateKey), + ); + console.info(`SVM Facilitator account: ${svmAccount.address}`); + + const svmSigner = toFacilitatorSvmSigner(svmAccount); + + facilitator.register(SVM_NETWORK, new ExactSvmScheme(svmSigner)); +} + +// Initialize Express app +const app = express(); +app.use(express.json()); + +/** + * POST /verify + * Verify a payment against requirements + */ +app.post("/verify", async (req, res) => { + try { + const { paymentPayload, paymentRequirements } = req.body as { + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + }; + + if (!paymentPayload || !paymentRequirements) { + return res.status(400).json({ + error: "Missing paymentPayload or paymentRequirements", + }); + } + + const response: VerifyResponse = await facilitator.verify( + paymentPayload, + paymentRequirements, + ); + + res.json(response); + } catch (error) { + console.error("Verify error:", error); + res.status(500).json({ + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}); + +/** + * POST /settle + * Queue a settlement to process asynchronously + */ +app.post("/settle", async (req, res) => { + try { + const { paymentPayload, paymentRequirements } = req.body as { + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + }; + + if (!paymentPayload || !paymentRequirements) { + return res.status(400).json({ + error: "Missing paymentPayload or paymentRequirements", + }); + } + + const queuedJob = await enqueueSettleJob(paymentPayload, paymentRequirements); + res.status(202).json(queuedJob); + } catch (error) { + console.error("Settle enqueue error:", error); + res.status(500).json({ + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}); + +/** + * GET /settle/:jobId + * Get queued settlement status or final result + */ +app.get("/settle/:jobId", async (req, res) => { + try { + const job = await getSettleJob(req.params.jobId); + if (!job) { + return res.status(404).json({ + error: `Settlement job not found: ${req.params.jobId}`, + }); + } + + res.json(toSettleJobResult(job)); + } catch (error) { + console.error("Settle status error:", error); + res.status(500).json({ + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}); + +/** + * GET /supported + * Get supported payment kinds and extensions + */ +app.get("/supported", async (req, res) => { + try { + const response = facilitator.getSupported(); + res.json(response); + } catch (error) { + console.error("Supported error:", error); + res.status(500).json({ + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}); + +/** + * GET /health + * Health check endpoint + */ +app.get("/health", (req, res) => { + res.json({ status: "ok" }); +}); + +// Start the server +async function shutdown(signal: string): Promise { + if (isShuttingDown) { + return; + } + isShuttingDown = true; + console.log(`\nReceived ${signal}. Shutting down gracefully...`); + + const forcedExitTimer = setTimeout(() => { + console.error(`Forced shutdown after ${SHUTDOWN_TIMEOUT_MS}ms`); + process.exit(1); + }, SHUTDOWN_TIMEOUT_MS); + forcedExitTimer.unref(); + + if (httpServer) { + await new Promise(resolve => { + httpServer!.close(() => resolve()); + }); + } + + await Promise.allSettled([ + Promise.resolve(settleWorkerRedis.disconnect()), + Promise.resolve(redisClient.disconnect()), + ]); + + clearTimeout(forcedExitTimer); + process.exit(0); +} + +process.on("SIGINT", () => { + void shutdown("SIGINT"); +}); + +process.on("SIGTERM", () => { + void shutdown("SIGTERM"); +}); + +await redisClient.connect(); +await settleWorkerRedis.connect(); +void settleWorkerLoop(); + +httpServer = app.listen(parseInt(PORT, 10), () => { + console.log(`🚀 All Networks Facilitator listening on http://localhost:${PORT}`); + console.log(` Supported networks: ${facilitator.getSupported().kinds.map(k => k.network).join(", ")}`); + console.log(` Redis settle queue: ${SETTLE_QUEUE_KEY}`); + console.log(); +}); diff --git a/bazaar.ts b/bazaar.ts new file mode 100644 index 0000000..d0637f4 --- /dev/null +++ b/bazaar.ts @@ -0,0 +1,331 @@ +/** + * Facilitator with Discovery Extension Example + * + * Demonstrates how to create a facilitator with bazaar discovery extension that + * catalogs discovered x402 resources. + */ + +import { base58 } from "@scure/base"; +import { createKeyPairSignerFromBytes } from "@solana/kit"; +import { x402Facilitator } from "@x402/core/facilitator"; +import { + PaymentPayload, + PaymentRequirements, + SettleResponse, + VerifyResponse, +} from "@x402/core/types"; +import { toFacilitatorEvmSigner } from "@x402/evm"; +import { ExactEvmScheme } from "@x402/evm/exact/facilitator"; +import { toFacilitatorSvmSigner } from "@x402/svm"; +import { ExactSvmScheme } from "@x402/svm/exact/facilitator"; +import { extractDiscoveryInfo, DiscoveryInfo } from "@x402/extensions/bazaar"; +import dotenv from "dotenv"; +import express from "express"; +import { createWalletClient, http, publicActions } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { baseSepolia } from "viem/chains"; + +dotenv.config(); + +// Configuration +const PORT = process.env.PORT || "4022"; + +// Configuration - optional per network +const evmPrivateKey = process.env.EVM_PRIVATE_KEY as `0x${string}` | undefined; +const svmPrivateKey = process.env.SVM_PRIVATE_KEY as string | undefined; + +// Validate at least one private key is provided +if (!evmPrivateKey && !svmPrivateKey) { + console.error( + "❌ At least one of EVM_PRIVATE_KEY or SVM_PRIVATE_KEY is required", + ); + process.exit(1); +} + +// Network configuration +const EVM_NETWORK = "eip155:84532"; // Base Sepolia +const SVM_NETWORK = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"; // Solana Devnet + +// DiscoveredResource represents a discovered x402 resource for the bazaar catalog +interface DiscoveredResource { + resource: string; + description?: string; + mimeType?: string; + type: string; + x402Version: number; + accepts: PaymentRequirements[]; + discoveryInfo?: DiscoveryInfo; + lastUpdated: string; +} + +// BazaarCatalog stores discovered resources +class BazaarCatalog { + private resources: Map = new Map(); + + add(res: DiscoveredResource): void { + this.resources.set(res.resource, res); + } + + getAll(): DiscoveredResource[] { + return Array.from(this.resources.values()); + } +} + +const bazaarCatalog = new BazaarCatalog(); + +// Initialize the x402 Facilitator with discovery hooks +const facilitator = new x402Facilitator() + .onBeforeVerify(async context => { + console.log("Before verify", context); + }) + .onAfterVerify(async context => { + console.log("✅ Payment verified"); + + // Extract discovered resource from payment for bazaar catalog + try { + const discovered = extractDiscoveryInfo( + context.paymentPayload, + context.requirements, + true, // validate + ); + + if (discovered) { + console.log(` 📝 Discovered resource: ${discovered.resourceUrl}`); + console.log(` 📝 Description: ${discovered.description}`); + console.log(` 📝 MimeType: ${discovered.mimeType}`); + console.log(` 📝 Method: ${discovered.method}`); + console.log(` 📝 X402Version: ${discovered.x402Version}`); + + bazaarCatalog.add({ + resource: discovered.resourceUrl, + description: discovered.description, + mimeType: discovered.mimeType, + type: "http", + x402Version: discovered.x402Version, + accepts: [context.requirements], + discoveryInfo: discovered.discoveryInfo, + lastUpdated: new Date().toISOString(), + }); + console.log(" ✅ Added to bazaar catalog"); + } + } catch (err) { + console.log(` ⚠️ Failed to extract discovery info: ${err}`); + } + }) + .onVerifyFailure(async context => { + console.log("Verify failure", context); + }) + .onBeforeSettle(async context => { + console.log("Before settle", context); + }) + .onAfterSettle(async context => { + console.log(`🎉 Payment settled: ${context.result.transaction}`); + }) + .onSettleFailure(async context => { + console.log("Settle failure", context); + }); + +// Register EVM scheme if private key is provided +if (evmPrivateKey) { + const evmAccount = privateKeyToAccount(evmPrivateKey); + console.info(`EVM Facilitator account: ${evmAccount.address}`); + + // Create a Viem client with both wallet and public capabilities + const viemClient = createWalletClient({ + account: evmAccount, + chain: baseSepolia, + transport: http(), + }).extend(publicActions); + + const evmSigner = toFacilitatorEvmSigner({ + getCode: (args: { address: `0x${string}` }) => viemClient.getCode(args), + address: evmAccount.address, + readContract: (args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; + }) => + viemClient.readContract({ + ...args, + args: args.args || [], + }), + verifyTypedData: (args: { + address: `0x${string}`; + domain: Record; + types: Record; + primaryType: string; + message: Record; + signature: `0x${string}`; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) => viemClient.verifyTypedData(args as any), + writeContract: (args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args: readonly unknown[]; + }) => + viemClient.writeContract({ + ...args, + args: args.args || [], + }), + sendTransaction: (args: { to: `0x${string}`; data: `0x${string}` }) => + viemClient.sendTransaction(args), + waitForTransactionReceipt: (args: { hash: `0x${string}` }) => + viemClient.waitForTransactionReceipt(args), + }); + + facilitator.register( + EVM_NETWORK, + new ExactEvmScheme(evmSigner, { deployERC4337WithEIP6492: true }), + ); +} + +// Register SVM scheme if private key is provided +if (svmPrivateKey) { + const svmAccount = await createKeyPairSignerFromBytes( + base58.decode(svmPrivateKey), + ); + console.info(`SVM Facilitator account: ${svmAccount.address}`); + + const svmSigner = toFacilitatorSvmSigner(svmAccount); + + facilitator.register(SVM_NETWORK, new ExactSvmScheme(svmSigner)); +} + +// Initialize Express app +const app = express(); +app.use(express.json()); + +/** + * POST /verify + * Verify a payment against requirements + * + * Note: Payment tracking and bazaar discovery are handled by lifecycle hooks + */ +app.post("/verify", async (req, res) => { + try { + const { paymentPayload, paymentRequirements } = req.body as { + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + }; + + if (!paymentPayload || !paymentRequirements) { + return res.status(400).json({ + error: "Missing paymentPayload or paymentRequirements", + }); + } + + // Hooks will automatically: + // - Track verified payment (onAfterVerify) + // - Extract and catalog discovery info (onAfterVerify) + const response: VerifyResponse = await facilitator.verify( + paymentPayload, + paymentRequirements, + ); + + res.json(response); + } catch (error) { + console.error("Verify error:", error); + res.status(500).json({ + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}); + +/** + * POST /settle + * Settle a payment on-chain + */ +app.post("/settle", async (req, res) => { + try { + const { paymentPayload, paymentRequirements } = req.body; + + if (!paymentPayload || !paymentRequirements) { + return res.status(400).json({ + error: "Missing paymentPayload or paymentRequirements", + }); + } + + const response: SettleResponse = await facilitator.settle( + paymentPayload as PaymentPayload, + paymentRequirements as PaymentRequirements, + ); + + res.json(response); + } catch (error) { + console.error("Settle error:", error); + + // Check if this was an abort from hook + if ( + error instanceof Error && + error.message.includes("Settlement aborted:") + ) { + return res.json({ + success: false, + errorReason: error.message.replace("Settlement aborted: ", ""), + network: req.body?.paymentPayload?.network || "unknown", + } as SettleResponse); + } + + res.status(500).json({ + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}); + +/** + * GET /supported + * Get supported payment kinds and extensions + */ +app.get("/supported", async (req, res) => { + try { + const response = facilitator.getSupported(); + res.json(response); + } catch (error) { + console.error("Supported error:", error); + res.status(500).json({ + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}); + +/** + * GET /discovery/resources + * List all discovered resources from bazaar + */ +app.get("/discovery/resources", async (req, res) => { + try { + const resources = bazaarCatalog.getAll(); + res.json({ + x402Version: 2, + items: resources, + pagination: { + limit: 100, + offset: 0, + total: resources.length, + }, + }); + } catch (error) { + console.error("Discovery error:", error); + res.status(500).json({ + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}); + +/** + * GET /health + * Health check endpoint + */ +app.get("/health", (req, res) => { + res.json({ status: "ok" }); +}); + +// Start the server +app.listen(parseInt(PORT), () => { + console.log(`🚀 Discovery Facilitator listening on http://localhost:${PORT}`); + console.log(` Supported networks: ${facilitator.getSupported().kinds.map(k => k.network).join(", ")}`); + console.log(` Discovery endpoint: GET /discovery/resources`); + console.log(); +}); diff --git a/eslint.config.js b/eslint.config.js index e2fde7b..784ecd5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -21,7 +21,6 @@ export default [ module: "readonly", require: "readonly", Buffer: "readonly", - console: "readonly", exports: "readonly", setTimeout: "readonly", clearTimeout: "readonly", @@ -40,7 +39,10 @@ export default [ "import/first": "error", "prettier/prettier": "error", "@typescript-eslint/member-ordering": "error", - "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_$" }, + ], "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], "jsdoc/check-alignment": "error", "jsdoc/no-undefined-types": "off", diff --git a/index.ts b/index.ts index 6d2b921..5607acd 100644 --- a/index.ts +++ b/index.ts @@ -1,200 +1,203 @@ -/* eslint-env node */ -import { config } from "dotenv"; -import express, { Request, Response } from "express"; -import { verify, settle, startWorker, settlePayload } from "x402/facilitator"; - -import { - PaymentRequirementsSchema, - type PaymentRequirements, - type PaymentPayload, - PaymentPayloadSchema, - createConnectedClient, - createSigner, - SupportedEVMNetworks, - SupportedSVMNetworks, - Signer, - ConnectedClient, - SupportedPaymentKind, - isSvmSignerWallet, - type X402Config, -} from "x402/types"; - -config(); - -const EVM_PRIVATE_KEY = process.env.EVM_PRIVATE_KEY || ""; -const SVM_PRIVATE_KEY = process.env.SVM_PRIVATE_KEY || ""; -const SVM_RPC_URL = process.env.SVM_RPC_URL || ""; - -if (!EVM_PRIVATE_KEY && !SVM_PRIVATE_KEY) { - console.error("Missing required environment variables"); - process.exit(1); -} - -// Create X402 config -const x402Config: X402Config = { - svmConfig: SVM_RPC_URL ? { rpcUrl: SVM_RPC_URL } : undefined, - redis: process.env.REDIS_HOST - ? { - host: process.env.REDIS_HOST, - port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT, 10) : 6379, - } - : undefined, -}; - -const app = express(); - -// Configure express to parse JSON bodies -app.use(express.json()); - -type VerifyRequest = { - paymentPayload: PaymentPayload; - paymentRequirements: PaymentRequirements; -}; - -type SettleRequest = { - paymentPayload: PaymentPayload; - paymentRequirements: PaymentRequirements; -}; - - -app.get("/verify", (req: Request, res: Response) => { - res.json({ - endpoint: "/verify", - description: "POST to verify x402 payments", - body: { - paymentPayload: "PaymentPayload", - paymentRequirements: "PaymentRequirements", - }, - }); -}); - -app.post("/verify", async (req: Request, res: Response) => { - try { - const body: VerifyRequest = req.body; - const paymentRequirements = PaymentRequirementsSchema.parse(body.paymentRequirements); - const paymentPayload = PaymentPayloadSchema.parse(body.paymentPayload); - - // use the correct client/signer based on the requested network - // svm verify requires a Signer because it signs & simulates the txn - let client: Signer | ConnectedClient; - if (SupportedEVMNetworks.includes(paymentRequirements.network)) { - client = createConnectedClient(paymentRequirements.network); - } else if (SupportedSVMNetworks.includes(paymentRequirements.network)) { - client = await createSigner(paymentRequirements.network, SVM_PRIVATE_KEY); - } else { - throw new Error("Invalid network"); - } - - // verify - const valid = await verify(client, paymentPayload, paymentRequirements, x402Config); - res.json(valid); - } catch (error) { - console.error("error", error); - res.status(400).json({ error: "Invalid request" }); - } -}); - -app.get("/settle", (req: Request, res: Response) => { - res.json({ - endpoint: "/settle", - description: "POST to settle x402 payments", - body: { - paymentPayload: "PaymentPayload", - paymentRequirements: "PaymentRequirements", - }, - }); -}); - -app.get("/supported", async (req: Request, res: Response) => { - let kinds: SupportedPaymentKind[] = []; - - // evm - if (EVM_PRIVATE_KEY) { - kinds.push({ - x402Version: 1, - scheme: "exact", - network: "og-devnet", - }); - } - - // svm - if (SVM_PRIVATE_KEY) { - const signer = await createSigner("solana-devnet", SVM_PRIVATE_KEY); - const feePayer = isSvmSignerWallet(signer) ? signer.address : undefined; - - kinds.push({ - x402Version: 1, - scheme: "exact", - network: "solana-devnet", - extra: { - feePayer, - }, - }); - } - res.json({ - kinds, - }); -}); - -app.post("/settle", async (req: Request, res: Response) => { - try { - const body: SettleRequest = req.body; - - // get headers from req - const headers = req.headers; - const inputHash = headers["x-input-hash"] as string; - const outputHash = headers["x-output-hash"] as string; - const settlementType = headers["x-settlement-type"] as string; - - const paymentRequirements = PaymentRequirementsSchema.parse(body.paymentRequirements); - const paymentPayload = PaymentPayloadSchema.parse(body.paymentPayload); - - // use the correct private key based on the requested network - let signer: Signer; - if (SupportedEVMNetworks.includes(paymentRequirements.network)) { - signer = await createSigner(paymentRequirements.network, EVM_PRIVATE_KEY); - } else if (SupportedSVMNetworks.includes(paymentRequirements.network)) { - signer = await createSigner(paymentRequirements.network, SVM_PRIVATE_KEY); - } else { - throw new Error("Invalid network"); - } - - // settle payment - const response = await settle(signer, paymentPayload, paymentRequirements, x402Config); - // settle input and output - const settlement_data = { - network: paymentRequirements.network, - inputHash, - outputHash, - msg: "", - settlement_type: settlementType, - } - const payload_resp = await settlePayload(signer, settlement_data, x402Config); - - res.json(response); - } catch (error) { - console.error("error", error); - res.status(400).json({ error: `Invalid request: ${error}` }); - } -}); - - -app.listen(process.env.PORT || 3000, () => { - console.log(`Server listening at http://localhost:${process.env.PORT || 3000}`); - - if (x402Config.redis) { - startWorker(async (network: string) => { - const netAny = network as any; - console.log(`Worker processing network: ${netAny}`); - - if (SupportedEVMNetworks.includes(netAny)) { - return createSigner(netAny, EVM_PRIVATE_KEY); - } else if (SupportedSVMNetworks.includes(netAny)) { - return createSigner(netAny, SVM_PRIVATE_KEY); - } - throw new Error(`Unsupported network: ${network}`); - }, x402Config).catch((err: any) => { - console.error("Worker process failed:", err); - }); - } -}); +// /* eslint-env node */ +// import { config } from "dotenv"; +// import express, { Request, Response } from "express"; +// import { verify, settle, startWorker, settlePayload } from "x402/facilitator"; + +// import { +// PaymentRequirementsSchema, +// type PaymentRequirements, +// type PaymentPayload, +// PaymentPayloadSchema, +// createConnectedClient, +// createSigner, +// SupportedEVMNetworks, +// SupportedSVMNetworks, +// Signer, +// ConnectedClient, +// SupportedPaymentKind, +// isSvmSignerWallet, +// type X402Config, +// } from "x402/types"; + +// config(); + +// const EVM_PRIVATE_KEY = process.env.EVM_PRIVATE_KEY || ""; +// const SVM_PRIVATE_KEY = process.env.SVM_PRIVATE_KEY || ""; +// const SVM_RPC_URL = process.env.SVM_RPC_URL || ""; + +// if (!EVM_PRIVATE_KEY && !SVM_PRIVATE_KEY) { +// console.error("Missing required environment variables"); +// process.exit(1); +// } + +// // Create X402 config +// const x402Config: X402Config = { +// svmConfig: SVM_RPC_URL ? { rpcUrl: SVM_RPC_URL } : undefined, +// redis: process.env.REDIS_HOST +// ? { +// host: process.env.REDIS_HOST, +// port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT, 10) : 6379, +// } +// : undefined, +// }; + +// const app = express(); + +// // Configure express to parse JSON bodies +// app.use(express.json()); + +// type VerifyRequest = { +// paymentPayload: PaymentPayload; +// paymentRequirements: PaymentRequirements; +// }; + +// type SettleRequest = { +// paymentPayload: PaymentPayload; +// paymentRequirements: PaymentRequirements; +// }; + + +// app.get("/verify", (req: Request, res: Response) => { +// res.json({ +// endpoint: "/verify", +// description: "POST to verify x402 payments", +// body: { +// paymentPayload: "PaymentPayload", +// paymentRequirements: "PaymentRequirements", +// }, +// }); +// }); + +// app.post("/verify", async (req: Request, res: Response) => { +// try { +// const body: VerifyRequest = req.body; +// console.log("body", body); +// const paymentRequirements = PaymentRequirementsSchema.parse(body.paymentRequirements); +// const paymentPayload = PaymentPayloadSchema.parse(body.paymentPayload); + +// // use the correct client/signer based on the requested network +// // svm verify requires a Signer because it signs & simulates the txn +// let client: Signer | ConnectedClient; +// if (SupportedEVMNetworks.includes(paymentRequirements.network)) { +// client = createConnectedClient(paymentRequirements.network); +// } else if (SupportedSVMNetworks.includes(paymentRequirements.network)) { +// client = await createSigner(paymentRequirements.network, SVM_PRIVATE_KEY); +// } else { +// throw new Error("Invalid network"); +// } + +// // verify +// const valid = await verify(client, paymentPayload, paymentRequirements, x402Config); +// res.json(valid); +// } catch (error) { +// console.error("error", error); +// res.status(400).json({ error: "Invalid request" }); +// } +// }); + +// app.get("/settle", (req: Request, res: Response) => { +// res.json({ +// endpoint: "/settle", +// description: "POST to settle x402 payments", +// body: { +// paymentPayload: "PaymentPayload", +// paymentRequirements: "PaymentRequirements", +// }, +// }); +// }); + +// app.get("/supported", async (req: Request, res: Response) => { +// let kinds: SupportedPaymentKind[] = []; + +// // evm +// if (EVM_PRIVATE_KEY) { +// kinds.push({ +// x402Version: 1, +// scheme: "exact", +// network: "og-evm", +// }); +// } + +// // svm +// if (SVM_PRIVATE_KEY) { +// const signer = await createSigner("solana-devnet", SVM_PRIVATE_KEY); +// const feePayer = isSvmSignerWallet(signer) ? signer.address : undefined; + +// kinds.push({ +// x402Version: 1, +// scheme: "exact", +// network: "solana-devnet", +// extra: { +// feePayer, +// }, +// }); +// } +// res.json({ +// kinds, +// }); +// }); + +// app.post("/settle", async (req: Request, res: Response) => { +// try { +// const body: SettleRequest = req.body; + +// // get headers from req +// const headers = req.headers; +// const inputHash = headers["x-input-hash"] as string; +// const outputHash = headers["x-output-hash"] as string; +// const settlementType = headers["x-settlement-type"] as string; +// const modelType = headers["x-model-type"] as string; + +// const paymentRequirements = PaymentRequirementsSchema.parse(body.paymentRequirements); +// const paymentPayload = PaymentPayloadSchema.parse(body.paymentPayload); + +// // use the correct private key based on the requested network +// let signer: Signer; +// if (SupportedEVMNetworks.includes(paymentRequirements.network)) { +// signer = await createSigner(paymentRequirements.network, EVM_PRIVATE_KEY); +// } else if (SupportedSVMNetworks.includes(paymentRequirements.network)) { +// signer = await createSigner(paymentRequirements.network, SVM_PRIVATE_KEY); +// } else { +// throw new Error("Invalid network"); +// } + +// // settle payment +// const response = await settle(signer, paymentPayload, paymentRequirements, x402Config); +// // settle input and output +// const settlement_data = { +// network: paymentRequirements.network, +// inputHash, +// outputHash, +// msg: "", +// settlement_type: settlementType, +// model_type: modelType, +// } +// const payload_resp = await settlePayload(signer, settlement_data, x402Config); + +// res.json(response); +// } catch (error) { +// console.error("error", error); +// res.status(400).json({ error: `Invalid request: ${error}` }); +// } +// }); + + +// app.listen(process.env.PORT || 3000, () => { +// console.log(`Server listening at http://localhost:${process.env.PORT || 3000}`); + +// if (x402Config.redis) { +// startWorker(async (network: string) => { +// const netAny = network as any; +// console.log(`Worker processing network: ${netAny}`); + +// if (SupportedEVMNetworks.includes(netAny)) { +// return createSigner(netAny, EVM_PRIVATE_KEY); +// } else if (SupportedSVMNetworks.includes(netAny)) { +// return createSigner(netAny, SVM_PRIVATE_KEY); +// } +// throw new Error(`Unsupported network: ${network}`); +// }, x402Config).catch((err: any) => { +// console.error("Worker process failed:", err); +// }); +// } +// }); diff --git a/package-lock.json b/package-lock.json index 818c684..9e40a32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,10 +1,41 @@ { - "name": "x402", - "lockfileVersion": 3, - "requires": true, - "packages": { - "packages/typescript/x402": { - "extraneous": true - } + "name": "@x402/advanced-facilitator-example", + "version": "2.0.0", + "type": "module", + "private": true, + "scripts": { + "start": "tsx all_networks.ts", + "dev": "tsx all_networks.ts", + "dev:all-networks": "tsx all_networks.ts", + "dev:bazaar": "tsx bazaar.ts", + "build": "tsc", + "lint": "eslint .", + "format": "prettier --write .", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@scure/base": "^1.2.6", + "@solana/kit": "^2.1.1", + "@x402/core": "workspace:*", + "@x402/evm": "workspace:*", + "@x402/extensions": "workspace:*", + "@x402/svm": "workspace:*", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "viem": "^2.21.54" + }, + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/express": "^4.17.21", + "@types/node": "^22.10.1", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.15.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "^3.3.3", + "tsx": "^4.19.2", + "typescript": "^5.7.2" } -} +} \ No newline at end of file diff --git a/package.json b/package.json index e93737f..dfcefe6 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,43 @@ { - "name": "facilitator-x402", - "private": true, + "name": "@x402/advanced-facilitator-example", + "version": "2.0.0", "type": "module", + "private": true, "scripts": { + "start": "tsx all_networks.ts", + "start-prod": "node dist/all_networks.js", + "dev": "tsx all_networks.ts", + "dev:all-networks": "tsx all_networks.ts", + "dev:bazaar": "tsx bazaar.ts", "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx index.ts", - "solana": "tsx solana.ts", - "format": "prettier -c .prettierrc --write \"**/*.{tsgit,js,cjs,json,md}\"", - "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", - "lint": "eslint . --ext .ts --fix", - "lint:check": "eslint . --ext .ts" + "lint": "eslint .", + "format": "prettier --write .", + "typecheck": "tsc --noEmit" }, "dependencies": { - "dotenv": "^16.4.7", - "express": "^4.18.2", - "x402": "workspace:*" + "@scure/base": "^1.2.6", + "@solana/kit": "^2.1.1", + "@x402/core": "workspace:*", + "@x402/evm": "workspace:*", + "@x402/extensions": "workspace:*", + "@x402/svm": "workspace:*", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "redis": "^4.7.0", + "viem": "^2.21.54" }, "devDependencies": { "@eslint/js": "^9.24.0", - "@types/express": "^5.0.5", - "@types/node": "^24.1.10", + "@types/express": "^4.17.21", + "@types/node": "^22.10.1", "@typescript-eslint/eslint-plugin": "^8.29.1", "@typescript-eslint/parser": "^8.29.1", - "eslint": "^9.24.0", + "eslint": "^9.15.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsdoc": "^50.6.9", "eslint-plugin-prettier": "^5.2.6", - "prettier": "3.5.2", - "tsx": "^4.7.0", - "typescript": "^5.3.0" + "prettier": "^3.3.3", + "tsx": "^4.19.2", + "typescript": "^5.7.2" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dcc03e5..e4dbaee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,25 +8,287 @@ importers: .: dependencies: + '@scure/base': + specifier: ^1.2.6 + version: 1.2.6 + '@solana/kit': + specifier: ^2.1.1 + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@x402/core': + specifier: workspace:* + version: link:typescript/packages/core + '@x402/evm': + specifier: workspace:* + version: link:typescript/packages/mechanisms/evm + '@x402/extensions': + specifier: workspace:* + version: link:typescript/packages/extensions + '@x402/svm': + specifier: workspace:* + version: link:typescript/packages/mechanisms/svm dotenv: - specifier: ^16.4.7 + specifier: ^16.4.5 version: 16.6.1 express: - specifier: ^4.18.2 + specifier: ^4.19.2 version: 4.21.2 - x402: + redis: + specifier: ^4.7.0 + version: 4.7.1 + viem: + specifier: ^2.21.54 + version: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12) + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.39.1 + '@types/express': + specifier: ^4.17.21 + version: 4.17.25 + '@types/node': + specifier: ^22.10.1 + version: 22.19.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) + eslint: + specifier: ^9.15.0 + version: 9.39.1 + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.39.1) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.39.1)(prettier@3.5.2) + prettier: + specifier: ^3.3.3 + version: 3.5.2 + tsx: + specifier: ^4.19.2 + version: 4.20.6 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + + typescript/packages/core: + dependencies: + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.39.1 + '@types/node': + specifier: ^22.13.4 + version: 22.19.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) + eslint: + specifier: ^9.24.0 + version: 9.39.1 + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.39.1) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.39.1)(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3) + tsx: + specifier: ^4.19.2 + version: 4.20.6 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vite: + specifier: ^6.2.6 + version: 6.4.1(@types/node@22.19.0)(tsx@4.20.6) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.0)(tsx@4.20.6)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) + + typescript/packages/extensions: + dependencies: + '@scure/base': + specifier: ^1.2.6 + version: 1.2.6 + '@x402/core': + specifier: workspace:~ + version: link:../core + ajv: + specifier: ^8.17.1 + version: 8.17.1 + siwe: + specifier: ^2.3.2 + version: 2.3.2(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + tweetnacl: + specifier: ^1.0.3 + version: 1.0.3 + viem: + specifier: ^2.43.5 + version: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.39.1 + '@types/node': + specifier: ^22.13.4 + version: 22.19.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) + eslint: + specifier: ^9.24.0 + version: 9.39.1 + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.39.1) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.39.1)(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3) + tsx: + specifier: ^4.19.2 + version: 4.20.6 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vite: + specifier: ^6.2.6 + version: 6.4.1(@types/node@22.19.0)(tsx@4.20.6) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.0)(tsx@4.20.6)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) + + typescript/packages/http/axios: + dependencies: + '@x402/core': + specifier: workspace:~ + version: link:../../core + axios: + specifier: ^1.7.9 + version: 1.13.2 + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.39.1 + '@types/node': + specifier: ^22.13.4 + version: 22.19.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) + eslint: + specifier: ^9.24.0 + version: 9.39.1 + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.39.1) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.39.1)(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3) + tsx: + specifier: ^4.19.2 + version: 4.20.6 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vite: + specifier: ^6.2.6 + version: 6.4.1(@types/node@22.19.0)(tsx@4.20.6) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.0)(tsx@4.20.6)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) + + typescript/packages/http/express: + dependencies: + '@coinbase/cdp-sdk': + specifier: ^1.22.0 + version: 1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': + specifier: ^2.1.1 + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@x402/core': + specifier: workspace:~ + version: link:../../core + '@x402/extensions': + specifier: workspace:~ + version: link:../../extensions + '@x402/paywall': specifier: workspace:* - version: link:typescript/packages/x402 + version: link:../paywall + viem: + specifier: ^2.39.3 + version: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + zod: + specifier: ^3.24.2 + version: 3.25.76 devDependencies: '@eslint/js': specifier: ^9.24.0 version: 9.39.1 '@types/express': - specifier: ^5.0.5 + specifier: ^5.0.1 version: 5.0.5 '@types/node': - specifier: ^24.1.10 - version: 24.10.0 + specifier: ^22.13.4 + version: 22.19.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 version: 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) @@ -45,42 +307,252 @@ importers: eslint-plugin-prettier: specifier: ^5.2.6 version: 5.5.4(eslint@9.39.1)(prettier@3.5.2) + express: + specifier: ^4.18.2 + version: 4.21.2 prettier: specifier: 3.5.2 version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3) tsx: - specifier: ^4.7.0 + specifier: ^4.19.2 + version: 4.20.6 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vite: + specifier: ^6.2.6 + version: 6.4.1(@types/node@22.19.0)(tsx@4.20.6) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.0)(tsx@4.20.6)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) + + typescript/packages/http/fetch: + dependencies: + '@x402/core': + specifier: workspace:~ + version: link:../../core + viem: + specifier: ^2.39.3 + version: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.39.1 + '@types/node': + specifier: ^22.13.4 + version: 22.19.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) + eslint: + specifier: ^9.24.0 + version: 9.39.1 + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.39.1) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.39.1)(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3) + tsx: + specifier: ^4.19.2 + version: 4.20.6 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vite: + specifier: ^6.2.6 + version: 6.4.1(@types/node@22.19.0)(tsx@4.20.6) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.0)(tsx@4.20.6)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) + + typescript/packages/http/hono: + dependencies: + '@x402/core': + specifier: workspace:~ + version: link:../../core + '@x402/extensions': + specifier: workspace:~ + version: link:../../extensions + '@x402/paywall': + specifier: workspace:* + version: link:../paywall + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.39.1 + '@types/node': + specifier: ^22.13.4 + version: 22.19.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) + eslint: + specifier: ^9.24.0 + version: 9.39.1 + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.39.1) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.39.1)(prettier@3.5.2) + hono: + specifier: ^4.7.1 + version: 4.10.4 + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3) + tsx: + specifier: ^4.19.2 + version: 4.20.6 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vite: + specifier: ^6.2.6 + version: 6.4.1(@types/node@22.19.0)(tsx@4.20.6) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.0)(tsx@4.20.6)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) + + typescript/packages/http/next: + dependencies: + '@coinbase/cdp-sdk': + specifier: ^1.22.0 + version: 1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@x402/core': + specifier: workspace:~ + version: link:../../core + '@x402/extensions': + specifier: workspace:~ + version: link:../../extensions + '@x402/paywall': + specifier: workspace:* + version: link:../paywall + next: + specifier: ^16.0.10 + version: 16.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.39.1 + '@types/node': + specifier: ^22.13.4 + version: 22.19.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) + eslint: + specifier: ^9.24.0 + version: 9.39.1 + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.39.1) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.39.1)(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3) + tsx: + specifier: ^4.19.2 version: 4.20.6 typescript: - specifier: ^5.3.0 + specifier: ^5.7.3 version: 5.9.3 + vite: + specifier: ^6.2.6 + version: 6.4.1(@types/node@22.19.0)(tsx@4.20.6) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.0)(tsx@4.20.6)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) - typescript/packages/x402: + typescript/packages/http/paywall: dependencies: - '@openzeppelin/merkle-tree': - specifier: ^1.0.8 - version: 1.0.8 '@scure/base': specifier: ^1.2.6 version: 1.2.6 '@solana-program/compute-budget': specifier: ^0.8.0 - version: 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + version: 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) '@solana-program/token': specifier: ^0.5.1 - version: 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + version: 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) '@solana-program/token-2022': specifier: ^0.4.2 - version: 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) + version: 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) '@solana/kit': specifier: ^2.1.1 - version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/transaction-confirmation': specifier: ^2.1.1 - version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/wallet-standard-features': specifier: ^1.3.0 version: 1.3.0 + '@tanstack/react-query': + specifier: ^5.90.7 + version: 5.90.7(react@19.2.0) + '@wagmi/connectors': + specifier: ^5.8.1 + version: 5.11.2(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(zod@3.25.76) + '@wagmi/core': + specifier: ^2.17.1 + version: 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@wallet-standard/app': specifier: ^1.1.0 version: 1.1.0 @@ -90,34 +562,25 @@ importers: '@wallet-standard/features': specifier: ^1.1.0 version: 1.1.0 - hot-shots: - specifier: ^12.0.0 - version: 12.0.0 - ioredis: - specifier: ^5.8.2 - version: 5.8.2 + '@x402/core': + specifier: workspace:~ + version: link:../../core viem: - specifier: ^2.21.26 - version: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + specifier: ^2.39.3 + version: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: - specifier: ^2.15.6 - version: 2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + specifier: ^2.17.1 + version: 2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) zod: specifier: ^3.24.2 version: 3.25.76 devDependencies: - '@coinbase/onchainkit': - specifier: ^0.38.14 - version: 0.38.19(@farcaster/miniapp-sdk@0.2.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) '@craftamap/esbuild-plugin-html': specifier: ^0.9.0 version: 0.9.0(bufferutil@4.0.9)(esbuild@0.25.12)(utf-8-validate@5.0.10) '@eslint/js': specifier: ^9.24.0 version: 9.39.1 - '@types/ioredis': - specifier: ^5.0.0 - version: 5.0.0 '@types/node': specifier: ^22.13.4 version: 22.19.0 @@ -133,12 +596,12 @@ importers: '@typescript-eslint/parser': specifier: ^8.29.1 version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) - '@wagmi/connectors': - specifier: ^5.8.1 - version: 5.11.2(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(zod@3.25.76) - '@wagmi/core': - specifier: ^2.17.1 - version: 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@x402/evm': + specifier: workspace:~ + version: link:../../mechanisms/evm + '@x402/svm': + specifier: workspace:~ + version: link:../../mechanisms/svm buffer: specifier: ^6.0.3 version: 6.0.3 @@ -185,8 +648,203 @@ importers: specifier: ^3.0.5 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) + typescript/packages/mcp: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.12.1 + version: 1.26.0(zod@3.25.76) + '@x402/core': + specifier: workspace:~ + version: link:../core + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.39.1 + '@types/node': + specifier: ^22.13.4 + version: 22.19.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) + '@x402/evm': + specifier: workspace:~ + version: link:../mechanisms/evm + eslint: + specifier: ^9.24.0 + version: 9.39.1 + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.39.1) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.39.1)(prettier@3.5.2) + express: + specifier: ^4.21.2 + version: 4.21.2 + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3) + tsx: + specifier: ^4.19.2 + version: 4.20.6 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + viem: + specifier: ^2.27.2 + version: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + vite: + specifier: ^6.2.6 + version: 6.4.1(@types/node@22.19.0)(tsx@4.20.6) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.0)(tsx@4.20.6)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) + + typescript/packages/mechanisms/evm: + dependencies: + '@x402/core': + specifier: workspace:~ + version: link:../../core + viem: + specifier: ^2.39.3 + version: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.39.1 + '@types/node': + specifier: ^22.13.4 + version: 22.19.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) + eslint: + specifier: ^9.24.0 + version: 9.39.1 + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.39.1) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.39.1)(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3) + tsx: + specifier: ^4.19.2 + version: 4.20.6 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vite: + specifier: ^6.2.6 + version: 6.4.1(@types/node@22.19.0)(tsx@4.20.6) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.0)(tsx@4.20.6)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) + + typescript/packages/mechanisms/svm: + dependencies: + '@solana-program/compute-budget': + specifier: ^0.11.0 + version: 0.11.0(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@solana-program/token': + specifier: ^0.9.0 + version: 0.9.0(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@solana-program/token-2022': + specifier: ^0.6.1 + version: 0.6.1(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) + '@solana/kit': + specifier: ^5.1.0 + version: 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@x402/core': + specifier: workspace:~ + version: link:../../core + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.39.1 + '@scure/base': + specifier: ^1.2.6 + version: 1.2.6 + '@types/node': + specifier: ^22.13.4 + version: 22.19.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) + eslint: + specifier: ^9.24.0 + version: 9.39.1 + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.39.1) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.39.1)(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3) + tsx: + specifier: ^4.19.2 + version: 4.20.6 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vite: + specifier: ^6.2.6 + version: 6.4.1(@types/node@22.19.0)(tsx@4.20.6) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.0)(tsx@4.20.6)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.6) + packages: + '@adraffy/ens-normalize@1.10.1': + resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + '@adraffy/ens-normalize@1.11.1': resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} @@ -206,12 +864,6 @@ packages: '@coinbase/cdp-sdk@1.38.5': resolution: {integrity: sha512-j8mvx1wMox/q2SjB7C09HtdRXVOpGpfkP7nG4+OjdowPj8pmQ03rigzycd86L8mawl6TUPXdm41YSQVmtc8SzQ==} - '@coinbase/onchainkit@0.38.19': - resolution: {integrity: sha512-4uiujoTO5/8/dpWVZoTlBC7z0Y1N5fgBYDR6pKN/r6a8pX83ObUuOSGhSzJ8Xbu8NpPU6TXX+VuzLiwiLg/irg==} - peerDependencies: - react: ^18 || ^19 - react-dom: ^18 || ^19 - '@coinbase/wallet-sdk@3.9.3': resolution: {integrity: sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==} @@ -258,6 +910,9 @@ packages: peerDependencies: '@noble/ciphers': ^1.0.0 + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@es-joy/jsdoccomment@0.50.2': resolution: {integrity: sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==} engines: {node: '>=18'} @@ -472,63 +1127,174 @@ packages: resolution: {integrity: sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==} engines: {node: '>=14'} - '@farcaster/frame-sdk@0.1.12': - resolution: {integrity: sha512-qlikvkxsrsvKGutr3PM3Yvsuni2Fku15aPagrCyHZWbYAJrZ4mRJM6u9S8eLAXMMnYd9gJ9yor6COYgmZMBOgQ==} - engines: {node: '>=22.11.0'} + '@gemini-wallet/core@0.2.0': + resolution: {integrity: sha512-vv9aozWnKrrPWQ3vIFcWk7yta4hQW1Ie0fsNNPeXnjAxkbXr2hqMagEptLuMxpEP2W3mnRu05VDNKzcvAuuZDw==} + peerDependencies: + viem: '>=2.0.0' + + '@gemini-wallet/core@0.3.1': + resolution: {integrity: sha512-XP+/NRAaRV7Adlt6aFxrF/3a0i3qpxFTSVm/dzG+mceXTSgIGOUUT65w69ttLQ/GWEcAEQL/hKoV6PFAJA/DmQ==} + peerDependencies: + viem: '>=2.0.0' + + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] - '@farcaster/miniapp-core@0.4.1': - resolution: {integrity: sha512-20FxHTRToYUKx7CQ8PvIy9OoQ6XjdmF1pRMS7dsj37qdqjVDeEkYoK8yXwnoReZoJRcYwIg8P3i6V8bTWNR5mg==} + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] - '@farcaster/miniapp-sdk@0.2.1': - resolution: {integrity: sha512-2SnDeOtDdlN1lGQt7UyH2jkrZRQDOkmhcrlzNWazYChyPh9XfV8+9fMS+Lr/E2pEws9Q40Xl9POrCzdpUC19lg==} + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] - '@farcaster/miniapp-wagmi-connector@1.1.0': - resolution: {integrity: sha512-gf0nDx9nNJ6hJXbFBCgiTitb0eEqBvCU/njcyTXf7ebZhT0pzOrarOod2dkeisU5Py+WWjFyOVcqmeo4G3IvDA==} - peerDependencies: - '@farcaster/miniapp-sdk': ^0.2.0 - '@wagmi/core': ^2.14.1 - viem: ^2.21.55 + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] - '@farcaster/quick-auth@0.0.6': - resolution: {integrity: sha512-tiZndhpfDtEhaKlkmS5cVDuS+A/tafqZT3y9I44rC69m3beJok6e8dIH2JhxVy3EvOWTyTBnrmNn6GOOh+qK6A==} - peerDependencies: - typescript: 5.8.3 + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] - '@farcaster/quick-auth@0.0.8': - resolution: {integrity: sha512-NRIq1BcbcQCC6xBF5owfckkY00xKQVpqhpLNl5rICVpl0xeDsiVbkenIrHaUuyjtCK2W28YVc2ZCFRyz9ERHKg==} - peerDependencies: - typescript: 5.8.3 + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] - '@gemini-wallet/core@0.2.0': - resolution: {integrity: sha512-vv9aozWnKrrPWQ3vIFcWk7yta4hQW1Ie0fsNNPeXnjAxkbXr2hqMagEptLuMxpEP2W3mnRu05VDNKzcvAuuZDw==} - peerDependencies: - viem: '>=2.0.0' + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] - '@gemini-wallet/core@0.3.1': - resolution: {integrity: sha512-XP+/NRAaRV7Adlt6aFxrF/3a0i3qpxFTSVm/dzG+mceXTSgIGOUUT65w69ttLQ/GWEcAEQL/hKoV6PFAJA/DmQ==} - peerDependencies: - viem: '>=2.0.0' + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] - '@graphql-typed-document-node/core@3.2.0': - resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} - engines: {node: '>=18.18.0'} + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] '@ioredis/commands@1.4.0': resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} @@ -556,10 +1322,6 @@ packages: '@lit/reactive-element@2.1.1': resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==} - '@metamask/abi-utils@2.0.4': - resolution: {integrity: sha512-StnIgUB75x7a7AgUhiaUZDpCsqGp7VkNnZh2XivXkJ6mPkE83U8ARGQj5MbRis7VJY8BC5V1AbB1fjdh0hupPQ==} - engines: {node: '>=16.0.0'} - '@metamask/eth-json-rpc-provider@1.0.1': resolution: {integrity: sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA==} engines: {node: '>=14.0.0'} @@ -640,6 +1402,67 @@ packages: resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} engines: {node: '>=16.0.0'} + '@modelcontextprotocol/sdk@1.26.0': + resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@next/env@16.1.6': + resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} + + '@next/swc-darwin-arm64@16.1.6': + resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.1.6': + resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.1.6': + resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@16.1.6': + resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@16.1.6': + resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@16.1.6': + resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@16.1.6': + resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.1.6': + resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@noble/ciphers@1.2.1': resolution: {integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==} engines: {node: ^14.21.3 || >=16} @@ -648,6 +1471,9 @@ packages: resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} + '@noble/curves@1.2.0': + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + '@noble/curves@1.4.2': resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==} @@ -659,10 +1485,6 @@ packages: resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} engines: {node: ^14.21.3 || >=16} - '@noble/curves@1.9.0': - resolution: {integrity: sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==} - engines: {node: ^14.21.3 || >=16} - '@noble/curves@1.9.1': resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} engines: {node: ^14.21.3 || >=16} @@ -671,6 +1493,10 @@ packages: resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@1.3.2': + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + '@noble/hashes@1.4.0': resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -699,9 +1525,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@openzeppelin/merkle-tree@1.0.8': - resolution: {integrity: sha512-E2c9/Y3vjZXwVvPZKqCKUn7upnvam1P1ZhowJyZVQSkzZm5WhumtaRr+wkUXrZVfkIc7Gfrl7xzabElqDL09ow==} - '@paulmillr/qr@0.2.1': resolution: {integrity: sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==} deprecated: 'The package is now available as "qr": npm install qr' @@ -714,6 +1537,35 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@redis/bloom@1.2.0': + resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/client@1.6.1': + resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} + engines: {node: '>=14'} + + '@redis/graph@1.1.1': + resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/json@1.0.7': + resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/search@1.2.0': + resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/time-series@1.1.0': + resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} + peerDependencies: + '@redis/client': ^1.0.0 + '@reown/appkit-common@1.7.8': resolution: {integrity: sha512-ridIhc/x6JOp7KbDdwGKY4zwf8/iK8EYBl+HtWrruutSLwZyVi5P8WaZa+8iajL6LcDcDF7LoyLwMTym7SRuwQ==} @@ -893,6 +1745,11 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@solana-program/compute-budget@0.11.0': + resolution: {integrity: sha512-7f1ePqB/eURkTwTOO9TNIdUXZcyrZoX3Uy2hNo7cXMfNhPFWp9AVgIyRNBc2jf15sdUa9gNpW+PfP2iV8AYAaw==} + peerDependencies: + '@solana/kit': ^5.0 + '@solana-program/compute-budget@0.8.0': resolution: {integrity: sha512-qPKxdxaEsFxebZ4K5RPuy7VQIm/tfJLa1+Nlt3KNA8EYQkz9Xm8htdoEaXVrer9kpgzzp9R3I3Bh6omwCM06tQ==} peerDependencies: @@ -909,6 +1766,12 @@ packages: '@solana/kit': ^2.1.0 '@solana/sysvars': ^2.1.0 + '@solana-program/token-2022@0.6.1': + resolution: {integrity: sha512-Ex02cruDMGfBMvZZCrggVR45vdQQSI/unHVpt/7HPt/IwFYB4eTlXtO8otYZyqV/ce5GqZ8S6uwyRf0zy6fdbA==} + peerDependencies: + '@solana/kit': ^5.0 + '@solana/sysvars': ^5.0 + '@solana-program/token@0.5.1': resolution: {integrity: sha512-bJvynW5q9SFuVOZ5vqGVkmaPGA0MCC+m9jgJj1nk5m20I389/ms69ASnhWGoOPNcie7S9OwBX0gTj2fiyWpfag==} peerDependencies: @@ -919,6 +1782,11 @@ packages: peerDependencies: '@solana/kit': ^3.0 + '@solana-program/token@0.9.0': + resolution: {integrity: sha512-vnZxndd4ED4Fc56sw93cWZ2djEeeOFxtaPS8SPf5+a+JZjKA/EnKqzbE1y04FuMhIVrLERQ8uR8H2h72eZzlsA==} + peerDependencies: + '@solana/kit': ^5.0 + '@solana/accounts@2.3.0': resolution: {integrity: sha512-QgQTj404Z6PXNOyzaOpSzjgMOuGwG8vC66jSDB+3zHaRcEPRVRd2sVSrd1U6sHtnV3aiaS6YyDuPQMheg4K2jw==} engines: {node: '>=20.18.0'} @@ -931,6 +1799,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/accounts@5.5.1': + resolution: {integrity: sha512-TfOY9xixg5rizABuLVuZ9XI2x2tmWUC/OoN556xwfDlhBHBjKfszicYYOyD6nbFmwTGYarCmyGIdteXxTXIdhQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/addresses@2.3.0': resolution: {integrity: sha512-ypTNkY2ZaRFpHLnHAgaW8a83N0/WoqdFvCqf4CQmnMdFsZSdC7qOwcbd7YzdaQn9dy+P2hybewzB+KP7LutxGA==} engines: {node: '>=20.18.0'} @@ -943,6 +1820,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/addresses@5.5.1': + resolution: {integrity: sha512-5xoah3Q9G30HQghu/9BiHLb5pzlPKRC3zydQDmE3O9H//WfayxTFppsUDCL6FjYUHqj/wzK6CWHySglc2RkpdA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/assertions@2.3.0': resolution: {integrity: sha512-Ekoet3khNg3XFLN7MIz8W31wPQISpKUGDGTylLptI+JjCDWx3PIa88xjEMqFo02WJ8sBj2NLV64Xg1sBcsHjZQ==} engines: {node: '>=20.18.0'} @@ -955,6 +1841,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/assertions@5.5.1': + resolution: {integrity: sha512-YTCSWAlGwSlVPnWtWLm3ukz81wH4j2YaCveK+TjpvUU88hTy6fmUqxi0+hvAMAe4zKXpJyj3Az7BrLJRxbIm4Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/buffer-layout@4.0.1': resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==} engines: {node: '>=5.10'} @@ -971,6 +1866,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/codecs-core@5.5.1': + resolution: {integrity: sha512-TgBt//bbKBct0t6/MpA8ElaOA3sa8eYVvR7LGslCZ84WiAwwjCY0lW/lOYsFHJQzwREMdUyuEyy5YWBKtdh8Rw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/codecs-data-structures@2.3.0': resolution: {integrity: sha512-qvU5LE5DqEdYMYgELRHv+HMOx73sSoV1ZZkwIrclwUmwTbTaH8QAJURBj0RhQ/zCne7VuLLOZFFGv6jGigWhSw==} engines: {node: '>=20.18.0'} @@ -983,6 +1887,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/codecs-data-structures@5.5.1': + resolution: {integrity: sha512-97bJWGyUY9WvBz3mX1UV3YPWGDTez6btCfD0ip3UVEXJbItVuUiOkzcO5iFDUtQT5riKT6xC+Mzl+0nO76gd0w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/codecs-numbers@2.3.0': resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==} engines: {node: '>=20.18.0'} @@ -995,6 +1908,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/codecs-numbers@5.5.1': + resolution: {integrity: sha512-rllMIZAHqmtvC0HO/dc/21wDuWaD0B8Ryv8o+YtsICQBuiL/0U4AGwH7Pi5GNFySYk0/crSuwfIqQFtmxNSPFw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/codecs-strings@2.3.0': resolution: {integrity: sha512-y5pSBYwzVziXu521hh+VxqUtp0hYGTl1eWGoc1W+8mdvBdC1kTqm/X7aYQw33J42hw03JjryvYOvmGgk3Qz/Ug==} engines: {node: '>=20.18.0'} @@ -1009,6 +1931,18 @@ packages: fastestsmallesttextencoderdecoder: ^1.0.22 typescript: '>=5.3.3' + '@solana/codecs-strings@5.5.1': + resolution: {integrity: sha512-7klX4AhfHYA+uKKC/nxRGP2MntbYQCR3N6+v7bk1W/rSxYuhNmt+FN8aoThSZtWIKwN6BEyR1167ka8Co1+E7A==} + engines: {node: '>=20.18.0'} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: ^5.0.0 + peerDependenciesMeta: + fastestsmallesttextencoderdecoder: + optional: true + typescript: + optional: true + '@solana/codecs@2.3.0': resolution: {integrity: sha512-JVqGPkzoeyU262hJGdH64kNLH0M+Oew2CIPOa/9tR3++q2pEd4jU2Rxdfye9sd0Ce3XJrR5AIa8ZfbyQXzjh+g==} engines: {node: '>=20.18.0'} @@ -1021,6 +1955,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/codecs@5.5.1': + resolution: {integrity: sha512-Vea29nJub/bXjfzEV7ZZQ/PWr1pYLZo3z0qW0LQL37uKKVzVFRQlwetd7INk3YtTD3xm9WUYr7bCvYUk3uKy2g==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/errors@2.3.0': resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==} engines: {node: '>=20.18.0'} @@ -1035,6 +1978,16 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/errors@5.5.1': + resolution: {integrity: sha512-vFO3p+S7HoyyrcAectnXbdsMfwUzY2zYFUc2DEe5BwpiE9J1IAxPBGjOWO6hL1bbYdBrlmjNx8DXCslqS+Kcmg==} + engines: {node: '>=20.18.0'} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/fast-stable-stringify@2.3.0': resolution: {integrity: sha512-KfJPrMEieUg6D3hfQACoPy0ukrAV8Kio883llt/8chPEG3FVTX9z/Zuf4O01a15xZmBbmQ7toil2Dp0sxMJSxw==} engines: {node: '>=20.18.0'} @@ -1047,6 +2000,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/fast-stable-stringify@5.5.1': + resolution: {integrity: sha512-Ni7s2FN33zTzhTFgRjEbOVFO+UAmK8qi3Iu0/GRFYK4jN696OjKHnboSQH/EacQ+yGqS54bfxf409wU5dsLLCw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/functional@2.3.0': resolution: {integrity: sha512-AgsPh3W3tE+nK3eEw/W9qiSfTGwLYEvl0rWaxHht/lRcuDVwfKRzeSa5G79eioWFFqr+pTtoCr3D3OLkwKz02Q==} engines: {node: '>=20.18.0'} @@ -1059,12 +2021,30 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/functional@5.5.1': + resolution: {integrity: sha512-tTHoJcEQq3gQx5qsdsDJ0LEJeFzwNpXD80xApW9o/PPoCNimI3SALkZl+zNW8VnxRrV3l3yYvfHWBKe/X3WG3w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/instruction-plans@3.0.3': resolution: {integrity: sha512-eqoaPtWtmLTTpdvbt4BZF5H6FIlJtXi9H7qLOM1dLYonkOX2Ncezx5NDCZ9tMb2qxVMF4IocYsQnNSnMfjQF1w==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' + '@solana/instruction-plans@5.5.1': + resolution: {integrity: sha512-7z3CB7YMcFKuVvgcnNY8bY6IsZ8LG61Iytbz7HpNVGX2u1RthOs1tRW8luTzSG1MPL0Ox7afyAVMYeFqSPHnaQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/instructions@2.3.0': resolution: {integrity: sha512-PLMsmaIKu7hEAzyElrk2T7JJx4D+9eRwebhFZpy2PXziNSmFF929eRHKUsKqBFM3cYR1Yy3m6roBZfA+bGE/oQ==} engines: {node: '>=20.18.0'} @@ -1077,6 +2057,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/instructions@5.5.1': + resolution: {integrity: sha512-h0G1CG6S+gUUSt0eo6rOtsaXRBwCq1+Js2a+Ps9Bzk9q7YHNFA75/X0NWugWLgC92waRp66hrjMTiYYnLBoWOQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/keys@2.3.0': resolution: {integrity: sha512-ZVVdga79pNH+2pVcm6fr2sWz9HTwfopDVhYb0Lh3dh+WBmJjwkabXEIHey2rUES7NjFa/G7sV8lrUn/v8LDCCQ==} engines: {node: '>=20.18.0'} @@ -1089,6 +2078,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/keys@5.5.1': + resolution: {integrity: sha512-KRD61cL7CRL+b4r/eB9dEoVxIf/2EJ1Pm1DmRYhtSUAJD2dJ5Xw8QFuehobOGm9URqQ7gaQl+Fkc1qvDlsWqKg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/kit@2.3.0': resolution: {integrity: sha512-sb6PgwoW2LjE5oTFu4lhlS/cGt/NB3YrShEyx7JgWFWysfgLdJnhwWThgwy/4HjNsmtMrQGWVls0yVBHcMvlMQ==} engines: {node: '>=20.18.0'} @@ -1101,6 +2099,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/kit@5.5.1': + resolution: {integrity: sha512-irKUGiV2yRoyf+4eGQ/ZeCRxa43yjFEL1DUI5B0DkcfZw3cr0VJtVJnrG8OtVF01vT0OUfYOcUn6zJW5TROHvQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/nominal-types@2.3.0': resolution: {integrity: sha512-uKlMnlP4PWW5UTXlhKM8lcgIaNj8dvd8xO4Y9l+FVvh9RvW2TO0GwUO6JCo7JBzCB0PSqRJdWWaQ8pu1Ti/OkA==} engines: {node: '>=20.18.0'} @@ -1113,6 +2120,24 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/nominal-types@5.5.1': + resolution: {integrity: sha512-I1ImR+kfrLFxN5z22UDiTWLdRZeKtU0J/pkWkO8qm/8WxveiwdIv4hooi8pb6JnlR4mSrWhq0pCIOxDYrL9GIQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/offchain-messages@5.5.1': + resolution: {integrity: sha512-g+xHH95prTU+KujtbOzj8wn+C7ZNoiLhf3hj6nYq3MTyxOXtBEysguc97jJveUZG0K97aIKG6xVUlMutg5yxhw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/options@2.3.0': resolution: {integrity: sha512-PPnnZBRCWWoZQ11exPxf//DRzN2C6AoFsDI/u2AsQfYih434/7Kp4XLpfOMT/XESi+gdBMFNNfbES5zg3wAIkw==} engines: {node: '>=20.18.0'} @@ -1125,6 +2150,24 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/options@5.5.1': + resolution: {integrity: sha512-eo971c9iLNLmk+yOFyo7yKIJzJ/zou6uKpy6mBuyb/thKtS/haiKIc3VLhyTXty3OH2PW8yOlORJnv4DexJB8A==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/plugin-core@5.5.1': + resolution: {integrity: sha512-VUZl30lDQFJeiSyNfzU1EjYt2QZvoBFKEwjn1lilUJw7KgqD5z7mbV7diJhT+dLFs36i0OsjXvq5kSygn8YJ3A==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/programs@2.3.0': resolution: {integrity: sha512-UXKujV71VCI5uPs+cFdwxybtHZAIZyQkqDiDnmK+DawtOO9mBn4Nimdb/6RjR2CXT78mzO9ZCZ3qfyX+ydcB7w==} engines: {node: '>=20.18.0'} @@ -1137,6 +2180,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/programs@5.5.1': + resolution: {integrity: sha512-7U9kn0Jsx1NuBLn5HRTFYh78MV4XN145Yc3WP/q5BlqAVNlMoU9coG5IUTJIG847TUqC1lRto3Dnpwm6T4YRpA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/promises@2.3.0': resolution: {integrity: sha512-GjVgutZKXVuojd9rWy1PuLnfcRfqsaCm7InCiZc8bqmJpoghlyluweNc7ml9Y5yQn1P2IOyzh9+p/77vIyNybQ==} engines: {node: '>=20.18.0'} @@ -1149,6 +2201,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/promises@5.5.1': + resolution: {integrity: sha512-T9lfuUYkGykJmppEcssNiCf6yiYQxJkhiLPP+pyAc2z84/7r3UVIb2tNJk4A9sucS66pzJnVHZKcZVGUUp6wzA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/rpc-api@2.3.0': resolution: {integrity: sha512-UUdiRfWoyYhJL9PPvFeJr4aJ554ob2jXcpn4vKmRVn9ire0sCbpQKYx6K8eEKHZWXKrDW8IDspgTl0gT/aJWVg==} engines: {node: '>=20.18.0'} @@ -1159,7 +2220,16 @@ packages: resolution: {integrity: sha512-Yym9/Ama62OY69rAZgbOCAy1QlqaWAyb0VlqFuwSaZV1pkFCCFSwWEJEsiN1n8pb2ZP+RtwNvmYixvWizx9yvA==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: '>=5.3.3' + typescript: '>=5.3.3' + + '@solana/rpc-api@5.5.1': + resolution: {integrity: sha512-XWOQQPhKl06Vj0xi3RYHAc6oEQd8B82okYJ04K7N0Vvy3J4PN2cxeK7klwkjgavdcN9EVkYCChm2ADAtnztKnA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true '@solana/rpc-parsed-types@2.3.0': resolution: {integrity: sha512-B5pHzyEIbBJf9KHej+zdr5ZNAdSvu7WLU2lOUPh81KHdHQs6dEb310LGxcpCc7HVE8IEdO20AbckewDiAN6OCg==} @@ -1173,6 +2243,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-parsed-types@5.5.1': + resolution: {integrity: sha512-HEi3G2nZqGEsa3vX6U0FrXLaqnUCg4SKIUrOe8CezD+cSFbRTOn3rCLrUmJrhVyXlHoQVaRO9mmeovk31jWxJg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/rpc-spec-types@2.3.0': resolution: {integrity: sha512-xQsb65lahjr8Wc9dMtP7xa0ZmDS8dOE2ncYjlvfyw/h4mpdXTUdrSMi6RtFwX33/rGuztQ7Hwaid5xLNSLvsFQ==} engines: {node: '>=20.18.0'} @@ -1185,6 +2264,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-spec-types@5.5.1': + resolution: {integrity: sha512-6OFKtRpIEJQs8Jb2C4OO8KyP2h2Hy1MFhatMAoXA+0Ik8S3H+CicIuMZvGZ91mIu/tXicuOOsNNLu3HAkrakrw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/rpc-spec@2.3.0': resolution: {integrity: sha512-fA2LMX4BMixCrNB2n6T83AvjZ3oUQTu7qyPLyt8gHQaoEAXs8k6GZmu6iYcr+FboQCjUmRPgMaABbcr9j2J9Sw==} engines: {node: '>=20.18.0'} @@ -1197,6 +2285,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-spec@5.5.1': + resolution: {integrity: sha512-m3LX2bChm3E3by4mQrH4YwCAFY57QBzuUSWqlUw7ChuZ+oLLOq7b2czi4i6L4Vna67j3eCmB3e+4tqy1j5wy7Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/rpc-subscriptions-api@2.3.0': resolution: {integrity: sha512-9mCjVbum2Hg9KGX3LKsrI5Xs0KX390lS+Z8qB80bxhar6MJPugqIPH8uRgLhCW9GN3JprAfjRNl7our8CPvsPQ==} engines: {node: '>=20.18.0'} @@ -1209,6 +2306,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-subscriptions-api@5.5.1': + resolution: {integrity: sha512-5Oi7k+GdeS8xR2ly1iuSFkAv6CZqwG0Z6b1QZKbEgxadE1XGSDrhM2cn59l+bqCozUWCqh4c/A2znU/qQjROlw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/rpc-subscriptions-channel-websocket@2.3.0': resolution: {integrity: sha512-2oL6ceFwejIgeWzbNiUHI2tZZnaOxNTSerszcin7wYQwijxtpVgUHiuItM/Y70DQmH9sKhmikQp+dqeGalaJxw==} engines: {node: '>=20.18.0'} @@ -1223,6 +2329,15 @@ packages: typescript: '>=5.3.3' ws: ^8.18.0 + '@solana/rpc-subscriptions-channel-websocket@5.5.1': + resolution: {integrity: sha512-7tGfBBrYY8TrngOyxSHoCU5shy86iA9SRMRrPSyBhEaZRAk6dnbdpmUTez7gtdVo0BCvh9nzQtUycKWSS7PnFQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/rpc-subscriptions-spec@2.3.0': resolution: {integrity: sha512-rdmVcl4PvNKQeA2l8DorIeALCgJEMSu7U8AXJS1PICeb2lQuMeaR+6cs/iowjvIB0lMVjYN2sFf6Q3dJPu6wWg==} engines: {node: '>=20.18.0'} @@ -1235,6 +2350,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-subscriptions-spec@5.5.1': + resolution: {integrity: sha512-iq+rGq5fMKP3/mKHPNB6MC8IbVW41KGZg83Us/+LE3AWOTWV1WT20KT2iH1F1ik9roi42COv/TpoZZvhKj45XQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/rpc-subscriptions@2.3.0': resolution: {integrity: sha512-Uyr10nZKGVzvCOqwCZgwYrzuoDyUdwtgQRefh13pXIrdo4wYjVmoLykH49Omt6abwStB0a4UL5gX9V4mFdDJZg==} engines: {node: '>=20.18.0'} @@ -1247,6 +2371,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-subscriptions@5.5.1': + resolution: {integrity: sha512-CTMy5bt/6mDh4tc6vUJms9EcuZj3xvK0/xq8IQ90rhkpYvate91RjBP+egvjgSayUg9yucU9vNuUpEjz4spM7w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/rpc-transformers@2.3.0': resolution: {integrity: sha512-UuHYK3XEpo9nMXdjyGKkPCOr7WsZsxs7zLYDO1A5ELH3P3JoehvrDegYRAGzBS2VKsfApZ86ZpJToP0K3PhmMA==} engines: {node: '>=20.18.0'} @@ -1259,6 +2392,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-transformers@5.5.1': + resolution: {integrity: sha512-OsWqLCQdcrRJKvHiMmwFhp9noNZ4FARuMkHT5us3ustDLXaxOjF0gfqZLnMkulSLcKt7TGXqMhBV+HCo7z5M8Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/rpc-transport-http@2.3.0': resolution: {integrity: sha512-HFKydmxGw8nAF5N+S0NLnPBDCe5oMDtI2RAmW8DMqP4U3Zxt2XWhvV1SNkAldT5tF0U1vP+is6fHxyhk4xqEvg==} engines: {node: '>=20.18.0'} @@ -1271,6 +2413,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-transport-http@5.5.1': + resolution: {integrity: sha512-yv8GoVSHqEV0kUJEIhkdOVkR2SvJ6yoWC51cJn2rSV7plr6huLGe0JgujCmB7uZhhaLbcbP3zxXxu9sOjsi7Fg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/rpc-types@2.3.0': resolution: {integrity: sha512-O09YX2hED2QUyGxrMOxQ9GzH1LlEwwZWu69QbL4oYmIf6P5dzEEHcqRY6L1LsDVqc/dzAdEs/E1FaPrcIaIIPw==} engines: {node: '>=20.18.0'} @@ -1283,6 +2434,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/rpc-types@5.5.1': + resolution: {integrity: sha512-bibTFQ7PbHJJjGJPmfYC2I+/5CRFS4O2p9WwbFraX1Keeel+nRrt/NBXIy8veP5AEn2sVJIyJPpWBRpCx1oATA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/rpc@2.3.0': resolution: {integrity: sha512-ZWN76iNQAOCpYC7yKfb3UNLIMZf603JckLKOOLTHuy9MZnTN8XV6uwvDFhf42XvhglgUjGCEnbUqWtxQ9pa/pQ==} engines: {node: '>=20.18.0'} @@ -1295,6 +2455,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/rpc@5.5.1': + resolution: {integrity: sha512-ku8zTUMrkCWci66PRIBC+1mXepEnZH/q1f3ck0kJZ95a06bOTl5KU7HeXWtskkyefzARJ5zvCs54AD5nxjQJ+A==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/signers@2.3.0': resolution: {integrity: sha512-OSv6fGr/MFRx6J+ZChQMRqKNPGGmdjkqarKkRzkwmv7v8quWsIRnJT5EV8tBy3LI4DLO/A8vKiNSPzvm1TdaiQ==} engines: {node: '>=20.18.0'} @@ -1307,6 +2476,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/signers@5.5.1': + resolution: {integrity: sha512-FY0IVaBT2kCAze55vEieR6hag4coqcuJ31Aw3hqRH7mv6sV8oqwuJmUrx+uFwOp1gwd5OEAzlv6N4hOOple4sQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/subscribable@2.3.0': resolution: {integrity: sha512-DkgohEDbMkdTWiKAoatY02Njr56WXx9e/dKKfmne8/Ad6/2llUIrax78nCdlvZW9quXMaXPTxZvdQqo9N669Og==} engines: {node: '>=20.18.0'} @@ -1319,6 +2497,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/subscribable@5.5.1': + resolution: {integrity: sha512-9K0PsynFq0CsmK1CDi5Y2vUIJpCqkgSS5yfDN0eKPgHqEptLEaia09Kaxc90cSZDZU5mKY/zv1NBmB6Aro9zQQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/sysvars@2.3.0': resolution: {integrity: sha512-LvjADZrpZ+CnhlHqfI5cmsRzX9Rpyb1Ox2dMHnbsRNzeKAMhu9w4ZBIaeTdO322zsTr509G1B+k2ABD3whvUBA==} engines: {node: '>=20.18.0'} @@ -1331,6 +2518,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/sysvars@5.5.1': + resolution: {integrity: sha512-k3Quq87Mm+geGUu1GWv6knPk0ALsfY6EKSJGw9xUJDHzY/RkYSBnh0RiOrUhtFm2TDNjOailg8/m0VHmi3reFA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/transaction-confirmation@2.3.0': resolution: {integrity: sha512-UiEuiHCfAAZEKdfne/XljFNJbsKAe701UQHKXEInYzIgBjRbvaeYZlBmkkqtxwcasgBTOmEaEKT44J14N9VZDw==} engines: {node: '>=20.18.0'} @@ -1343,6 +2539,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/transaction-confirmation@5.5.1': + resolution: {integrity: sha512-j4mKlYPHEyu+OD7MBt3jRoX4ScFgkhZC6H65on4Fux6LMScgivPJlwnKoZMnsgxFgWds0pl+BYzSiALDsXlYtw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/transaction-messages@2.3.0': resolution: {integrity: sha512-bgqvWuy3MqKS5JdNLH649q+ngiyOu5rGS3DizSnWwYUd76RxZl1kN6CoqHSrrMzFMvis6sck/yPGG3wqrMlAww==} engines: {node: '>=20.18.0'} @@ -1355,6 +2560,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/transaction-messages@5.5.1': + resolution: {integrity: sha512-aXyhMCEaAp3M/4fP0akwBBQkFPr4pfwoC5CLDq999r/FUwDax2RE/h4Ic7h2Xk+JdcUwsb+rLq85Y52hq84XvQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/transactions@2.3.0': resolution: {integrity: sha512-LnTvdi8QnrQtuEZor5Msje61sDpPstTVwKg4y81tNxDhiyomjuvnSNLAq6QsB9gIxUqbNzPZgOG9IU4I4/Uaug==} engines: {node: '>=20.18.0'} @@ -1367,6 +2581,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/transactions@5.5.1': + resolution: {integrity: sha512-8hHtDxtqalZ157pnx6p8k10D7J/KY/biLzfgh9R09VNLLY3Fqi7kJvJCr7M2ik3oRll56pxhraAGCC9yIT6eOA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/wallet-standard-features@1.3.0': resolution: {integrity: sha512-ZhpZtD+4VArf6RPitsVExvgkF+nGghd1rzPjd97GmBximpnt1rsUxMOEyoIEuH3XBxPyNB6Us7ha7RHWQR+abg==} engines: {node: '>=16'} @@ -1374,6 +2597,24 @@ packages: '@solana/web3.js@1.98.4': resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} + '@spruceid/siwe-parser@2.1.2': + resolution: {integrity: sha512-d/r3S1LwJyMaRAKQ0awmo9whfXeE88Qt00vRj91q5uv5ATtWIQEGJ67Yr5eSZw5zp1/fZCXZYuEckt8lSkereQ==} + + '@stablelib/binary@1.0.1': + resolution: {integrity: sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==} + + '@stablelib/int@1.0.1': + resolution: {integrity: sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==} + + '@stablelib/random@1.0.2': + resolution: {integrity: sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w==} + + '@stablelib/wipe@1.0.1': + resolution: {integrity: sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} @@ -1403,19 +2644,21 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@4.19.8': + resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} + '@types/express-serve-static-core@5.1.0': resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + '@types/express@5.0.5': resolution: {integrity: sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==} '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - '@types/ioredis@5.0.0': - resolution: {integrity: sha512-zJbJ3FVE17CNl5KXzdeSPtdltc4tMT3TzC6fxQS0sQngkbFZ6h+0uTafsRqu+eSLIugf6Yb0Ea0SUuRr42Nk9g==} - deprecated: This is a stub types definition. ioredis provides its own type definitions, so you do not need this installed. - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1437,8 +2680,8 @@ packages: '@types/node@22.19.0': resolution: {integrity: sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==} - '@types/node@24.10.0': - resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==} + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -1722,8 +2965,8 @@ packages: zod: optional: true - abitype@1.1.0: - resolution: {integrity: sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==} + abitype@1.1.1: + resolution: {integrity: sha512-Loe5/6tAgsBukY95eGaPSDmQHIjRZYQq8PB1MpsNccDIK8WiV+Uw6WzaIXipvaxTEL2yEB0OpEaQv3gs8pkS9Q==} peerDependencies: typescript: '>=5.0.4' zod: ^3.22.0 || ^4.0.0 @@ -1733,8 +2976,8 @@ packages: zod: optional: true - abitype@1.1.1: - resolution: {integrity: sha512-Loe5/6tAgsBukY95eGaPSDmQHIjRZYQq8PB1MpsNccDIK8WiV+Uw6WzaIXipvaxTEL2yEB0OpEaQv3gs8pkS9Q==} + abitype@1.2.3: + resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} peerDependencies: typescript: '>=5.0.4' zod: ^3.22.0 || ^4.0.0 @@ -1748,6 +2991,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1758,6 +3005,9 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + aes-js@4.0.0-beta.5: + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -1766,9 +3016,20 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1792,6 +3053,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + apg-js@4.4.0: + resolution: {integrity: sha512-fefmXFknJmtgtNEXfPwZKYkMFX4Fyeyz+fNF6JWp87biGOPslJbCBVU158zvKRZfHBKnJDy8CMM40oLFGkXT8Q==} + are-docs-informative@0.0.2: resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} engines: {node: '>=14'} @@ -1868,12 +3132,13 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + big.js@6.2.2: resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} - bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - bn.js@5.2.2: resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==} @@ -1881,6 +3146,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + borsh@0.7.0: resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} @@ -1944,6 +3213,9 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} + caniuse-lite@1.0.30001769: + resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1967,6 +3239,9 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} @@ -1974,10 +3249,6 @@ packages: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} engines: {node: '>=6'} - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} @@ -1993,9 +3264,6 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - comlink@4.4.2: - resolution: {integrity: sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==} - commander@14.0.0: resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} engines: {node: '>=20'} @@ -2029,6 +3297,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -2039,6 +3311,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} @@ -2046,6 +3322,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -2191,6 +3471,10 @@ packages: detect-browser@5.3.0: resolution: {integrity: sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dijkstrajs@1.0.3: resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} @@ -2422,9 +3706,9 @@ packages: ethereum-cryptography@2.2.1: resolution: {integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==} - ethereum-cryptography@3.2.0: - resolution: {integrity: sha512-Urr5YVsalH+Jo0sYkTkv1MyI9bLYZwW8BENZCeE1QYaTHETEYx0Nv/SVsWkSqpYrzweg6d8KMY1wTjH/1m/BIg==} - engines: {node: ^14.21.3 || >=16, npm: '>=9'} + ethers@6.16.0: + resolution: {integrity: sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==} + engines: {node: '>=14.0.0'} eventemitter2@6.4.9: resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} @@ -2436,14 +3720,32 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.2.1: + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + extension-port-stream@3.0.0: resolution: {integrity: sha512-an2S5quJMiy5bnZKEf6AkfH/7r8CzHvhchU40gxN+OM6HPhe7Z9T1FUychcf2M9PpPOO0Hf7BAEfJkw2TDIBDw==} engines: {node: '>=12.0.0'} @@ -2478,6 +3780,9 @@ packages: fast-stable-stringify@1.0.0: resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastestsmallesttextencoderdecoder@1.0.22: resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} @@ -2497,9 +3802,6 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2512,6 +3814,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -2559,6 +3865,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} 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} @@ -2578,6 +3888,10 @@ packages: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} + generic-pool@3.9.0: + resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} + engines: {node: '>= 4'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2607,6 +3921,7 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globals@14.0.0: @@ -2627,15 +3942,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - graphql-request@6.1.0: - resolution: {integrity: sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==} - peerDependencies: - graphql: 14 - 16 - - graphql@16.12.0: - resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} - engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - h3@1.15.4: resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} @@ -2670,9 +3976,9 @@ packages: resolution: {integrity: sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg==} engines: {node: '>=16.9.0'} - hot-shots@12.0.0: - resolution: {integrity: sha512-VdEXhNX4FYOY/Qx8sPcdR8+y/jXgwGlQdhk77dgFvJsVx+uS9WwiHEc+NKJqEQmd+0FTwy2/5FOtqe4cZ/JV5g==} - engines: {node: '>=16.0.0'} + hono@4.11.9: + resolution: {integrity: sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==} + engines: {node: '>=16.9.0'} html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} @@ -2682,6 +3988,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -2701,6 +4011,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + idb-keyval@6.2.1: resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} @@ -2737,6 +4051,10 @@ packages: resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} engines: {node: '>=12.22.0'} + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -2822,6 +4140,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -2898,12 +4219,12 @@ packages: engines: {node: '>=8'} hasBin: true - jose@5.10.0: - resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} - jose@6.1.0: resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==} + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -2941,6 +4262,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -3028,9 +4355,17 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3050,10 +4385,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -3099,9 +4442,6 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nan@2.24.0: - resolution: {integrity: sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==} - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3114,6 +4454,31 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + next@16.1.6: + resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + node-addon-api@2.0.2: resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} @@ -3201,8 +4566,8 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} - ox@0.4.4: - resolution: {integrity: sha512-oJPEeCDs9iNiPs6J0rTx+Y0KGeCGyCAA3zo94yZhm8G5WpOxrwUtn2Ie/Y8IyARSqqY/j9JTKA3Fc1xs1DvFnw==} + ox@0.11.3: + resolution: {integrity: sha512-1bWYGk/xZel3xro3l8WGg6eq4YEKlaqvyMtVhfMFpbJzK2F6rj4EDRtqDCWVEJMkzcmEi9uW2QxsqELokOlarw==} peerDependencies: typescript: '>=5.4.0' peerDependenciesMeta: @@ -3233,14 +4598,6 @@ packages: typescript: optional: true - ox@0.9.6: - resolution: {integrity: sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==} - peerDependencies: - typescript: '>=5.4.0' - peerDependenciesMeta: - typescript: - optional: true - p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -3299,6 +4656,9 @@ packages: path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -3339,6 +4699,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -3424,6 +4788,10 @@ packages: yaml: optional: true + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -3475,15 +4843,14 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - qrcode@1.5.4: - resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} - engines: {node: '>=10.13.0'} - hasBin: true - qs@6.13.0: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + query-string@7.1.3: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} @@ -3505,6 +4872,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-dom@19.2.0: resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} peerDependencies: @@ -3537,6 +4908,9 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} + redis@4.7.1: + resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -3549,6 +4923,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} @@ -3577,6 +4955,10 @@ packages: 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'} + rpc-websockets@9.3.1: resolution: {integrity: sha512-bY6a+i/lEtBJ/mUxwsCTgevoV1P0foXTVA7UoThzaIWbM+3NDqorf8NBWs5DmqKTFeA1IoNzgvkWjFCPgnzUiQ==} @@ -3631,10 +5013,18 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serve-static@1.16.2: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -3658,6 +5048,10 @@ packages: engines: {node: '>= 0.10'} hasBin: true + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3689,6 +5083,11 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + siwe@2.3.2: + resolution: {integrity: sha512-aSf+6+Latyttbj5nMu6GF3doMfv2UYj83hhwZgUF20ky6fTS83uVhkQABdIVnEuS8y1bBdk7p6ltb9SmlhTTlA==} + peerDependencies: + ethers: ^5.6.8 || ^6.0.8 + socket.io-client@4.8.1: resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} engines: {node: '>=10.0.0'} @@ -3736,6 +5135,10 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -3801,6 +5204,19 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} @@ -3829,9 +5245,6 @@ packages: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} - tailwind-merge@2.6.0: - resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} - text-encoding-utf-8@1.0.2: resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} @@ -3929,6 +5342,9 @@ packages: tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -3956,6 +5372,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -3964,6 +5383,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -3998,15 +5421,17 @@ packages: uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - unix-dgram@2.0.7: - resolution: {integrity: sha512-pWaQorcdxEUBFIKjCqqIlQaOoNVmchyoaNAJ/1LwyyfK2XSxcBhgJNiSE8ZRhR0xkNGyk4xInt1G03QPoKXY5A==} - engines: {node: '>=0.10.48'} + undici-types@7.21.0: + resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==} unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} @@ -4109,6 +5534,9 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + valid-url@1.0.9: + resolution: {integrity: sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==} + valtio@1.13.2: resolution: {integrity: sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==} engines: {node: '>=12.20.0'} @@ -4133,8 +5561,8 @@ packages: typescript: optional: true - viem@2.38.6: - resolution: {integrity: sha512-aqO6P52LPXRjdnP6rl5Buab65sYa4cZ6Cpn+k4OLOzVJhGIK8onTVoKMFMT04YjDfyDICa/DZyV9HmvLDgcjkw==} + viem@2.45.1: + resolution: {integrity: sha512-LN6Pp7vSfv50LgwhkfSbIXftAM5J89lP9x8TeDa8QM7o41IxlHrDh0F9X+FfnCWtsz11pEVV5sn+yBUoOHNqYA==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: @@ -4253,6 +5681,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} @@ -4364,6 +5793,18 @@ packages: utf-8-validate: optional: true + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -4382,6 +5823,9 @@ packages: y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -4394,6 +5838,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} @@ -4459,6 +5908,8 @@ packages: snapshots: + '@adraffy/ens-normalize@1.10.1': {} + '@adraffy/ens-normalize@1.11.1': {} '@asamuzakjp/css-color@3.2.0': @@ -4479,7 +5930,7 @@ snapshots: idb-keyval: 6.2.1 ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.3(@types/react@19.2.2)(react@19.2.0)(use-sync-external-store@1.4.0(react@19.2.0)) transitivePeerDependencies: - '@types/react' @@ -4491,16 +5942,16 @@ snapshots: - utf-8-validate - zod - '@base-org/account@2.4.0(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)': + '@base-org/account@2.4.0(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)': dependencies: - '@coinbase/cdp-sdk': 1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@coinbase/cdp-sdk': 1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.3(@types/react@19.2.2)(react@19.2.0)(use-sync-external-store@1.4.0(react@19.2.0)) transitivePeerDependencies: - '@types/react' @@ -4516,11 +5967,11 @@ snapshots: - ws - zod - '@coinbase/cdp-sdk@1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@coinbase/cdp-sdk@1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: - '@solana-program/system': 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/system': 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) axios: 1.13.2 @@ -4528,7 +5979,7 @@ snapshots: jose: 6.1.0 md5: 2.3.0 uncrypto: 0.1.3 - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - bufferutil @@ -4539,59 +5990,51 @@ snapshots: - utf-8-validate - ws - '@coinbase/onchainkit@0.38.19(@farcaster/miniapp-sdk@0.2.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)': + '@coinbase/cdp-sdk@1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: - '@farcaster/frame-sdk': 0.1.12(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@farcaster/miniapp-wagmi-connector': 1.1.0(@farcaster/miniapp-sdk@0.2.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@tanstack/react-query': 5.90.7(react@19.2.0) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - clsx: 2.1.1 - graphql: 16.12.0 - graphql-request: 6.1.0(graphql@16.12.0) - qrcode: 1.5.4 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - tailwind-merge: 2.6.0 - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - wagmi: 2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + '@solana-program/system': 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) + abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) + axios: 1.13.2 + axios-retry: 4.5.0(axios@1.13.2) + jose: 6.1.0 + md5: 2.3.0 + uncrypto: 0.1.3 + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - bufferutil + - debug + - encoding + - fastestsmallesttextencoderdecoder + - typescript + - utf-8-validate + - ws + + '@coinbase/cdp-sdk@1.38.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana-program/system': 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) + abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) + axios: 1.13.2 + axios-retry: 4.5.0(axios@1.13.2) + jose: 6.1.0 + md5: 2.3.0 + uncrypto: 0.1.3 + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + zod: 3.25.76 transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@farcaster/miniapp-sdk' - - '@netlify/blobs' - - '@planetscale/database' - - '@react-native-async-storage/async-storage' - - '@tanstack/query-core' - - '@types/react' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - bufferutil - - db0 - debug - encoding - - expo-auth-session - - expo-crypto - - expo-web-browser - fastestsmallesttextencoderdecoder - - immer - - ioredis - - react-native - - supports-color - typescript - - uploadthing - - use-sync-external-store - utf-8-validate - ws - - zod '@coinbase/wallet-sdk@3.9.3': dependencies: @@ -4615,7 +6058,7 @@ snapshots: idb-keyval: 6.2.1 ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.3(@types/react@19.2.2)(react@19.2.0)(use-sync-external-store@1.4.0(react@19.2.0)) transitivePeerDependencies: - '@types/react' @@ -4662,6 +6105,11 @@ snapshots: dependencies: '@noble/ciphers': 1.3.0 + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + '@es-joy/jsdoccomment@0.50.2': dependencies: '@types/estree': 1.0.8 @@ -4814,82 +6262,25 @@ snapshots: ethereum-cryptography: 2.2.1 micro-ftch: 0.3.1 - '@farcaster/frame-sdk@0.1.12(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': - dependencies: - '@farcaster/miniapp-sdk': 0.2.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@farcaster/quick-auth': 0.0.8(typescript@5.9.3) - comlink: 4.4.2 - eventemitter3: 5.0.1 - ox: 0.4.4(typescript@5.9.3)(zod@3.25.76) - transitivePeerDependencies: - - bufferutil - - encoding - - typescript - - utf-8-validate - - zod - - '@farcaster/miniapp-core@0.4.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) - ox: 0.4.4(typescript@5.9.3)(zod@3.25.76) - zod: 3.25.76 - transitivePeerDependencies: - - bufferutil - - encoding - - typescript - - utf-8-validate - - '@farcaster/miniapp-sdk@0.2.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': - dependencies: - '@farcaster/miniapp-core': 0.4.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@farcaster/quick-auth': 0.0.6(typescript@5.9.3) - comlink: 4.4.2 - eventemitter3: 5.0.1 - ox: 0.4.4(typescript@5.9.3)(zod@3.25.76) - transitivePeerDependencies: - - bufferutil - - encoding - - typescript - - utf-8-validate - - zod - - '@farcaster/miniapp-wagmi-connector@1.1.0(@farcaster/miniapp-sdk@0.2.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': - dependencies: - '@farcaster/miniapp-sdk': 0.2.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - - '@farcaster/quick-auth@0.0.6(typescript@5.9.3)': - dependencies: - jose: 5.10.0 - typescript: 5.9.3 - zod: 3.25.76 - - '@farcaster/quick-auth@0.0.8(typescript@5.9.3)': - dependencies: - jose: 5.10.0 - typescript: 5.9.3 - zod: 3.25.76 - - '@gemini-wallet/core@0.2.0(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@gemini-wallet/core@0.2.0(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@metamask/rpc-errors': 7.0.2 eventemitter3: 5.0.1 - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - supports-color - '@gemini-wallet/core@0.3.1(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@gemini-wallet/core@0.3.1(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@metamask/rpc-errors': 7.0.2 eventemitter3: 5.0.1 - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - supports-color - '@graphql-typed-document-node/core@3.2.0(graphql@16.12.0)': + '@hono/node-server@1.19.9(hono@4.11.9)': dependencies: - graphql: 16.12.0 + hono: 4.11.9 '@humanfs/core@0.19.1': {} @@ -4902,7 +6293,105 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@ioredis/commands@1.4.0': {} + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@ioredis/commands@1.4.0': + optional: true '@isaacs/cliui@8.0.2': dependencies: @@ -4933,13 +6422,6 @@ snapshots: dependencies: '@lit-labs/ssr-dom-shim': 1.4.0 - '@metamask/abi-utils@2.0.4': - dependencies: - '@metamask/superstruct': 3.2.1 - '@metamask/utils': 9.3.0 - transitivePeerDependencies: - - supports-color - '@metamask/eth-json-rpc-provider@1.0.1': dependencies: '@metamask/json-rpc-engine': 7.3.3 @@ -5125,10 +6607,62 @@ snapshots: transitivePeerDependencies: - supports-color + '@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.9) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.11.9 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@next/env@16.1.6': {} + + '@next/swc-darwin-arm64@16.1.6': + optional: true + + '@next/swc-darwin-x64@16.1.6': + optional: true + + '@next/swc-linux-arm64-gnu@16.1.6': + optional: true + + '@next/swc-linux-arm64-musl@16.1.6': + optional: true + + '@next/swc-linux-x64-gnu@16.1.6': + optional: true + + '@next/swc-linux-x64-musl@16.1.6': + optional: true + + '@next/swc-win32-arm64-msvc@16.1.6': + optional: true + + '@next/swc-win32-x64-msvc@16.1.6': + optional: true + '@noble/ciphers@1.2.1': {} '@noble/ciphers@1.3.0': {} + '@noble/curves@1.2.0': + dependencies: + '@noble/hashes': 1.3.2 + '@noble/curves@1.4.2': dependencies: '@noble/hashes': 1.4.0 @@ -5141,10 +6675,6 @@ snapshots: dependencies: '@noble/hashes': 1.7.1 - '@noble/curves@1.9.0': - dependencies: - '@noble/hashes': 1.8.0 - '@noble/curves@1.9.1': dependencies: '@noble/hashes': 1.8.0 @@ -5153,6 +6683,8 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@noble/hashes@1.3.2': {} + '@noble/hashes@1.4.0': {} '@noble/hashes@1.7.0': {} @@ -5173,13 +6705,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@openzeppelin/merkle-tree@1.0.8': - dependencies: - '@metamask/abi-utils': 2.0.4 - ethereum-cryptography: 3.2.0 - transitivePeerDependencies: - - supports-color - '@paulmillr/qr@0.2.1': {} '@pkgjs/parseargs@0.11.0': @@ -5187,11 +6712,37 @@ snapshots: '@pkgr/core@0.2.9': {} + '@redis/bloom@1.2.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/client@1.6.1': + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + + '@redis/graph@1.1.1(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/json@1.0.7(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/search@1.2.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/time-series@1.1.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + '@reown/appkit-common@1.7.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - bufferutil - typescript @@ -5202,7 +6753,7 @@ snapshots: dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript @@ -5215,7 +6766,7 @@ snapshots: '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(ioredis@5.8.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@19.2.2)(react@19.2.0) - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -5365,7 +6916,7 @@ snapshots: '@walletconnect/logger': 2.1.2 '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(ioredis@5.8.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@19.2.2)(react@19.2.0) - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -5419,7 +6970,7 @@ snapshots: '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(ioredis@5.8.2)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: 6.0.0 valtio: 1.13.2(@types/react@19.2.2)(react@19.2.0) - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -5529,7 +7080,7 @@ snapshots: '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.23.1 - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript @@ -5556,47 +7107,76 @@ snapshots: '@scure/bip32@1.7.0': dependencies: - '@noble/curves': 1.9.1 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@scure/bip39@1.3.0': dependencies: - '@noble/hashes': 1.4.0 - '@scure/base': 1.1.9 + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + + '@scure/bip39@1.5.4': + dependencies: + '@noble/hashes': 1.7.1 + '@scure/base': 1.2.6 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@socket.io/component-emitter@3.1.2': {} + + '@solana-program/compute-budget@0.11.0(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))': + dependencies: + '@solana/kit': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + + '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@scure/bip39@1.5.4': + '@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@noble/hashes': 1.7.1 - '@scure/base': 1.2.6 + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@scure/bip39@1.6.0': + '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))': dependencies: - '@noble/hashes': 1.8.0 - '@scure/base': 1.2.6 + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/sysvars': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@socket.io/component-emitter@3.1.2': {} + '@solana-program/token-2022@0.6.1(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))': + dependencies: + '@solana/kit': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/sysvars': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))': + '@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/token@0.9.0(@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))': dependencies: - '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) '@solana/accounts@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: @@ -5622,6 +7202,19 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/accounts@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/rpc-spec': 5.5.1(typescript@5.9.3) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/addresses@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/assertions': 2.3.0(typescript@5.9.3) @@ -5644,6 +7237,18 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/addresses@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/assertions': 5.5.1(typescript@5.9.3) + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/nominal-types': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/assertions@2.3.0(typescript@5.9.3)': dependencies: '@solana/errors': 2.3.0(typescript@5.9.3) @@ -5654,6 +7259,12 @@ snapshots: '@solana/errors': 3.0.3(typescript@5.9.3) typescript: 5.9.3 + '@solana/assertions@5.5.1(typescript@5.9.3)': + dependencies: + '@solana/errors': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + '@solana/buffer-layout@4.0.1': dependencies: buffer: 6.0.3 @@ -5668,6 +7279,12 @@ snapshots: '@solana/errors': 3.0.3(typescript@5.9.3) typescript: 5.9.3 + '@solana/codecs-core@5.5.1(typescript@5.9.3)': + dependencies: + '@solana/errors': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + '@solana/codecs-data-structures@2.3.0(typescript@5.9.3)': dependencies: '@solana/codecs-core': 2.3.0(typescript@5.9.3) @@ -5682,6 +7299,14 @@ snapshots: '@solana/errors': 3.0.3(typescript@5.9.3) typescript: 5.9.3 + '@solana/codecs-data-structures@5.5.1(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + '@solana/codecs-numbers@2.3.0(typescript@5.9.3)': dependencies: '@solana/codecs-core': 2.3.0(typescript@5.9.3) @@ -5694,6 +7319,13 @@ snapshots: '@solana/errors': 3.0.3(typescript@5.9.3) typescript: 5.9.3 + '@solana/codecs-numbers@5.5.1(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + '@solana/codecs-strings@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/codecs-core': 2.3.0(typescript@5.9.3) @@ -5710,6 +7342,15 @@ snapshots: fastestsmallesttextencoderdecoder: 1.0.22 typescript: 5.9.3 + '@solana/codecs-strings@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + optionalDependencies: + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.9.3 + '@solana/codecs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/codecs-core': 2.3.0(typescript@5.9.3) @@ -5732,6 +7373,18 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/codecs@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) + '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) + '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/options': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/errors@2.3.0(typescript@5.9.3)': dependencies: chalk: 5.6.2 @@ -5744,6 +7397,13 @@ snapshots: commander: 14.0.0 typescript: 5.9.3 + '@solana/errors@5.5.1(typescript@5.9.3)': + dependencies: + chalk: 5.6.2 + commander: 14.0.2 + optionalDependencies: + typescript: 5.9.3 + '@solana/fast-stable-stringify@2.3.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -5752,6 +7412,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@solana/fast-stable-stringify@5.5.1(typescript@5.9.3)': + optionalDependencies: + typescript: 5.9.3 + '@solana/functional@2.3.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -5760,6 +7424,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@solana/functional@5.5.1(typescript@5.9.3)': + optionalDependencies: + typescript: 5.9.3 + '@solana/instruction-plans@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/errors': 3.0.3(typescript@5.9.3) @@ -5771,6 +7439,19 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/instruction-plans@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/instructions': 5.5.1(typescript@5.9.3) + '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/promises': 5.5.1(typescript@5.9.3) + '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/instructions@2.3.0(typescript@5.9.3)': dependencies: '@solana/codecs-core': 2.3.0(typescript@5.9.3) @@ -5783,6 +7464,13 @@ snapshots: '@solana/errors': 3.0.3(typescript@5.9.3) typescript: 5.9.3 + '@solana/instructions@5.5.1(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + '@solana/keys@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/assertions': 2.3.0(typescript@5.9.3) @@ -5805,6 +7493,68 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/keys@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/assertions': 5.5.1(typescript@5.9.3) + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/nominal-types': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/functional': 2.3.0(typescript@5.9.3) + '@solana/instructions': 2.3.0(typescript@5.9.3) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/functional': 2.3.0(typescript@5.9.3) + '@solana/instructions': 2.3.0(typescript@5.9.3) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -5830,7 +7580,59 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/functional': 3.0.3(typescript@5.9.3) + '@solana/instruction-plans': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/instructions': 3.0.3(typescript@5.9.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/programs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/signers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/functional': 3.0.3(typescript@5.9.3) + '@solana/instruction-plans': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/instructions': 3.0.3(typescript@5.9.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/programs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/signers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -5844,11 +7646,11 @@ snapshots: '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.3) '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/signers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) typescript: 5.9.3 @@ -5856,6 +7658,37 @@ snapshots: - fastestsmallesttextencoderdecoder - ws + '@solana/kit@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/accounts': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/functional': 5.5.1(typescript@5.9.3) + '@solana/instruction-plans': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/instructions': 5.5.1(typescript@5.9.3) + '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/offchain-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/plugin-core': 5.5.1(typescript@5.9.3) + '@solana/programs': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-api': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 5.5.1(typescript@5.9.3) + '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) + '@solana/rpc-subscriptions': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/signers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/sysvars': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-confirmation': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - fastestsmallesttextencoderdecoder + - utf-8-validate + '@solana/nominal-types@2.3.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -5864,6 +7697,25 @@ snapshots: dependencies: typescript: 5.9.3 + '@solana/nominal-types@5.5.1(typescript@5.9.3)': + optionalDependencies: + typescript: 5.9.3 + + '@solana/offchain-messages@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) + '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) + '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/nominal-types': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/options@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/codecs-core': 2.3.0(typescript@5.9.3) @@ -5886,6 +7738,22 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/options@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) + '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) + '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/plugin-core@5.5.1(typescript@5.9.3)': + optionalDependencies: + typescript: 5.9.3 + '@solana/programs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -5902,6 +7770,15 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/programs@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/promises@2.3.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -5910,6 +7787,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@solana/promises@5.5.1(typescript@5.9.3)': + optionalDependencies: + typescript: 5.9.3 + '@solana/rpc-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -5944,6 +7825,24 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/rpc-api@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 5.5.1(typescript@5.9.3) + '@solana/rpc-spec': 5.5.1(typescript@5.9.3) + '@solana/rpc-transformers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/rpc-parsed-types@2.3.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -5952,6 +7851,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@solana/rpc-parsed-types@5.5.1(typescript@5.9.3)': + optionalDependencies: + typescript: 5.9.3 + '@solana/rpc-spec-types@2.3.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -5960,6 +7863,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@solana/rpc-spec-types@5.5.1(typescript@5.9.3)': + optionalDependencies: + typescript: 5.9.3 + '@solana/rpc-spec@2.3.0(typescript@5.9.3)': dependencies: '@solana/errors': 2.3.0(typescript@5.9.3) @@ -5972,6 +7879,13 @@ snapshots: '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) typescript: 5.9.3 + '@solana/rpc-spec@5.5.1(typescript@5.9.3)': + dependencies: + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + '@solana/rpc-subscriptions-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -5995,26 +7909,89 @@ snapshots: '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) typescript: 5.9.3 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-subscriptions-api@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 5.5.1(typescript@5.9.3) + '@solana/rpc-transformers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/functional': 2.3.0(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) + '@solana/subscribable': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/functional': 2.3.0(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) + '@solana/subscribable': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/functional': 2.3.0(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) + '@solana/subscribable': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/functional': 3.0.3(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) + '@solana/subscribable': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) - '@solana/subscribable': 2.3.0(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/functional': 3.0.3(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) + '@solana/subscribable': 3.0.3(typescript@5.9.3) typescript: 5.9.3 - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 3.0.3(typescript@5.9.3) '@solana/functional': 3.0.3(typescript@5.9.3) '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) '@solana/subscribable': 3.0.3(typescript@5.9.3) typescript: 5.9.3 - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + '@solana/rpc-subscriptions-channel-websocket@5.5.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/functional': 5.5.1(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 5.5.1(typescript@5.9.3) + '@solana/subscribable': 5.5.1(typescript@5.9.3) + ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate '@solana/rpc-subscriptions-spec@2.3.0(typescript@5.9.3)': dependencies: @@ -6032,6 +8009,51 @@ snapshots: '@solana/subscribable': 3.0.3(typescript@5.9.3) typescript: 5.9.3 + '@solana/rpc-subscriptions-spec@5.5.1(typescript@5.9.3)': + dependencies: + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/promises': 5.5.1(typescript@5.9.3) + '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) + '@solana/subscribable': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.3) + '@solana/functional': 2.3.0(typescript@5.9.3) + '@solana/promises': 2.3.0(typescript@5.9.3) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/subscribable': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.3) + '@solana/functional': 2.3.0(typescript@5.9.3) + '@solana/promises': 2.3.0(typescript@5.9.3) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/subscribable': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.3.0(typescript@5.9.3) @@ -6050,7 +8072,43 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/fast-stable-stringify': 3.0.3(typescript@5.9.3) + '@solana/functional': 3.0.3(typescript@5.9.3) + '@solana/promises': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-subscriptions-api': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions-channel-websocket': 3.0.3(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) + '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/subscribable': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/fast-stable-stringify': 3.0.3(typescript@5.9.3) + '@solana/functional': 3.0.3(typescript@5.9.3) + '@solana/promises': 3.0.3(typescript@5.9.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) + '@solana/rpc-subscriptions-api': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions-channel-websocket': 3.0.3(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) + '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/subscribable': 3.0.3(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 3.0.3(typescript@5.9.3) '@solana/fast-stable-stringify': 3.0.3(typescript@5.9.3) @@ -6058,7 +8116,7 @@ snapshots: '@solana/promises': 3.0.3(typescript@5.9.3) '@solana/rpc-spec-types': 3.0.3(typescript@5.9.3) '@solana/rpc-subscriptions-api': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 3.0.3(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-channel-websocket': 3.0.3(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.9.3) '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -6068,6 +8126,26 @@ snapshots: - fastestsmallesttextencoderdecoder - ws + '@solana/rpc-subscriptions@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/fast-stable-stringify': 5.5.1(typescript@5.9.3) + '@solana/functional': 5.5.1(typescript@5.9.3) + '@solana/promises': 5.5.1(typescript@5.9.3) + '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) + '@solana/rpc-subscriptions-api': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions-channel-websocket': 5.5.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/rpc-subscriptions-spec': 5.5.1(typescript@5.9.3) + '@solana/rpc-transformers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/subscribable': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - fastestsmallesttextencoderdecoder + - utf-8-validate + '@solana/rpc-transformers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/errors': 2.3.0(typescript@5.9.3) @@ -6090,6 +8168,18 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/rpc-transformers@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/functional': 5.5.1(typescript@5.9.3) + '@solana/nominal-types': 5.5.1(typescript@5.9.3) + '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/rpc-transport-http@2.3.0(typescript@5.9.3)': dependencies: '@solana/errors': 2.3.0(typescript@5.9.3) @@ -6106,6 +8196,15 @@ snapshots: typescript: 5.9.3 undici-types: 7.16.0 + '@solana/rpc-transport-http@5.5.1(typescript@5.9.3)': + dependencies: + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/rpc-spec': 5.5.1(typescript@5.9.3) + '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) + undici-types: 7.21.0 + optionalDependencies: + typescript: 5.9.3 + '@solana/rpc-types@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -6130,6 +8229,19 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/rpc-types@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) + '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/nominal-types': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/rpc@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/errors': 2.3.0(typescript@5.9.3) @@ -6160,6 +8272,22 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/rpc@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/fast-stable-stringify': 5.5.1(typescript@5.9.3) + '@solana/functional': 5.5.1(typescript@5.9.3) + '@solana/rpc-api': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-spec': 5.5.1(typescript@5.9.3) + '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) + '@solana/rpc-transformers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-transport-http': 5.5.1(typescript@5.9.3) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/signers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -6188,6 +8316,22 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/signers@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/instructions': 5.5.1(typescript@5.9.3) + '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/nominal-types': 5.5.1(typescript@5.9.3) + '@solana/offchain-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/subscribable@2.3.0(typescript@5.9.3)': dependencies: '@solana/errors': 2.3.0(typescript@5.9.3) @@ -6198,6 +8342,12 @@ snapshots: '@solana/errors': 3.0.3(typescript@5.9.3) typescript: 5.9.3 + '@solana/subscribable@5.5.1(typescript@5.9.3)': + dependencies: + '@solana/errors': 5.5.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + '@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -6218,6 +8368,51 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/accounts': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/promises': 2.3.0(typescript@5.9.3) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/promises': 2.3.0(typescript@5.9.3) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -6235,7 +8430,41 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/promises': 3.0.3(typescript@5.9.3) + '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 3.0.3(typescript@5.9.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/promises': 3.0.3(typescript@5.9.3) + '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -6243,7 +8472,7 @@ snapshots: '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/promises': 3.0.3(typescript@5.9.3) '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -6252,6 +8481,25 @@ snapshots: - fastestsmallesttextencoderdecoder - ws + '@solana/transaction-confirmation@5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/promises': 5.5.1(typescript@5.9.3) + '@solana/rpc': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions': 5.5.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - fastestsmallesttextencoderdecoder + - utf-8-validate + '@solana/transaction-messages@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -6282,6 +8530,22 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/transaction-messages@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) + '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/functional': 5.5.1(typescript@5.9.3) + '@solana/instructions': 5.5.1(typescript@5.9.3) + '@solana/nominal-types': 5.5.1(typescript@5.9.3) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/transactions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) @@ -6318,6 +8582,25 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/transactions@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 5.5.1(typescript@5.9.3) + '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) + '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) + '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/functional': 5.5.1(typescript@5.9.3) + '@solana/instructions': 5.5.1(typescript@5.9.3) + '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/nominal-types': 5.5.1(typescript@5.9.3) + '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/wallet-standard-features@1.3.0': dependencies: '@wallet-standard/base': 1.1.0 @@ -6327,7 +8610,7 @@ snapshots: dependencies: '@babel/runtime': 7.28.4 '@noble/curves': 1.9.7 - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.8.0 '@solana/buffer-layout': 4.0.1 '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) agentkeepalive: 4.6.0 @@ -6346,6 +8629,30 @@ snapshots: - typescript - utf-8-validate + '@spruceid/siwe-parser@2.1.2': + dependencies: + '@noble/hashes': 1.8.0 + apg-js: 4.4.0 + uri-js: 4.4.1 + valid-url: 1.0.9 + + '@stablelib/binary@1.0.1': + dependencies: + '@stablelib/int': 1.0.1 + + '@stablelib/int@1.0.1': {} + + '@stablelib/random@1.0.2': + dependencies: + '@stablelib/binary': 1.0.1 + '@stablelib/wipe': 1.0.1 + + '@stablelib/wipe@1.0.1': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + '@swc/helpers@0.5.17': dependencies: tslib: 2.8.1 @@ -6360,7 +8667,7 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 24.10.0 + '@types/node': 22.19.0 '@types/chai@5.2.3': dependencies: @@ -6369,7 +8676,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 24.10.0 + '@types/node': 22.19.0 '@types/debug@4.1.12': dependencies: @@ -6379,13 +8686,27 @@ snapshots: '@types/estree@1.0.8': {} + '@types/express-serve-static-core@4.19.8': + dependencies: + '@types/node': 22.19.0 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + '@types/express-serve-static-core@5.1.0': dependencies: - '@types/node': 24.10.0 + '@types/node': 22.19.0 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.8 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.10 + '@types/express@5.0.5': dependencies: '@types/body-parser': 1.19.6 @@ -6394,12 +8715,6 @@ snapshots: '@types/http-errors@2.0.5': {} - '@types/ioredis@5.0.0': - dependencies: - ioredis: 5.8.2 - transitivePeerDependencies: - - supports-color - '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -6416,9 +8731,9 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@24.10.0': + '@types/node@22.7.5': dependencies: - undici-types: 7.16.0 + undici-types: 6.19.8 '@types/qs@6.14.0': {} @@ -6435,16 +8750,16 @@ snapshots: '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 - '@types/node': 24.10.0 + '@types/node': 22.19.0 '@types/send@1.2.1': dependencies: - '@types/node': 24.10.0 + '@types/node': 22.19.0 '@types/serve-static@1.15.10': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 24.10.0 + '@types/node': 22.19.0 '@types/send': 0.17.6 '@types/trusted-types@2.0.7': {} @@ -6594,19 +8909,19 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@wagmi/connectors@5.11.2(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(zod@3.25.76)': + '@wagmi/connectors@5.11.2(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(zod@3.25.76)': dependencies: '@base-org/account': 1.1.1(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(zod@3.25.76) '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(zod@3.25.76) - '@gemini-wallet/core': 0.2.0(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@gemini-wallet/core': 0.2.0(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.19(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + porto: 0.2.19(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -6641,19 +8956,19 @@ snapshots: - wagmi - zod - '@wagmi/connectors@6.1.3(9bec34897937062261cacb99e1720c4a)': + '@wagmi/connectors@6.1.3(5bf9f274321a9402fd289aeb8002985e)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + '@base-org/account': 2.4.0(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.2)(bufferutil@4.0.9)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(utf-8-validate@5.0.10)(zod@3.25.76) - '@gemini-wallet/core': 0.3.1(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@gemini-wallet/core': 0.3.1(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.2)(bufferutil@4.0.9)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + porto: 0.2.35(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -6695,11 +9010,11 @@ snapshots: - ws - zod - '@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.0(@types/react@19.2.2)(react@19.2.0)(use-sync-external-store@1.4.0(react@19.2.0)) optionalDependencies: '@tanstack/query-core': 5.90.7 @@ -7267,22 +9582,27 @@ snapshots: typescript: 5.9.3 zod: 3.25.76 - abitype@1.1.0(typescript@5.9.3)(zod@3.22.4): + abitype@1.1.1(typescript@5.9.3)(zod@3.25.76): optionalDependencies: typescript: 5.9.3 - zod: 3.22.4 + zod: 3.25.76 + + abitype@1.1.1(typescript@5.9.3)(zod@4.1.12): + optionalDependencies: + typescript: 5.9.3 + zod: 4.1.12 - abitype@1.1.0(typescript@5.9.3)(zod@3.25.76): + abitype@1.2.3(typescript@5.9.3)(zod@3.22.4): optionalDependencies: typescript: 5.9.3 - zod: 3.25.76 + zod: 3.22.4 - abitype@1.1.1(typescript@5.9.3)(zod@3.25.76): + abitype@1.2.3(typescript@5.9.3)(zod@3.25.76): optionalDependencies: typescript: 5.9.3 zod: 3.25.76 - abitype@1.1.1(typescript@5.9.3)(zod@4.1.12): + abitype@1.2.3(typescript@5.9.3)(zod@4.1.12): optionalDependencies: typescript: 5.9.3 zod: 4.1.12 @@ -7292,18 +9612,29 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 acorn@8.15.0: {} + aes-js@4.0.0-beta.5: {} + agent-base@7.1.4: {} agentkeepalive@4.6.0: dependencies: humanize-ms: 1.2.1 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -7311,6 +9642,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -7328,6 +9666,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + apg-js@4.4.0: {} + are-docs-informative@0.0.2: {} argparse@2.0.1: {} @@ -7423,12 +9763,9 @@ snapshots: base64-js@1.5.1: {} - big.js@6.2.2: {} + baseline-browser-mapping@2.9.19: {} - bindings@1.5.0: - dependencies: - file-uri-to-path: 1.0.0 - optional: true + big.js@6.2.2: {} bn.js@5.2.2: {} @@ -7449,6 +9786,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.0 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + borsh@0.7.0: dependencies: bn.js: 5.2.2 @@ -7517,6 +9868,8 @@ snapshots: camelcase@5.3.1: {} + caniuse-lite@1.0.30001769: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -7540,6 +9893,8 @@ snapshots: dependencies: readdirp: 4.1.2 + client-only@0.0.1: {} + cliui@6.0.0: dependencies: string-width: 4.2.3 @@ -7548,8 +9903,6 @@ snapshots: clsx@1.2.1: {} - clsx@2.1.1: {} - cluster-key-slot@1.1.2: {} color-convert@2.0.1: @@ -7562,8 +9915,6 @@ snapshots: dependencies: delayed-stream: 1.0.0 - comlink@4.4.2: {} - commander@14.0.0: {} commander@14.0.2: {} @@ -7584,16 +9935,25 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.1: {} + content-type@1.0.5: {} cookie-es@1.2.2: {} cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.7.1: {} core-util-is@1.0.3: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + crc-32@1.2.2: {} cross-fetch@3.2.0: @@ -7700,7 +10060,8 @@ snapshots: delayed-stream@1.0.0: {} - denque@2.1.0: {} + denque@2.1.0: + optional: true depd@2.0.0: {} @@ -7714,6 +10075,9 @@ snapshots: detect-browser@5.3.0: {} + detect-libc@2.1.2: + optional: true + dijkstrajs@1.0.3: {} doctrine@2.1.0: @@ -8077,13 +10441,18 @@ snapshots: '@scure/bip32': 1.4.0 '@scure/bip39': 1.3.0 - ethereum-cryptography@3.2.0: + ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: - '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.0 - '@noble/hashes': 1.8.0 - '@scure/bip32': 1.7.0 - '@scure/bip39': 1.6.0 + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate eventemitter2@6.4.9: {} @@ -8091,8 +10460,19 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + expect-type@1.2.2: {} + express-rate-limit@8.2.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.0.1 + express@4.21.2: dependencies: accepts: 1.3.8 @@ -8129,6 +10509,39 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.1 + 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.0 + 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.7 + qs: 6.14.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extension-port-stream@3.0.0: dependencies: readable-stream: 3.6.2 @@ -8158,6 +10571,8 @@ snapshots: fast-stable-stringify@1.0.0: {} + fast-uri@3.1.0: {} + fastestsmallesttextencoderdecoder@1.0.22: {} fastq@1.19.1: @@ -8172,9 +10587,6 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-uri-to-path@1.0.0: - optional: true - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -8193,6 +10605,17 @@ snapshots: transitivePeerDependencies: - supports-color + 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.1 + transitivePeerDependencies: + - supports-color + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -8239,6 +10662,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + fsevents@2.3.3: optional: true @@ -8257,6 +10682,8 @@ snapshots: generator-function@2.0.1: {} + generic-pool@3.9.0: {} + get-caller-file@2.0.5: {} get-intrinsic@1.3.0: @@ -8317,16 +10744,6 @@ snapshots: graphemer@1.4.0: {} - graphql-request@6.1.0(graphql@16.12.0): - dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.12.0) - cross-fetch: 3.2.0 - graphql: 16.12.0 - transitivePeerDependencies: - - encoding - - graphql@16.12.0: {} - h3@1.15.4: dependencies: cookie-es: 1.2.2 @@ -8363,9 +10780,7 @@ snapshots: hono@4.10.4: {} - hot-shots@12.0.0: - optionalDependencies: - unix-dgram: 2.0.7 + hono@4.11.9: {} html-encoding-sniffer@4.0.0: dependencies: @@ -8379,6 +10794,14 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + 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 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -8405,6 +10828,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + idb-keyval@6.2.1: {} idb-keyval@6.2.2: {} @@ -8443,6 +10870,9 @@ snapshots: standard-as-callback: 2.1.0 transitivePeerDependencies: - supports-color + optional: true + + ip-address@10.0.1: {} ipaddr.js@1.9.1: {} @@ -8528,6 +10958,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -8613,10 +11045,10 @@ snapshots: - bufferutil - utf-8-validate - jose@5.10.0: {} - jose@6.1.0: {} + jose@6.1.3: {} + joycon@3.1.1: {} js-tokens@9.0.1: {} @@ -8665,6 +11097,10 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-safe@5.0.1: {} @@ -8720,9 +11156,11 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.defaults@4.2.0: {} + lodash.defaults@4.2.0: + optional: true - lodash.isarguments@3.1.0: {} + lodash.isarguments@3.1.0: + optional: true lodash.merge@4.6.2: {} @@ -8748,8 +11186,12 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} methods@1.1.2: {} @@ -8763,10 +11205,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} minimatch@3.1.2: @@ -8806,15 +11254,38 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nan@2.24.0: - optional: true - nanoid@3.3.11: {} natural-compare@1.4.0: {} negotiator@0.6.3: {} + negotiator@1.0.0: {} + + next@16.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + '@next/env': 16.1.6 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001769 + postcss: 8.4.31 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + styled-jsx: 5.1.6(react@19.2.0) + optionalDependencies: + '@next/swc-darwin-arm64': 16.1.6 + '@next/swc-darwin-x64': 16.1.6 + '@next/swc-linux-arm64-gnu': 16.1.6 + '@next/swc-linux-arm64-musl': 16.1.6 + '@next/swc-linux-x64-gnu': 16.1.6 + '@next/swc-linux-x64-musl': 16.1.6 + '@next/swc-win32-arm64-msvc': 16.1.6 + '@next/swc-win32-x64-msvc': 16.1.6 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + node-addon-api@2.0.2: {} node-fetch-native@1.6.7: {} @@ -8909,79 +11380,80 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 - ox@0.4.4(typescript@5.9.3)(zod@3.25.76): + ox@0.11.3(typescript@5.9.3)(zod@3.22.4): dependencies: '@adraffy/ens-normalize': 1.11.1 - '@noble/curves': 1.9.7 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.1(typescript@5.9.3)(zod@3.25.76) + abitype: 1.2.3(typescript@5.9.3)(zod@3.22.4) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - zod - ox@0.6.7(typescript@5.9.3)(zod@3.25.76): + ox@0.11.3(typescript@5.9.3)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.11.1 - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 - '@scure/bip32': 1.6.2 - '@scure/bip39': 1.5.4 - abitype: 1.0.8(typescript@5.9.3)(zod@3.25.76) + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - zod - ox@0.6.9(typescript@5.9.3)(zod@3.25.76): + ox@0.11.3(typescript@5.9.3)(zod@4.1.12): dependencies: '@adraffy/ens-normalize': 1.11.1 - '@noble/curves': 1.9.7 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.1(typescript@5.9.3)(zod@3.25.76) + abitype: 1.2.3(typescript@5.9.3)(zod@4.1.12) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - zod - ox@0.9.14(typescript@5.9.3)(zod@4.1.12): + ox@0.6.7(typescript@5.9.3)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.11.1 - '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.1 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.1(typescript@5.9.3)(zod@4.1.12) + abitype: 1.1.1(typescript@5.9.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - zod - ox@0.9.6(typescript@5.9.3)(zod@3.22.4): + ox@0.6.9(typescript@5.9.3)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.11.1 - '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.1 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.9.3)(zod@3.22.4) + abitype: 1.1.1(typescript@5.9.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - zod - ox@0.9.6(typescript@5.9.3)(zod@3.25.76): + ox@0.9.14(typescript@5.9.3)(zod@4.1.12): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 @@ -8989,7 +11461,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.9.3)(zod@3.25.76) + abitype: 1.1.1(typescript@5.9.3)(zod@4.1.12) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -9045,6 +11517,8 @@ snapshots: path-to-regexp@0.1.12: {} + path-to-regexp@8.3.0: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -9082,6 +11556,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -9092,41 +11568,41 @@ snapshots: pony-cause@2.1.11: {} - porto@0.2.19(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): + porto@0.2.19(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) hono: 4.10.4 idb-keyval: 6.2.2 mipd: 0.0.7(typescript@5.9.3) ox: 0.9.14(typescript@5.9.3)(zod@4.1.12) - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zod: 4.1.12 zustand: 5.0.8(@types/react@19.2.2)(react@19.2.0)(use-sync-external-store@1.4.0(react@19.2.0)) optionalDependencies: '@tanstack/react-query': 5.90.7(react@19.2.0) react: 19.2.0 typescript: 5.9.3 - wagmi: 2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + wagmi: 2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) transitivePeerDependencies: - '@types/react' - immer - use-sync-external-store - porto@0.2.35(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): + porto@0.2.35(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)): dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) hono: 4.10.4 idb-keyval: 6.2.2 mipd: 0.0.7(typescript@5.9.3) ox: 0.9.14(typescript@5.9.3)(zod@4.1.12) - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zod: 4.1.12 zustand: 5.0.8(@types/react@19.2.2)(react@19.2.0)(use-sync-external-store@1.4.0(react@19.2.0)) optionalDependencies: '@tanstack/react-query': 5.90.7(react@19.2.0) react: 19.2.0 typescript: 5.9.3 - wagmi: 2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + wagmi: 2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) transitivePeerDependencies: - '@types/react' - immer @@ -9141,6 +11617,12 @@ snapshots: postcss: 8.5.6 tsx: 4.20.6 + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -9186,13 +11668,11 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 - qrcode@1.5.4: + qs@6.13.0: dependencies: - dijkstrajs: 1.0.3 - pngjs: 5.0.0 - yargs: 15.4.1 + side-channel: 1.1.0 - qs@6.13.0: + qs@6.14.1: dependencies: side-channel: 1.1.0 @@ -9218,6 +11698,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + react-dom@19.2.0(react@19.2.0): dependencies: react: 19.2.0 @@ -9245,11 +11732,22 @@ snapshots: real-require@0.1.0: {} - redis-errors@1.2.0: {} + redis-errors@1.2.0: + optional: true redis-parser@3.0.0: dependencies: redis-errors: 1.2.0 + optional: true + + redis@4.7.1: + dependencies: + '@redis/bloom': 1.2.0(@redis/client@1.6.1) + '@redis/client': 1.6.1 + '@redis/graph': 1.1.1(@redis/client@1.6.1) + '@redis/json': 1.0.7(@redis/client@1.6.1) + '@redis/search': 1.2.0(@redis/client@1.6.1) + '@redis/time-series': 1.1.0(@redis/client@1.6.1) reflect.getprototypeof@1.0.10: dependencies: @@ -9273,6 +11771,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + require-main-filename@2.0.0: {} resolve-from@4.0.0: {} @@ -9317,6 +11817,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.5 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.3.0 + transitivePeerDependencies: + - supports-color + rpc-websockets@9.3.1: dependencies: '@swc/helpers': 0.5.17 @@ -9391,6 +11901,22 @@ snapshots: transitivePeerDependencies: - supports-color + 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.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serve-static@1.16.2: dependencies: encodeurl: 2.0.0 @@ -9400,6 +11926,15 @@ snapshots: 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 + set-blocking@2.0.0: {} set-function-length@1.2.2: @@ -9432,6 +11967,38 @@ snapshots: safe-buffer: 5.2.1 to-buffer: 1.2.2 + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -9470,6 +12037,14 @@ snapshots: signal-exit@4.1.0: {} + siwe@2.3.2(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + '@spruceid/siwe-parser': 2.1.2 + '@stablelib/random': 1.0.2 + ethers: 6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + uri-js: 4.4.1 + valid-url: 1.0.9 + socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 @@ -9513,10 +12088,13 @@ snapshots: stackback@0.0.2: {} - standard-as-callback@2.1.0: {} + standard-as-callback@2.1.0: + optional: true statuses@2.0.1: {} + statuses@2.0.2: {} + std-env@3.10.0: {} stop-iteration-iterator@1.1.0: @@ -9593,6 +12171,11 @@ snapshots: dependencies: js-tokens: 9.0.1 + styled-jsx@5.1.6(react@19.2.0): + dependencies: + client-only: 0.0.1 + react: 19.2.0 + sucrase@3.35.0: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -9619,8 +12202,6 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 - tailwind-merge@2.6.0: {} - text-encoding-utf-8@1.0.2: {} thenify-all@1.6.0: @@ -9703,6 +12284,8 @@ snapshots: tslib@1.14.1: {} + tslib@2.7.0: {} + tslib@2.8.1: {} tsup@8.5.0(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3): @@ -9740,6 +12323,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tweetnacl@1.0.3: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -9749,6 +12334,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -9799,15 +12390,13 @@ snapshots: uncrypto@0.1.3: {} + undici-types@6.19.8: {} + undici-types@6.21.0: {} undici-types@7.16.0: {} - unix-dgram@2.0.7: - dependencies: - bindings: 1.5.0 - nan: 2.24.0 - optional: true + undici-types@7.21.0: {} unpipe@1.0.0: {} @@ -9857,6 +12446,8 @@ snapshots: uuid@9.0.1: {} + valid-url@1.0.9: {} + valtio@1.13.2(@types/react@19.2.2)(react@19.2.0): dependencies: derive-valtio: 0.1.0(valtio@1.13.2(@types/react@19.2.2)(react@19.2.0)) @@ -9885,15 +12476,32 @@ snapshots: - utf-8-validate - zod - viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4): + viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.9.3)(zod@3.22.4) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.11.3(typescript@5.9.3)(zod@3.22.4) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.9.3)(zod@3.22.4) + abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - ox: 0.9.6(typescript@5.9.3)(zod@3.22.4) + ox: 0.11.3(typescript@5.9.3)(zod@3.25.76) ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.3 @@ -9902,15 +12510,15 @@ snapshots: - utf-8-validate - zod - viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76): + viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.9.3)(zod@3.25.76) + abitype: 1.2.3(typescript@5.9.3)(zod@4.1.12) isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - ox: 0.9.6(typescript@5.9.3)(zod@3.25.76) + ox: 0.11.3(typescript@5.9.3)(zod@4.1.12) ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.3 @@ -10011,14 +12619,14 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): + wagmi@2.19.2(@tanstack/query-core@5.90.7)(@tanstack/react-query@5.90.7(react@19.2.0))(@types/react@19.2.2)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): dependencies: '@tanstack/react-query': 5.90.7(react@19.2.0) - '@wagmi/connectors': 6.1.3(9bec34897937062261cacb99e1720c4a) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@wagmi/connectors': 6.1.3(5bf9f274321a9402fd289aeb8002985e) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.7)(@types/react@19.2.2)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) react: 19.2.0 use-sync-external-store: 1.4.0(react@19.2.0) - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -10181,6 +12789,11 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 5.0.10 + ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} @@ -10191,6 +12804,8 @@ snapshots: y18n@4.0.3: {} + yallist@4.0.0: {} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 @@ -10212,6 +12827,10 @@ snapshots: yocto-queue@0.1.0: {} + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod@3.22.4: {} zod@3.25.76: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 30ec529..a6ce226 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,8 @@ packages: - - '.' - - 'typescript/packages/x402' + - "." + - "typescript/packages/core" + - "typescript/packages/extensions" + - "typescript/packages/mcp" + - "typescript/packages/http/*" + - "typescript/packages/mechanisms/*" + - "typescript/packages/legacy/*" diff --git a/tsconfig.json b/tsconfig.json index 026fb5e..fc0e525 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,19 @@ { "compilerOptions": { - "target": "ES2020", - "module": "ES2020", - "moduleResolution": "bundler", + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, "skipLibCheck": true, - "strict": true, + "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, - "baseUrl": ".", - "types": [ - "node" - ], - "outDir": "dist" + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist" }, - "include": [ - "index.ts" - ], - "exclude": [ - "node_modules", - "dist" - ] -} \ No newline at end of file + "include": ["*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/typescript/package.json b/typescript/package.json index 4e579f6..307d86b 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -9,13 +9,20 @@ "version": "0.0.2", "description": "x402 Payment Protocol Monorepo", "main": "index.js", + "pnpm": { + "overrides": { + "cssstyle": "5.3.6" + } + }, "scripts": { "build": "turbo run build", "lint": "turbo run lint", "format": "turbo run format", "lint:check": "turbo run lint:check", "format:check": "turbo run format:check", - "test": "turbo run test" + "test": "turbo run test", + "test:integration": "pnpm --filter @x402/core --filter @x402/evm --filter @x402/svm test:integration", + "test:all": "pnpm test && pnpm test:integration" }, "keywords": [], "author": "", @@ -23,6 +30,8 @@ "devDependencies": { "tsup": "^8.4.0", "turbo": "^2.5.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "@changesets/cli": "^2.28.1", + "@changesets/changelog-github": "^0.5.1" } -} \ No newline at end of file +} diff --git a/typescript/packages/x402/.prettierignore b/typescript/packages/core/.prettierignore similarity index 100% rename from typescript/packages/x402/.prettierignore rename to typescript/packages/core/.prettierignore diff --git a/typescript/packages/x402/.prettierrc b/typescript/packages/core/.prettierrc similarity index 100% rename from typescript/packages/x402/.prettierrc rename to typescript/packages/core/.prettierrc diff --git a/typescript/packages/core/CHANGELOG.md b/typescript/packages/core/CHANGELOG.md new file mode 100644 index 0000000..9af23b1 --- /dev/null +++ b/typescript/packages/core/CHANGELOG.md @@ -0,0 +1,16 @@ +# @x402/core Changelog + +## 2.3.0 + +### Minor Changes + +- 51b8445: Added new hooks on clients & servers to improve extension extensibility +- 51b8445: Added new zod exports for type validation + +## 2.0.0 + +- Implements x402 2.0.0 for the TypeScript SDK. + +## 1.0.0 + +- Implements x402 1.0.0 for the TypeScript SDK. diff --git a/typescript/packages/core/README.md b/typescript/packages/core/README.md new file mode 100644 index 0000000..c2c9218 --- /dev/null +++ b/typescript/packages/core/README.md @@ -0,0 +1,293 @@ +# @x402/core + +Core implementation of the x402 payment protocol for TypeScript/JavaScript applications. Provides transport-agnostic client, server and facilitator components. + +## Installation + +```bash +pnpm install @x402/core +``` + +## Quick Start + +### Client Usage + +```typescript +import { x402Client } from '@x402/core/client'; +import { x402HTTPClient } from '@x402/core/http'; +import { ExactEvmScheme } from '@x402/evm/exact/client'; + +// Create core client and register payment schemes +const coreClient = new x402Client() + .register('eip155:*', new ExactEvmScheme(evmSigner)); + +// Wrap with HTTP client for header encoding/decoding +const client = new x402HTTPClient(coreClient); + +// Make a request +const response = await fetch('https://api.example.com/protected'); + +if (response.status === 402) { + // Extract payment requirements from response + const paymentRequired = client.getPaymentRequiredResponse( + (name) => response.headers.get(name), + await response.json() + ); + + // Create and send payment + const paymentPayload = await client.createPaymentPayload(paymentRequired); + + const paidResponse = await fetch('https://api.example.com/protected', { + headers: client.encodePaymentSignatureHeader(paymentPayload), + }); + + // Get settlement confirmation + const settlement = client.getPaymentSettleResponse( + (name) => paidResponse.headers.get(name) + ); + console.log('Transaction:', settlement.transaction); +} +``` + +### Server Usage + +```typescript +import { x402ResourceServer, HTTPFacilitatorClient } from '@x402/core/server'; +import { x402HTTPResourceServer } from '@x402/core/http'; +import { ExactEvmScheme } from '@x402/evm/exact/server'; + +// Connect to facilitator +const facilitatorClient = new HTTPFacilitatorClient({ + url: 'https://x402.org/facilitator', +}); + +// Create resource server with payment schemes +const resourceServer = new x402ResourceServer(facilitatorClient) + .register('eip155:*', new ExactEvmScheme()); + +// Initialize (fetches supported kinds from facilitator) +await resourceServer.initialize(); + +// Configure routes with payment requirements +const routes = { + 'GET /api/data': { + accepts: { + scheme: 'exact', + network: 'eip155:8453', + payTo: '0xYourAddress', + price: '$0.01', + }, + description: 'Premium data access', + mimeType: 'application/json', + }, +}; + +// Create HTTP server wrapper +const httpServer = new x402HTTPResourceServer(resourceServer, routes); +``` + +### Facilitator Usage + +```typescript +import { x402Facilitator } from '@x402/core/facilitator'; +import { registerExactEvmScheme } from '@x402/evm/exact/facilitator'; + +const facilitator = new x402Facilitator(); + +// Register scheme implementations using helper +registerExactEvmScheme(facilitator, { + signer: evmSigner, + networks: 'eip155:84532', +}); + +// Verify payment +const verifyResult = await facilitator.verify(paymentPayload, paymentRequirements); + +if (verifyResult.isValid) { + // Settle payment + const settleResult = await facilitator.settle(paymentPayload, paymentRequirements); + console.log('Transaction:', settleResult.transaction); +} +``` + +## Route Configuration + +Routes use the `accepts` field to define payment options: + +```typescript +const routes = { + // Single payment option + 'GET /api/data': { + accepts: { + scheme: 'exact', + network: 'eip155:8453', + payTo: '0xAddress', + price: '$0.01', + }, + description: 'Data endpoint', + mimeType: 'application/json', + }, + + // Multiple payment options (EVM + SVM) + 'POST /api/*': { + accepts: [ + { + scheme: 'exact', + network: 'eip155:8453', + payTo: evmAddress, + price: '$0.05', + }, + { + scheme: 'exact', + network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + payTo: svmAddress, + price: '$0.05', + }, + ], + }, +}; +``` + +## Client Configuration + +Use `fromConfig()` for declarative setup: + +```typescript +const client = x402Client.fromConfig({ + schemes: [ + { network: 'eip155:8453', client: new ExactEvmScheme(evmSigner) }, + { network: 'solana:mainnet', client: new ExactSvmScheme(svmSigner) }, + ], + policies: [ + // Filter by max price + (version, reqs) => reqs.filter(r => BigInt(r.amount) < BigInt('1000000')), + ], +}); +``` + +## Lifecycle Hooks + +### Client Hooks + +```typescript +client + .onBeforePaymentCreation(async (ctx) => { + console.log('Creating payment for:', ctx.selectedRequirements.network); + // Return { abort: true, reason: '...' } to cancel + }) + .onAfterPaymentCreation(async (ctx) => { + console.log('Payment created:', ctx.paymentPayload); + }) + .onPaymentCreationFailure(async (ctx) => { + console.error('Payment failed:', ctx.error); + // Return { recovered: true, payload: ... } to recover + }); +``` + +### Server Hooks + +```typescript +resourceServer + .onBeforeVerify(async (ctx) => { /* ... */ }) + .onAfterVerify(async (ctx) => { /* ... */ }) + .onBeforeSettle(async (ctx) => { /* ... */ }) + .onAfterSettle(async (ctx) => { /* ... */ }); +``` + +### Facilitator Hooks + +```typescript +facilitator + .onBeforeVerify(async (ctx) => { console.log('Before verify', ctx); }) + .onAfterVerify(async (ctx) => { console.log('After verify', ctx); }) + .onVerifyFailure(async (ctx) => { console.log('Verify failure', ctx); }) + .onBeforeSettle(async (ctx) => { console.log('Before settle', ctx); }) + .onAfterSettle(async (ctx) => { console.log('After settle', ctx); }) + .onSettleFailure(async (ctx) => { console.log('Settle failure', ctx); }); +``` + +## HTTP Headers + +### v2 Protocol (Current) + +| Header | Description | +|--------|-------------| +| `PAYMENT-SIGNATURE` | Base64-encoded payment payload | +| `PAYMENT-REQUIRED` | Base64-encoded payment requirements | +| `PAYMENT-RESPONSE` | Base64-encoded settlement response | + +### v1 Protocol (Legacy) + +| Header | Description | +|--------|-------------| +| `X-PAYMENT` | Base64-encoded payment payload | +| `X-PAYMENT-RESPONSE` | Base64-encoded settlement response | + +## Network Pattern Matching + +Register handlers for network families using wildcards: + +```typescript +// All EVM networks +server.register('eip155:*', new ExactEvmScheme()); + +// Specific network takes precedence +server.register('eip155:8453', new ExactEvmScheme()); +``` + +## Types + +```typescript +type Network = `${string}:${string}`; // e.g., "eip155:8453" + +type PaymentRequirements = { + scheme: string; + network: Network; + asset: string; + amount: string; + payTo: string; + maxTimeoutSeconds: number; + extra: Record; +}; + +type PaymentPayload = { + x402Version: number; + resource: ResourceInfo; + accepted: PaymentRequirements; + payload: Record; + extensions?: Record; +}; + +type PaymentRequired = { + x402Version: number; + error?: string; + resource: ResourceInfo; + accepts: PaymentRequirements[]; + extensions?: Record; +}; +``` + +## Framework Integration + +For framework-specific middleware, use: + +- `@x402/express` - Express.js middleware +- `@x402/hono` - Hono middleware +- `@x402/next` - Next.js integration +- `@x402/axios` - Axios interceptor +- `@x402/fetch` - Fetch wrapper + +## Implementation Packages + +For blockchain-specific implementations: + +- `@x402/evm` - Ethereum and EVM-compatible chains +- `@x402/svm` - Solana blockchain + +## Examples + +See the [examples directory](https://github.com/coinbase/x402/tree/main/examples/typescript) for complete examples. + +## Contributing + +Contributions welcome! See [Contributing Guide](https://github.com/coinbase/x402/blob/main/CONTRIBUTING.md). diff --git a/typescript/packages/core/eslint.config.js b/typescript/packages/core/eslint.config.js new file mode 100644 index 0000000..70e7558 --- /dev/null +++ b/typescript/packages/core/eslint.config.js @@ -0,0 +1,91 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**", "src/paywall/dist/**", "src/paywall/gen/**"], + }, + { + files: ["**/*.ts", "**/*.tsx"], + ignores: ["**/*.test.ts", "test/**/*"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, + { + files: ["**/*.test.ts", "test/**/*"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + }, + rules: { + "prettier/prettier": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/member-ordering": "off", + }, + }, +]; diff --git a/typescript/packages/x402/package.json b/typescript/packages/core/package.json similarity index 53% rename from typescript/packages/x402/package.json rename to typescript/packages/core/package.json index 4e33d89..80e044a 100644 --- a/typescript/packages/x402/package.json +++ b/typescript/packages/core/package.json @@ -1,15 +1,15 @@ { - "name": "x402", - "version": "0.7.1", + "name": "@x402/core", + "version": "2.3.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/cjs/index.d.ts", "scripts": { "start": "tsx --env-file=.env index.ts", "build": "tsup", - "build:paywall": "tsx src/paywall/build.ts", "test": "vitest run", "test:watch": "vitest", + "test:integration": "vitest run test/integrations", "watch": "tsc --watch", "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", @@ -22,81 +22,34 @@ "repository": "https://github.com/coinbase/x402", "description": "x402 Payment Protocol", "devDependencies": { - "@coinbase/onchainkit": "^0.38.14", - "@craftamap/esbuild-plugin-html": "^0.9.0", "@eslint/js": "^9.24.0", - "@types/ioredis": "^5.0.0", "@types/node": "^22.13.4", - "@types/react": "^19", - "@types/react-dom": "^19", "@typescript-eslint/eslint-plugin": "^8.29.1", "@typescript-eslint/parser": "^8.29.1", - "@wagmi/connectors": "^5.8.1", - "@wagmi/core": "^2.17.1", - "buffer": "^6.0.3", - "esbuild": "^0.25.4", "eslint": "^9.24.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsdoc": "^50.6.9", "eslint-plugin-prettier": "^5.2.6", "prettier": "3.5.2", - "react": "^19.0.0", - "react-dom": "^19.0.0", "tsup": "^8.4.0", "tsx": "^4.19.2", "typescript": "^5.7.3", - "viem": "^2.21.26", "vite": "^6.2.6", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.5" }, "dependencies": { - "@openzeppelin/merkle-tree": "^1.0.8", - "@scure/base": "^1.2.6", - "@solana-program/compute-budget": "^0.8.0", - "@solana-program/token": "^0.5.1", - "@solana-program/token-2022": "^0.4.2", - "@solana/kit": "^2.1.1", - "@solana/transaction-confirmation": "^2.1.1", - "@solana/wallet-standard-features": "^1.3.0", - "@wallet-standard/app": "^1.1.0", - "@wallet-standard/base": "^1.1.0", - "@wallet-standard/features": "^1.1.0", - "hot-shots": "^12.0.0", - "ioredis": "^5.8.2", - "viem": "^2.21.26", - "wagmi": "^2.15.6", "zod": "^3.24.2" }, "exports": { - "./shared": { + ".": { "import": { - "types": "./dist/esm/shared/index.d.mts", - "default": "./dist/esm/shared/index.mjs" + "types": "./dist/esm/index.d.mts", + "default": "./dist/esm/index.mjs" }, "require": { - "types": "./dist/cjs/shared/index.d.ts", - "default": "./dist/cjs/shared/index.js" - } - }, - "./shared/evm": { - "import": { - "types": "./dist/esm/shared/evm/index.d.mts", - "default": "./dist/esm/shared/evm/index.mjs" - }, - "require": { - "types": "./dist/cjs/shared/evm/index.d.ts", - "default": "./dist/cjs/shared/evm/index.js" - } - }, - "./schemes": { - "import": { - "types": "./dist/esm/schemes/index.d.mts", - "default": "./dist/esm/schemes/index.mjs" - }, - "require": { - "types": "./dist/cjs/schemes/index.d.ts", - "default": "./dist/cjs/schemes/index.js" + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" } }, "./client": { @@ -109,16 +62,6 @@ "default": "./dist/cjs/client/index.js" } }, - "./verify": { - "import": { - "types": "./dist/esm/verify/index.d.mts", - "default": "./dist/esm/verify/index.mjs" - }, - "require": { - "types": "./dist/cjs/verify/index.d.ts", - "default": "./dist/cjs/verify/index.js" - } - }, "./facilitator": { "import": { "types": "./dist/esm/facilitator/index.d.mts", @@ -129,14 +72,24 @@ "default": "./dist/cjs/facilitator/index.js" } }, - "./paywall": { + "./http": { "import": { - "types": "./dist/esm/paywall/index.d.mts", - "default": "./dist/esm/paywall/index.mjs" + "types": "./dist/esm/http/index.d.mts", + "default": "./dist/esm/http/index.mjs" }, "require": { - "types": "./dist/cjs/paywall/index.d.ts", - "default": "./dist/cjs/paywall/index.js" + "types": "./dist/cjs/http/index.d.ts", + "default": "./dist/cjs/http/index.js" + } + }, + "./server": { + "import": { + "types": "./dist/esm/server/index.d.mts", + "default": "./dist/esm/server/index.mjs" + }, + "require": { + "types": "./dist/cjs/server/index.d.ts", + "default": "./dist/cjs/server/index.js" } }, "./types": { @@ -148,9 +101,39 @@ "types": "./dist/cjs/types/index.d.ts", "default": "./dist/cjs/types/index.js" } + }, + "./types/v1": { + "import": { + "types": "./dist/esm/types/v1/index.d.mts", + "default": "./dist/esm/types/v1/index.mjs" + }, + "require": { + "types": "./dist/cjs/types/v1/index.d.ts", + "default": "./dist/cjs/types/v1/index.js" + } + }, + "./utils": { + "import": { + "types": "./dist/esm/utils/index.d.mts", + "default": "./dist/esm/utils/index.mjs" + }, + "require": { + "types": "./dist/cjs/utils/index.d.ts", + "default": "./dist/cjs/utils/index.js" + } + }, + "./schemas": { + "import": { + "types": "./dist/esm/schemas/index.d.mts", + "default": "./dist/esm/schemas/index.mjs" + }, + "require": { + "types": "./dist/cjs/schemas/index.d.ts", + "default": "./dist/cjs/schemas/index.js" + } } }, "files": [ "dist" ] -} \ No newline at end of file +} diff --git a/typescript/packages/core/src/client/index.ts b/typescript/packages/core/src/client/index.ts new file mode 100644 index 0000000..593c3e8 --- /dev/null +++ b/typescript/packages/core/src/client/index.ts @@ -0,0 +1,2 @@ +export * from "./x402Client"; +export * from "../http/x402HTTPClient"; \ No newline at end of file diff --git a/typescript/packages/core/src/client/x402Client.ts b/typescript/packages/core/src/client/x402Client.ts new file mode 100644 index 0000000..7b39f5a --- /dev/null +++ b/typescript/packages/core/src/client/x402Client.ts @@ -0,0 +1,382 @@ +import { x402Version } from ".."; +import { SchemeNetworkClient } from "../types/mechanisms"; +import { PaymentPayload, PaymentRequirements } from "../types/payments"; +import { Network, PaymentRequired } from "../types"; +import { findByNetworkAndScheme, findSchemesByNetwork } from "../utils"; + +/** + * Client Hook Context Interfaces + */ + +export interface PaymentCreationContext { + paymentRequired: PaymentRequired; + selectedRequirements: PaymentRequirements; +} + +export interface PaymentCreatedContext extends PaymentCreationContext { + paymentPayload: PaymentPayload; +} + +export interface PaymentCreationFailureContext extends PaymentCreationContext { + error: Error; +} + +/** + * Client Hook Type Definitions + */ + +export type BeforePaymentCreationHook = ( + context: PaymentCreationContext, +) => Promise; + +export type AfterPaymentCreationHook = (context: PaymentCreatedContext) => Promise; + +export type OnPaymentCreationFailureHook = ( + context: PaymentCreationFailureContext, +) => Promise; + +export type SelectPaymentRequirements = (x402Version: number, paymentRequirements: PaymentRequirements[]) => PaymentRequirements; + +/** + * A policy function that filters or transforms payment requirements. + * Policies are applied in order before the selector chooses the final option. + * + * @param x402Version - The x402 protocol version + * @param paymentRequirements - Array of payment requirements to filter/transform + * @returns Filtered array of payment requirements + */ +export type PaymentPolicy = (x402Version: number, paymentRequirements: PaymentRequirements[]) => PaymentRequirements[]; + + +/** + * Configuration for registering a payment scheme with a specific network + */ +export interface SchemeRegistration { + /** + * The network identifier (e.g., 'eip155:8453', 'solana:mainnet') + */ + network: Network; + + /** + * The scheme client implementation for this network + */ + client: SchemeNetworkClient; + + /** + * The x402 protocol version to use for this scheme + * + * @default 2 + */ + x402Version?: number; +} + +/** + * Configuration options for the fetch wrapper + */ +export interface x402ClientConfig { + /** + * Array of scheme registrations defining which payment methods are supported + */ + schemes: SchemeRegistration[]; + + /** + * Policies to apply to the client + */ + policies?: PaymentPolicy[]; + + /** + * Custom payment requirements selector function + * If not provided, uses the default selector (first available option) + */ + paymentRequirementsSelector?: SelectPaymentRequirements; +} + +/** + * Core client for managing x402 payment schemes and creating payment payloads. + * + * Handles registration of payment schemes, policy-based filtering of payment requirements, + * and creation of payment payloads based on server requirements. + */ +export class x402Client { + private readonly paymentRequirementsSelector: SelectPaymentRequirements; + private readonly registeredClientSchemes: Map>> = new Map(); + private readonly policies: PaymentPolicy[] = []; + + private beforePaymentCreationHooks: BeforePaymentCreationHook[] = []; + private afterPaymentCreationHooks: AfterPaymentCreationHook[] = []; + private onPaymentCreationFailureHooks: OnPaymentCreationFailureHook[] = []; + + /** + * Creates a new x402Client instance. + * + * @param paymentRequirementsSelector - Function to select payment requirements from available options + */ + constructor(paymentRequirementsSelector?: SelectPaymentRequirements) { + this.paymentRequirementsSelector = paymentRequirementsSelector || ((x402Version, accepts) => accepts[0]); + } + + /** + * Creates a new x402Client instance from a configuration object. + * + * @param config - The client configuration including schemes, policies, and payment requirements selector + * @returns A configured x402Client instance + */ + static fromConfig(config: x402ClientConfig): x402Client { + const client = new x402Client(config.paymentRequirementsSelector); + config.schemes.forEach(scheme => { + if (scheme.x402Version === 1) { + client.registerV1(scheme.network, scheme.client); + } else { + client.register(scheme.network, scheme.client); + } + }); + config.policies?.forEach(policy => { + client.registerPolicy(policy); + }); + return client; + } + + /** + * Registers a scheme client for the current x402 version. + * + * @param network - The network to register the client for + * @param client - The scheme network client to register + * @returns The x402Client instance for chaining + */ + register(network: Network, client: SchemeNetworkClient): x402Client { + return this._registerScheme(x402Version, network, client); + } + + /** + * Registers a scheme client for x402 version 1. + * + * @param network - The v1 network identifier (e.g., 'base-sepolia', 'solana-devnet') + * @param client - The scheme network client to register + * @returns The x402Client instance for chaining + */ + registerV1(network: string, client: SchemeNetworkClient): x402Client { + return this._registerScheme(1, network as Network, client); + } + + /** + * Registers a policy to filter or transform payment requirements. + * + * Policies are applied in order after filtering by registered schemes + * and before the selector chooses the final payment requirement. + * + * @param policy - Function to filter/transform payment requirements + * @returns The x402Client instance for chaining + * + * @example + * ```typescript + * // Prefer cheaper options + * client.registerPolicy((version, reqs) => + * reqs.filter(r => BigInt(r.value) < BigInt('1000000')) + * ); + * + * // Prefer specific networks + * client.registerPolicy((version, reqs) => + * reqs.filter(r => r.network.startsWith('eip155:')) + * ); + * ``` + */ + registerPolicy(policy: PaymentPolicy): x402Client { + this.policies.push(policy); + return this; + } + + /** + * Register a hook to execute before payment payload creation. + * Can abort creation by returning { abort: true, reason: string } + * + * @param hook - The hook function to register + * @returns The x402Client instance for chaining + */ + onBeforePaymentCreation(hook: BeforePaymentCreationHook): x402Client { + this.beforePaymentCreationHooks.push(hook); + return this; + } + + /** + * Register a hook to execute after successful payment payload creation. + * + * @param hook - The hook function to register + * @returns The x402Client instance for chaining + */ + onAfterPaymentCreation(hook: AfterPaymentCreationHook): x402Client { + this.afterPaymentCreationHooks.push(hook); + return this; + } + + /** + * Register a hook to execute when payment payload creation fails. + * Can recover from failure by returning { recovered: true, payload: PaymentPayload } + * + * @param hook - The hook function to register + * @returns The x402Client instance for chaining + */ + onPaymentCreationFailure(hook: OnPaymentCreationFailureHook): x402Client { + this.onPaymentCreationFailureHooks.push(hook); + return this; + } + + /** + * Creates a payment payload based on a PaymentRequired response. + * + * Automatically extracts x402Version, resource, and extensions from the PaymentRequired + * response and constructs a complete PaymentPayload with the accepted requirements. + * + * @param paymentRequired - The PaymentRequired response from the server + * @returns Promise resolving to the complete payment payload + */ + async createPaymentPayload( + paymentRequired: PaymentRequired, + ): Promise { + const clientSchemesByNetwork = this.registeredClientSchemes.get(paymentRequired.x402Version); + if (!clientSchemesByNetwork) { + throw new Error(`No client registered for x402 version: ${paymentRequired.x402Version}`); + } + + const requirements = this.selectPaymentRequirements(paymentRequired.x402Version, paymentRequired.accepts); + + const context: PaymentCreationContext = { + paymentRequired, + selectedRequirements: requirements, + }; + + // Execute beforePaymentCreation hooks + for (const hook of this.beforePaymentCreationHooks) { + const result = await hook(context); + if (result && "abort" in result && result.abort) { + throw new Error(`Payment creation aborted: ${result.reason}`); + } + } + + try { + const schemeNetworkClient = findByNetworkAndScheme(clientSchemesByNetwork, requirements.scheme, requirements.network); + if (!schemeNetworkClient) { + throw new Error(`No client registered for scheme: ${requirements.scheme} and network: ${requirements.network}`); + } + + const partialPayload = await schemeNetworkClient.createPaymentPayload(paymentRequired.x402Version, requirements); + + let paymentPayload: PaymentPayload; + if (partialPayload.x402Version == 1) { + paymentPayload = partialPayload as PaymentPayload; + } else { + paymentPayload = { + ...partialPayload, + extensions: paymentRequired.extensions, + resource: paymentRequired.resource, + accepted: requirements, + }; + } + + // Execute afterPaymentCreation hooks + const createdContext: PaymentCreatedContext = { + ...context, + paymentPayload, + }; + + for (const hook of this.afterPaymentCreationHooks) { + await hook(createdContext); + } + + return paymentPayload; + } catch (error) { + const failureContext: PaymentCreationFailureContext = { + ...context, + error: error as Error, + }; + + // Execute onPaymentCreationFailure hooks + for (const hook of this.onPaymentCreationFailureHooks) { + const result = await hook(failureContext); + if (result && "recovered" in result && result.recovered) { + return result.payload; + } + } + + throw error; + } + } + + + + /** + * Selects appropriate payment requirements based on registered clients and policies. + * + * Selection process: + * 1. Filter by registered schemes (network + scheme support) + * 2. Apply all registered policies in order + * 3. Use selector to choose final requirement + * + * @param x402Version - The x402 protocol version + * @param paymentRequirements - Array of available payment requirements + * @returns The selected payment requirements + */ + private selectPaymentRequirements(x402Version: number, paymentRequirements: PaymentRequirements[]): PaymentRequirements { + const clientSchemesByNetwork = this.registeredClientSchemes.get(x402Version); + if (!clientSchemesByNetwork) { + throw new Error(`No client registered for x402 version: ${x402Version}`); + } + + // Step 1: Filter by registered schemes + const supportedPaymentRequirements = paymentRequirements.filter(requirement => { + let clientSchemes = findSchemesByNetwork(clientSchemesByNetwork, requirement.network); + if (!clientSchemes) { + return false; + } + + return clientSchemes.has(requirement.scheme); + }) + + if (supportedPaymentRequirements.length === 0) { + throw new Error(`No network/scheme registered for x402 version: ${x402Version} which comply with the payment requirements. ${JSON.stringify({ + x402Version, + paymentRequirements, + x402Versions: Array.from(this.registeredClientSchemes.keys()), + networks: Array.from(clientSchemesByNetwork.keys()), + schemes: Array.from(clientSchemesByNetwork.values()).map(schemes => Array.from(schemes.keys())).flat(), + })}`); + } + + // Step 2: Apply all policies in order + let filteredRequirements = supportedPaymentRequirements; + for (const policy of this.policies) { + filteredRequirements = policy(x402Version, filteredRequirements); + + if (filteredRequirements.length === 0) { + throw new Error(`All payment requirements were filtered out by policies for x402 version: ${x402Version}`); + } + } + + // Step 3: Use selector to choose final requirement + return this.paymentRequirementsSelector(x402Version, filteredRequirements); + } + + /** + * Internal method to register a scheme client. + * + * @param x402Version - The x402 protocol version + * @param network - The network to register the client for + * @param client - The scheme network client to register + * @returns The x402Client instance for chaining + */ + private _registerScheme(x402Version: number, network: Network, client: SchemeNetworkClient): x402Client { + if (!this.registeredClientSchemes.has(x402Version)) { + this.registeredClientSchemes.set(x402Version, new Map()); + } + const clientSchemesByNetwork = this.registeredClientSchemes.get(x402Version)!; + if (!clientSchemesByNetwork.has(network)) { + clientSchemesByNetwork.set(network, new Map()); + } + + const clientByScheme = clientSchemesByNetwork.get(network)!; + if (!clientByScheme.has(client.scheme)) { + clientByScheme.set(client.scheme, client); + } + + return this; + } +} diff --git a/typescript/packages/core/src/facilitator/index.ts b/typescript/packages/core/src/facilitator/index.ts new file mode 100644 index 0000000..d1409c9 --- /dev/null +++ b/typescript/packages/core/src/facilitator/index.ts @@ -0,0 +1 @@ +export * from "./x402Facilitator"; diff --git a/typescript/packages/core/src/facilitator/x402Facilitator.ts b/typescript/packages/core/src/facilitator/x402Facilitator.ts new file mode 100644 index 0000000..db747d3 --- /dev/null +++ b/typescript/packages/core/src/facilitator/x402Facilitator.ts @@ -0,0 +1,529 @@ +import { x402Version } from ".."; +import { SettleResponse, VerifyResponse } from "../types/facilitator"; +import { SchemeNetworkFacilitator } from "../types/mechanisms"; +import { PaymentPayload, PaymentRequirements } from "../types/payments"; +import { Network } from "../types"; +import { type SchemeData } from "../utils"; + +/** + * Facilitator Hook Context Interfaces + */ + +export interface FacilitatorVerifyContext { + paymentPayload: PaymentPayload; + requirements: PaymentRequirements; +} + +export interface FacilitatorVerifyResultContext extends FacilitatorVerifyContext { + result: VerifyResponse; +} + +export interface FacilitatorVerifyFailureContext extends FacilitatorVerifyContext { + error: Error; +} + +export interface FacilitatorSettleContext { + paymentPayload: PaymentPayload; + requirements: PaymentRequirements; +} + +export interface FacilitatorSettleResultContext extends FacilitatorSettleContext { + result: SettleResponse; +} + +export interface FacilitatorSettleFailureContext extends FacilitatorSettleContext { + error: Error; +} + +/** + * Facilitator Hook Type Definitions + */ + +export type FacilitatorBeforeVerifyHook = ( + context: FacilitatorVerifyContext, +) => Promise; + +export type FacilitatorAfterVerifyHook = (context: FacilitatorVerifyResultContext) => Promise; + +export type FacilitatorOnVerifyFailureHook = ( + context: FacilitatorVerifyFailureContext, +) => Promise; + +export type FacilitatorBeforeSettleHook = ( + context: FacilitatorSettleContext, +) => Promise; + +export type FacilitatorAfterSettleHook = (context: FacilitatorSettleResultContext) => Promise; + +export type FacilitatorOnSettleFailureHook = ( + context: FacilitatorSettleFailureContext, +) => Promise; + +/** + * Facilitator client for the x402 payment protocol. + * Manages payment scheme registration, verification, and settlement. + */ +export class x402Facilitator { + private readonly registeredFacilitatorSchemes: Map< + number, + SchemeData[] // Array to support multiple facilitators per version + > = new Map(); + private readonly extensions: string[] = []; + + private beforeVerifyHooks: FacilitatorBeforeVerifyHook[] = []; + private afterVerifyHooks: FacilitatorAfterVerifyHook[] = []; + private onVerifyFailureHooks: FacilitatorOnVerifyFailureHook[] = []; + private beforeSettleHooks: FacilitatorBeforeSettleHook[] = []; + private afterSettleHooks: FacilitatorAfterSettleHook[] = []; + private onSettleFailureHooks: FacilitatorOnSettleFailureHook[] = []; + + /** + * Registers a scheme facilitator for the current x402 version. + * Networks are stored and used for getSupported() - no need to specify them later. + * + * @param networks - Single network or array of networks this facilitator supports + * @param facilitator - The scheme network facilitator to register + * @returns The x402Facilitator instance for chaining + */ + register(networks: Network | Network[], facilitator: SchemeNetworkFacilitator): x402Facilitator { + const networksArray = Array.isArray(networks) ? networks : [networks]; + return this._registerScheme(x402Version, networksArray, facilitator); + } + + /** + * Registers a scheme facilitator for x402 version 1. + * Networks are stored and used for getSupported() - no need to specify them later. + * + * @param networks - Single network or array of networks this facilitator supports + * @param facilitator - The scheme network facilitator to register + * @returns The x402Facilitator instance for chaining + */ + registerV1( + networks: Network | Network[], + facilitator: SchemeNetworkFacilitator, + ): x402Facilitator { + const networksArray = Array.isArray(networks) ? networks : [networks]; + return this._registerScheme(1, networksArray, facilitator); + } + + /** + * Registers a protocol extension. + * + * @param extension - The extension name to register (e.g., "bazaar", "sign_in_with_x") + * @returns The x402Facilitator instance for chaining + */ + registerExtension(extension: string): x402Facilitator { + // Check if already registered + if (!this.extensions.includes(extension)) { + this.extensions.push(extension); + } + return this; + } + + /** + * Gets the list of registered extensions. + * + * @returns Array of extension names + */ + getExtensions(): string[] { + return [...this.extensions]; + } + + /** + * Register a hook to execute before facilitator payment verification. + * Can abort verification by returning { abort: true, reason: string } + * + * @param hook - The hook function to register + * @returns The x402Facilitator instance for chaining + */ + onBeforeVerify(hook: FacilitatorBeforeVerifyHook): x402Facilitator { + this.beforeVerifyHooks.push(hook); + return this; + } + + /** + * Register a hook to execute after successful facilitator payment verification (isValid: true). + * This hook is NOT called when verification fails (isValid: false) - use onVerifyFailure for that. + * + * @param hook - The hook function to register + * @returns The x402Facilitator instance for chaining + */ + onAfterVerify(hook: FacilitatorAfterVerifyHook): x402Facilitator { + this.afterVerifyHooks.push(hook); + return this; + } + + /** + * Register a hook to execute when facilitator payment verification fails. + * Called when: verification returns isValid: false, or an exception is thrown during verification. + * Can recover from failure by returning { recovered: true, result: VerifyResponse } + * + * @param hook - The hook function to register + * @returns The x402Facilitator instance for chaining + */ + onVerifyFailure(hook: FacilitatorOnVerifyFailureHook): x402Facilitator { + this.onVerifyFailureHooks.push(hook); + return this; + } + + /** + * Register a hook to execute before facilitator payment settlement. + * Can abort settlement by returning { abort: true, reason: string } + * + * @param hook - The hook function to register + * @returns The x402Facilitator instance for chaining + */ + onBeforeSettle(hook: FacilitatorBeforeSettleHook): x402Facilitator { + this.beforeSettleHooks.push(hook); + return this; + } + + /** + * Register a hook to execute after successful facilitator payment settlement. + * + * @param hook - The hook function to register + * @returns The x402Facilitator instance for chaining + */ + onAfterSettle(hook: FacilitatorAfterSettleHook): x402Facilitator { + this.afterSettleHooks.push(hook); + return this; + } + + /** + * Register a hook to execute when facilitator payment settlement fails. + * Can recover from failure by returning { recovered: true, result: SettleResponse } + * + * @param hook - The hook function to register + * @returns The x402Facilitator instance for chaining + */ + onSettleFailure(hook: FacilitatorOnSettleFailureHook): x402Facilitator { + this.onSettleFailureHooks.push(hook); + return this; + } + + /** + * Gets supported payment kinds, extensions, and signers. + * Uses networks registered during register() calls - no parameters needed. + * Returns flat array format for backward compatibility with V1 clients. + * + * @returns Supported response with kinds as array (with version in each element), extensions, and signers + */ + getSupported(): { + kinds: Array<{ + x402Version: number; + scheme: string; + network: string; + extra?: Record; + }>; + extensions: string[]; + signers: Record; + } { + const kinds: Array<{ + x402Version: number; + scheme: string; + network: string; + extra?: Record; + }> = []; + const signersByFamily: Record> = {}; + + // Iterate over registered scheme data (array supports multiple facilitators per version) + for (const [version, schemeDataArray] of this.registeredFacilitatorSchemes) { + for (const schemeData of schemeDataArray) { + const { facilitator, networks } = schemeData; + const scheme = facilitator.scheme; + + // Iterate over stored concrete networks + for (const network of networks) { + const extra = facilitator.getExtra(network); + kinds.push({ + x402Version: version, + scheme, + network, + ...(extra && { extra }), + }); + + // Collect signers by CAIP family for this network + const family = facilitator.caipFamily; + if (!signersByFamily[family]) { + signersByFamily[family] = new Set(); + } + facilitator.getSigners(network).forEach(signer => signersByFamily[family].add(signer)); + } + } + } + + // Convert signer sets to arrays + const signers: Record = {}; + for (const [family, signerSet] of Object.entries(signersByFamily)) { + signers[family] = Array.from(signerSet); + } + + return { + kinds, + extensions: this.extensions, + signers, + }; + } + + /** + * Verifies a payment payload against requirements. + * + * @param paymentPayload - The payment payload to verify + * @param paymentRequirements - The payment requirements to verify against + * @returns Promise resolving to the verification response + */ + async verify( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + ): Promise { + const context: FacilitatorVerifyContext = { + paymentPayload, + requirements: paymentRequirements, + }; + + // Execute beforeVerify hooks + for (const hook of this.beforeVerifyHooks) { + const result = await hook(context); + if (result && "abort" in result && result.abort) { + return { + isValid: false, + invalidReason: result.reason, + }; + } + } + + try { + const schemeDataArray = this.registeredFacilitatorSchemes.get(paymentPayload.x402Version); + if (!schemeDataArray) { + throw new Error( + `No facilitator registered for x402 version: ${paymentPayload.x402Version}`, + ); + } + + // Find matching facilitator from array + let schemeNetworkFacilitator: SchemeNetworkFacilitator | undefined; + for (const schemeData of schemeDataArray) { + if (schemeData.facilitator.scheme === paymentRequirements.scheme) { + // Check if network matches + if (schemeData.networks.has(paymentRequirements.network)) { + schemeNetworkFacilitator = schemeData.facilitator; + break; + } + // Try pattern matching + const patternRegex = new RegExp("^" + schemeData.pattern.replace("*", ".*") + "$"); + if (patternRegex.test(paymentRequirements.network)) { + schemeNetworkFacilitator = schemeData.facilitator; + break; + } + } + } + + if (!schemeNetworkFacilitator) { + throw new Error( + `No facilitator registered for scheme: ${paymentRequirements.scheme} and network: ${paymentRequirements.network}`, + ); + } + + const verifyResult = await schemeNetworkFacilitator.verify( + paymentPayload, + paymentRequirements, + ); + + // Check if verification failed (isValid: false) + if (!verifyResult.isValid) { + const failureContext: FacilitatorVerifyFailureContext = { + ...context, + error: new Error(verifyResult.invalidReason || "Verification failed"), + }; + + // Execute onVerifyFailure hooks + for (const hook of this.onVerifyFailureHooks) { + const result = await hook(failureContext); + if (result && "recovered" in result && result.recovered) { + // If recovered, execute afterVerify hooks with recovered result + const recoveredContext: FacilitatorVerifyResultContext = { + ...context, + result: result.result, + }; + for (const hook of this.afterVerifyHooks) { + await hook(recoveredContext); + } + return result.result; + } + } + + return verifyResult; + } + + // Execute afterVerify hooks only for successful verification + const resultContext: FacilitatorVerifyResultContext = { + ...context, + result: verifyResult, + }; + + for (const hook of this.afterVerifyHooks) { + await hook(resultContext); + } + + return verifyResult; + } catch (error) { + const failureContext: FacilitatorVerifyFailureContext = { + ...context, + error: error as Error, + }; + + // Execute onVerifyFailure hooks + for (const hook of this.onVerifyFailureHooks) { + const result = await hook(failureContext); + if (result && "recovered" in result && result.recovered) { + return result.result; + } + } + + throw error; + } + } + + /** + * Settles a payment based on the payload and requirements. + * + * @param paymentPayload - The payment payload to settle + * @param paymentRequirements - The payment requirements for settlement + * @returns Promise resolving to the settlement response + */ + async settle( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + ): Promise { + const context: FacilitatorSettleContext = { + paymentPayload, + requirements: paymentRequirements, + }; + + // Execute beforeSettle hooks + for (const hook of this.beforeSettleHooks) { + const result = await hook(context); + if (result && "abort" in result && result.abort) { + throw new Error(`Settlement aborted: ${result.reason}`); + } + } + + try { + const schemeDataArray = this.registeredFacilitatorSchemes.get(paymentPayload.x402Version); + if (!schemeDataArray) { + throw new Error( + `No facilitator registered for x402 version: ${paymentPayload.x402Version}`, + ); + } + + // Find matching facilitator from array + let schemeNetworkFacilitator: SchemeNetworkFacilitator | undefined; + for (const schemeData of schemeDataArray) { + if (schemeData.facilitator.scheme === paymentRequirements.scheme) { + // Check if network matches + if (schemeData.networks.has(paymentRequirements.network)) { + schemeNetworkFacilitator = schemeData.facilitator; + break; + } + // Try pattern matching + const patternRegex = new RegExp("^" + schemeData.pattern.replace("*", ".*") + "$"); + if (patternRegex.test(paymentRequirements.network)) { + schemeNetworkFacilitator = schemeData.facilitator; + break; + } + } + } + + if (!schemeNetworkFacilitator) { + throw new Error( + `No facilitator registered for scheme: ${paymentRequirements.scheme} and network: ${paymentRequirements.network}`, + ); + } + + const settleResult = await schemeNetworkFacilitator.settle( + paymentPayload, + paymentRequirements, + ); + + // Execute afterSettle hooks + const resultContext: FacilitatorSettleResultContext = { + ...context, + result: settleResult, + }; + + for (const hook of this.afterSettleHooks) { + await hook(resultContext); + } + + return settleResult; + } catch (error) { + const failureContext: FacilitatorSettleFailureContext = { + ...context, + error: error as Error, + }; + + // Execute onSettleFailure hooks + for (const hook of this.onSettleFailureHooks) { + const result = await hook(failureContext); + if (result && "recovered" in result && result.recovered) { + return result.result; + } + } + + throw error; + } + } + + /** + * Internal method to register a scheme facilitator. + * + * @param x402Version - The x402 protocol version + * @param networks - Array of concrete networks this facilitator supports + * @param facilitator - The scheme network facilitator to register + * @returns The x402Facilitator instance for chaining + */ + private _registerScheme( + x402Version: number, + networks: Network[], + facilitator: SchemeNetworkFacilitator, + ): x402Facilitator { + if (!this.registeredFacilitatorSchemes.has(x402Version)) { + this.registeredFacilitatorSchemes.set(x402Version, []); + } + const schemeDataArray = this.registeredFacilitatorSchemes.get(x402Version)!; + + // Add new scheme data (supports multiple facilitators with same scheme name) + schemeDataArray.push({ + facilitator, + networks: new Set(networks), + pattern: this.derivePattern(networks), + }); + + return this; + } + + /** + * Derives a wildcard pattern from an array of networks. + * If all networks share the same namespace, returns wildcard pattern. + * Otherwise returns the first network for exact matching. + * + * @param networks - Array of networks + * @returns Derived pattern for matching + */ + private derivePattern(networks: Network[]): Network { + if (networks.length === 0) return "" as Network; + if (networks.length === 1) return networks[0]; + + // Extract namespaces (e.g., "eip155" from "eip155:84532") + const namespaces = networks.map(n => n.split(":")[0]); + const uniqueNamespaces = new Set(namespaces); + + // If all same namespace, use wildcard + if (uniqueNamespaces.size === 1) { + return `${namespaces[0]}:*` as Network; + } + + // Mixed namespaces - use first network for exact matching + return networks[0]; + } +} diff --git a/typescript/packages/core/src/http/httpFacilitatorClient.ts b/typescript/packages/core/src/http/httpFacilitatorClient.ts new file mode 100644 index 0000000..29ce532 --- /dev/null +++ b/typescript/packages/core/src/http/httpFacilitatorClient.ts @@ -0,0 +1,245 @@ +import { PaymentPayload, PaymentRequirements } from "../types/payments"; +import { + VerifyResponse, + SettleResponse, + SupportedResponse, + VerifyError, + SettleError, +} from "../types/facilitator"; + +const DEFAULT_FACILITATOR_URL = "https://x402.org/facilitator"; + +export interface FacilitatorConfig { + url?: string; + createAuthHeaders?: () => Promise<{ + verify: Record; + settle: Record; + supported: Record; + }>; +} + +/** + * Interface for facilitator clients + * Can be implemented for HTTP-based or local facilitators + */ +export interface FacilitatorClient { + /** + * Verify a payment with the facilitator + * + * @param paymentPayload - The payment to verify + * @param paymentRequirements - The requirements to verify against + * @returns Verification response + */ + verify( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + ): Promise; + + /** + * Settle a payment with the facilitator + * + * @param paymentPayload - The payment to settle + * @param paymentRequirements - The requirements for settlement + * @returns Settlement response + */ + settle( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + ): Promise; + + /** + * Get supported payment kinds and extensions from the facilitator + * + * @returns Supported payment kinds and extensions + */ + getSupported(): Promise; +} + +/** Number of retries for getSupported() on 429 rate limit errors */ +const GET_SUPPORTED_RETRIES = 3; +/** Base delay in ms for exponential backoff on retries */ +const GET_SUPPORTED_RETRY_DELAY_MS = 1000; + +/** + * HTTP-based client for interacting with x402 facilitator services + * Handles HTTP communication with facilitator endpoints + */ +export class HTTPFacilitatorClient implements FacilitatorClient { + readonly url: string; + private readonly _createAuthHeaders?: FacilitatorConfig["createAuthHeaders"]; + + /** + * Creates a new HTTPFacilitatorClient instance. + * + * @param config - Configuration options for the facilitator client + */ + constructor(config?: FacilitatorConfig) { + this.url = config?.url || DEFAULT_FACILITATOR_URL; + this._createAuthHeaders = config?.createAuthHeaders; + } + + /** + * Verify a payment with the facilitator + * + * @param paymentPayload - The payment to verify + * @param paymentRequirements - The requirements to verify against + * @returns Verification response + */ + async verify( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + ): Promise { + let headers: Record = { + "Content-Type": "application/json", + }; + + if (this._createAuthHeaders) { + const authHeaders = await this.createAuthHeaders("verify"); + headers = { ...headers, ...authHeaders.headers }; + } + + const response = await fetch(`${this.url}/verify`, { + method: "POST", + headers, + body: JSON.stringify({ + x402Version: paymentPayload.x402Version, + paymentPayload: this.toJsonSafe(paymentPayload), + paymentRequirements: this.toJsonSafe(paymentRequirements), + }), + }); + + const data = await response.json(); + + if (typeof data === "object" && data !== null && "isValid" in data) { + const verifyResponse = data as VerifyResponse; + if (!response.ok) { + throw new VerifyError(response.status, verifyResponse); + } + return verifyResponse; + } + + throw new Error(`Facilitator verify failed (${response.status}): ${JSON.stringify(data)}`); + } + + /** + * Settle a payment with the facilitator + * + * @param paymentPayload - The payment to settle + * @param paymentRequirements - The requirements for settlement + * @returns Settlement response + */ + async settle( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + ): Promise { + let headers: Record = { + "Content-Type": "application/json", + }; + + if (this._createAuthHeaders) { + const authHeaders = await this.createAuthHeaders("settle"); + headers = { ...headers, ...authHeaders.headers }; + } + + const response = await fetch(`${this.url}/settle`, { + method: "POST", + headers, + body: JSON.stringify({ + x402Version: paymentPayload.x402Version, + paymentPayload: this.toJsonSafe(paymentPayload), + paymentRequirements: this.toJsonSafe(paymentRequirements), + }), + }); + + const data = await response.json(); + + if (typeof data === "object" && data !== null && "success" in data) { + const settleResponse = data as SettleResponse; + if (!response.ok) { + throw new SettleError(response.status, settleResponse); + } + return settleResponse; + } + + throw new Error(`Facilitator settle failed (${response.status}): ${JSON.stringify(data)}`); + } + + /** + * Get supported payment kinds and extensions from the facilitator. + * Retries with exponential backoff on 429 rate limit errors. + * + * @returns Supported payment kinds and extensions + */ + async getSupported(): Promise { + let headers: Record = { + "Content-Type": "application/json", + }; + + if (this._createAuthHeaders) { + const authHeaders = await this.createAuthHeaders("supported"); + headers = { ...headers, ...authHeaders.headers }; + } + + let lastError: Error | null = null; + for (let attempt = 0; attempt < GET_SUPPORTED_RETRIES; attempt++) { + const response = await fetch(`${this.url}/supported`, { + method: "GET", + headers, + }); + + if (response.ok) { + return (await response.json()) as SupportedResponse; + } + + const errorText = await response.text().catch(() => response.statusText); + lastError = new Error(`Facilitator getSupported failed (${response.status}): ${errorText}`); + + // Retry on 429 rate limit errors with exponential backoff + if (response.status === 429 && attempt < GET_SUPPORTED_RETRIES - 1) { + const delay = GET_SUPPORTED_RETRY_DELAY_MS * Math.pow(2, attempt); + await new Promise(resolve => setTimeout(resolve, delay)); + continue; + } + + throw lastError; + } + + throw lastError ?? new Error("Facilitator getSupported failed after retries"); + } + + /** + * Creates authentication headers for a specific path. + * + * @param path - The path to create authentication headers for (e.g., "verify", "settle", "supported") + * @returns An object containing the authentication headers for the specified path + */ + async createAuthHeaders(path: string): Promise<{ + headers: Record; + }> { + if (this._createAuthHeaders) { + const authHeaders = (await this._createAuthHeaders()) as Record< + string, + Record + >; + return { + headers: authHeaders[path] ?? {}, + }; + } + return { + headers: {}, + }; + } + + /** + * Helper to convert objects to JSON-safe format. + * Handles BigInt and other non-JSON types. + * + * @param obj - The object to convert + * @returns The JSON-safe representation of the object + */ + private toJsonSafe(obj: unknown): unknown { + return JSON.parse( + JSON.stringify(obj, (_, value) => (typeof value === "bigint" ? value.toString() : value)), + ); + } +} diff --git a/typescript/packages/core/src/http/index.ts b/typescript/packages/core/src/http/index.ts new file mode 100644 index 0000000..ac20bc0 --- /dev/null +++ b/typescript/packages/core/src/http/index.ts @@ -0,0 +1,109 @@ +import { SettleResponse } from "../types"; +import { PaymentPayload, PaymentRequired } from "../types/payments"; +import { Base64EncodedRegex, safeBase64Decode, safeBase64Encode } from "../utils"; + +// HTTP Methods that typically use query parameters +export type QueryParamMethods = "GET" | "HEAD" | "DELETE"; + +// HTTP Methods that typically use request body +export type BodyMethods = "POST" | "PUT" | "PATCH"; + +/** + * Encodes a payment payload as a base64 header value. + * + * @param paymentPayload - The payment payload to encode + * @returns Base64 encoded string representation of the payment payload + */ +export function encodePaymentSignatureHeader(paymentPayload: PaymentPayload): string { + return safeBase64Encode(JSON.stringify(paymentPayload)); +} + +/** + * Decodes a base64 payment signature header into a payment payload. + * + * @param paymentSignatureHeader - The base64 encoded payment signature header + * @returns The decoded payment payload + */ +export function decodePaymentSignatureHeader(paymentSignatureHeader: string): PaymentPayload { + if (!Base64EncodedRegex.test(paymentSignatureHeader)) { + throw new Error("Invalid payment signature header"); + } + return JSON.parse(safeBase64Decode(paymentSignatureHeader)) as PaymentPayload; +} + +/** + * Encodes a payment required object as a base64 header value. + * + * @param paymentRequired - The payment required object to encode + * @returns Base64 encoded string representation of the payment required object + */ +export function encodePaymentRequiredHeader(paymentRequired: PaymentRequired): string { + return safeBase64Encode(JSON.stringify(paymentRequired)); +} + +/** + * Decodes a base64 payment required header into a payment required object. + * + * @param paymentRequiredHeader - The base64 encoded payment required header + * @returns The decoded payment required object + */ +export function decodePaymentRequiredHeader(paymentRequiredHeader: string): PaymentRequired { + if (!Base64EncodedRegex.test(paymentRequiredHeader)) { + throw new Error("Invalid payment required header"); + } + return JSON.parse(safeBase64Decode(paymentRequiredHeader)) as PaymentRequired; +} + +/** + * Encodes a payment response as a base64 header value. + * + * @param paymentResponse - The payment response to encode + * @returns Base64 encoded string representation of the payment response + */ +export function encodePaymentResponseHeader(paymentResponse: SettleResponse): string { + return safeBase64Encode(JSON.stringify(paymentResponse)); +} + +/** + * Decodes a base64 payment response header into a settle response. + * + * @param paymentResponseHeader - The base64 encoded payment response header + * @returns The decoded settle response + */ +export function decodePaymentResponseHeader(paymentResponseHeader: string): SettleResponse { + if (!Base64EncodedRegex.test(paymentResponseHeader)) { + throw new Error("Invalid payment response header"); + } + return JSON.parse(safeBase64Decode(paymentResponseHeader)) as SettleResponse; +} + +// Export HTTP service and types +export { + x402HTTPResourceServer, + HTTPAdapter, + HTTPRequestContext, + HTTPResponseInstructions, + HTTPProcessResult, + PaywallConfig, + PaywallProvider, + PaymentOption, + RouteConfig, + RoutesConfig, + CompiledRoute, + DynamicPayTo, + DynamicPrice, + UnpaidResponseBody, + UnpaidResponseResult, + ProcessSettleResultResponse, + ProcessSettleSuccessResponse, + ProcessSettleFailureResponse, + RouteValidationError, + RouteConfigurationError, + ProtectedRequestHook, +} from "./x402HTTPResourceServer"; +export { + HTTPFacilitatorClient, + FacilitatorClient, + FacilitatorConfig, +} from "./httpFacilitatorClient"; +export { x402HTTPClient, PaymentRequiredContext, PaymentRequiredHook } from "./x402HTTPClient"; diff --git a/typescript/packages/core/src/http/x402HTTPClient.ts b/typescript/packages/core/src/http/x402HTTPClient.ts new file mode 100644 index 0000000..2a300ee --- /dev/null +++ b/typescript/packages/core/src/http/x402HTTPClient.ts @@ -0,0 +1,156 @@ +import { + decodePaymentRequiredHeader, + decodePaymentResponseHeader, + encodePaymentSignatureHeader, +} from "."; +import { SettleResponse } from "../types"; +import { PaymentPayload, PaymentRequired } from "../types/payments"; +import { x402Client } from "../client/x402Client"; + +/** + * Context provided to onPaymentRequired hooks. + */ +export interface PaymentRequiredContext { + paymentRequired: PaymentRequired; +} + +/** + * Hook called when a 402 response is received, before payment processing. + * Return headers to try before payment, or void to proceed directly to payment. + */ +export type PaymentRequiredHook = ( + context: PaymentRequiredContext, +) => Promise<{ headers: Record } | void>; + +/** + * HTTP-specific client for handling x402 payment protocol over HTTP. + * + * Wraps a x402Client to provide HTTP-specific encoding/decoding functionality + * for payment headers and responses while maintaining the builder pattern. + */ +export class x402HTTPClient { + private paymentRequiredHooks: PaymentRequiredHook[] = []; + + /** + * Creates a new x402HTTPClient instance. + * + * @param client - The underlying x402Client for payment logic + */ + constructor(private readonly client: x402Client) {} + + /** + * Register a hook to handle 402 responses before payment. + * Hooks run in order; first to return headers wins. + * + * @param hook - The hook function to register + * @returns This instance for chaining + */ + onPaymentRequired(hook: PaymentRequiredHook): this { + this.paymentRequiredHooks.push(hook); + return this; + } + + /** + * Run hooks and return headers if any hook provides them. + * + * @param paymentRequired - The payment required response from the server + * @returns Headers to use for retry, or null to proceed to payment + */ + async handlePaymentRequired( + paymentRequired: PaymentRequired, + ): Promise | null> { + for (const hook of this.paymentRequiredHooks) { + const result = await hook({ paymentRequired }); + if (result?.headers) { + return result.headers; + } + } + return null; + } + + /** + * Encodes a payment payload into appropriate HTTP headers based on version. + * + * @param paymentPayload - The payment payload to encode + * @returns HTTP headers containing the encoded payment signature + */ + encodePaymentSignatureHeader(paymentPayload: PaymentPayload): Record { + switch (paymentPayload.x402Version) { + case 2: + return { + "PAYMENT-SIGNATURE": encodePaymentSignatureHeader(paymentPayload), + }; + case 1: + return { + "X-PAYMENT": encodePaymentSignatureHeader(paymentPayload), + }; + default: + throw new Error( + `Unsupported x402 version: ${(paymentPayload as PaymentPayload).x402Version}`, + ); + } + } + + /** + * Extracts payment required information from HTTP response. + * + * @param getHeader - Function to retrieve header value by name (case-insensitive) + * @param body - Optional response body for v1 compatibility + * @returns The payment required object + */ + getPaymentRequiredResponse( + getHeader: (name: string) => string | null | undefined, + body?: unknown, + ): PaymentRequired { + // v2 + const paymentRequired = getHeader("PAYMENT-REQUIRED"); + if (paymentRequired) { + return decodePaymentRequiredHeader(paymentRequired); + } + + // v1 + if ( + body && + body instanceof Object && + "x402Version" in body && + (body as PaymentRequired).x402Version === 1 + ) { + return body as PaymentRequired; + } + + throw new Error("Invalid payment required response"); + } + + /** + * Extracts payment settlement response from HTTP headers. + * + * @param getHeader - Function to retrieve header value by name (case-insensitive) + * @returns The settlement response object + */ + getPaymentSettleResponse(getHeader: (name: string) => string | null | undefined): SettleResponse { + // v2 + const paymentResponse = getHeader("PAYMENT-RESPONSE"); + if (paymentResponse) { + return decodePaymentResponseHeader(paymentResponse); + } + + // v1 + const xPaymentResponse = getHeader("X-PAYMENT-RESPONSE"); + if (xPaymentResponse) { + return decodePaymentResponseHeader(xPaymentResponse); + } + + throw new Error("Payment response header not found"); + } + + /** + * Creates a payment payload for the given payment requirements. + * Delegates to the underlying x402Client. + * + * @param paymentRequired - The payment required response from the server + * @returns Promise resolving to the payment payload + */ + async createPaymentPayload(paymentRequired: PaymentRequired): Promise { + return this.client.createPaymentPayload(paymentRequired); + } +} diff --git a/typescript/packages/core/src/http/x402HTTPResourceServer.ts b/typescript/packages/core/src/http/x402HTTPResourceServer.ts new file mode 100644 index 0000000..7524162 --- /dev/null +++ b/typescript/packages/core/src/http/x402HTTPResourceServer.ts @@ -0,0 +1,919 @@ +import { x402ResourceServer } from "../server"; +import { + decodePaymentSignatureHeader, + encodePaymentRequiredHeader, + encodePaymentResponseHeader, +} from "."; +import { + PaymentPayload, + PaymentRequired, + SettleResponse, + SettleError, + Price, + Network, + PaymentRequirements, +} from "../types"; +import { x402Version } from ".."; + +/** + * Framework-agnostic HTTP adapter interface + * Implementations provide framework-specific HTTP operations + */ +export interface HTTPAdapter { + getHeader(name: string): string | undefined; + getMethod(): string; + getPath(): string; + getUrl(): string; + getAcceptHeader(): string; + getUserAgent(): string; + + /** + * Get query parameters from the request URL + * + * @returns Record of query parameter key-value pairs + */ + getQueryParams?(): Record; + + /** + * Get a specific query parameter by name + * + * @param name - The query parameter name + * @returns The query parameter value(s) or undefined + */ + getQueryParam?(name: string): string | string[] | undefined; + + /** + * Get the parsed request body + * Framework adapters should parse JSON/form data appropriately + * + * @returns The parsed request body + */ + getBody?(): unknown; +} + +/** + * Paywall configuration for HTML responses + */ +export interface PaywallConfig { + appName?: string; + appLogo?: string; + sessionTokenEndpoint?: string; + currentUrl?: string; + testnet?: boolean; +} + +/** + * Paywall provider interface for generating HTML + */ +export interface PaywallProvider { + generateHtml(paymentRequired: PaymentRequired, config?: PaywallConfig): string; +} + +/** + * Dynamic payTo function that receives HTTP request context + */ +export type DynamicPayTo = (context: HTTPRequestContext) => string | Promise; + +/** + * Dynamic price function that receives HTTP request context + */ +export type DynamicPrice = (context: HTTPRequestContext) => Price | Promise; + +/** + * Result of the unpaid response callback containing content type and body. + */ +export interface UnpaidResponseResult { + /** + * The content type for the response (e.g., 'application/json', 'text/plain'). + */ + contentType: string; + + /** + * The response body to include in the 402 response. + */ + body: unknown; +} + +/** + * Dynamic function to generate a custom response for unpaid requests. + * Receives the HTTP request context and returns the content type and body to include in the 402 response. + */ +export type UnpaidResponseBody = ( + context: HTTPRequestContext, +) => UnpaidResponseResult | Promise; + +/** + * A single payment option for a route + * Represents one way a client can pay for access to the resource + */ +export interface PaymentOption { + scheme: string; + payTo: string | DynamicPayTo; + price: Price | DynamicPrice; + network: Network; + maxTimeoutSeconds?: number; + extra?: Record; +} + +/** + * Route configuration for HTTP endpoints + * + * The 'accepts' field defines payment options for the route. + * Can be a single PaymentOption or an array of PaymentOptions for multiple payment methods. + */ +export interface RouteConfig { + // Payment option(s): single or array + accepts: PaymentOption | PaymentOption[]; + + // HTTP-specific metadata + resource?: string; + description?: string; + mimeType?: string; + customPaywallHtml?: string; + + /** + * Optional callback to generate a custom response for unpaid API requests. + * This allows servers to return preview data, error messages, or other content + * when a request lacks payment. + * + * For browser requests (Accept: text/html), the paywall HTML takes precedence. + * This callback is only used for API clients. + * + * If not provided, defaults to { contentType: 'application/json', body: {} }. + * + * @param context - The HTTP request context + * @returns An object containing both contentType and body for the 402 response + */ + unpaidResponseBody?: UnpaidResponseBody; + + // Extensions + extensions?: Record; +} + +/** + * Routes configuration - maps path patterns to route configs + */ +export type RoutesConfig = Record | RouteConfig; + +/** + * Hook that runs on every request to a protected route, before payment processing. + * Can grant access without payment, deny the request, or continue to payment flow. + * + * @returns + * - `void` - Continue to payment processing (default behavior) + * - `{ grantAccess: true }` - Grant access without requiring payment + * - `{ abort: true; reason: string }` - Deny the request (returns 403) + */ +export type ProtectedRequestHook = ( + context: HTTPRequestContext, + routeConfig: RouteConfig, +) => Promise; + +/** + * Compiled route for efficient matching + */ +export interface CompiledRoute { + verb: string; + regex: RegExp; + config: RouteConfig; +} + +/** + * HTTP request context that encapsulates all request data + */ +export interface HTTPRequestContext { + adapter: HTTPAdapter; + path: string; + method: string; + paymentHeader?: string; +} + +/** + * HTTP response instructions for the framework middleware + */ +export interface HTTPResponseInstructions { + status: number; + headers: Record; + body?: unknown; // e.g. Paywall for web browser requests, but could be any other type + isHtml?: boolean; // e.g. if body is a paywall, then isHtml is true +} + +/** + * Result of processing an HTTP request for payment + */ +export type HTTPProcessResult = + | { type: "no-payment-required" } + | { + type: "payment-verified"; + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + declaredExtensions?: Record; + } + | { type: "payment-error"; response: HTTPResponseInstructions }; + +/** + * Result of processSettlement + */ +export type ProcessSettleSuccessResponse = SettleResponse & { + success: true; + headers: Record; + requirements: PaymentRequirements; +}; + +export type ProcessSettleFailureResponse = SettleResponse & { + success: false; + errorReason: string; + errorMessage?: string; +}; + +export type ProcessSettleResultResponse = + | ProcessSettleSuccessResponse + | ProcessSettleFailureResponse; + +/** + * Represents a validation error for a specific route's payment configuration. + */ +export interface RouteValidationError { + /** The route pattern (e.g., "GET /api/weather") */ + routePattern: string; + /** The payment scheme that failed validation */ + scheme: string; + /** The network that failed validation */ + network: Network; + /** The type of validation failure */ + reason: "missing_scheme" | "missing_facilitator"; + /** Human-readable error message */ + message: string; +} + +/** + * Error thrown when route configuration validation fails. + */ +export class RouteConfigurationError extends Error { + /** The validation errors that caused this exception */ + public readonly errors: RouteValidationError[]; + + /** + * Creates a new RouteConfigurationError with the given validation errors. + * + * @param errors - The validation errors that caused this exception. + */ + constructor(errors: RouteValidationError[]) { + const message = `x402 Route Configuration Errors:\n${errors.map(e => ` - ${e.message}`).join("\n")}`; + super(message); + this.name = "RouteConfigurationError"; + this.errors = errors; + } +} + +/** + * HTTP-enhanced x402 resource server + * Provides framework-agnostic HTTP protocol handling + */ +export class x402HTTPResourceServer { + private ResourceServer: x402ResourceServer; + private compiledRoutes: CompiledRoute[] = []; + private routesConfig: RoutesConfig; + private paywallProvider?: PaywallProvider; + private protectedRequestHooks: ProtectedRequestHook[] = []; + + /** + * Creates a new x402HTTPResourceServer instance. + * + * @param ResourceServer - The core x402ResourceServer instance to use + * @param routes - Route configuration for payment-protected endpoints + */ + constructor(ResourceServer: x402ResourceServer, routes: RoutesConfig) { + this.ResourceServer = ResourceServer; + this.routesConfig = routes; + + // Handle both single route and multiple routes + const normalizedRoutes = + typeof routes === "object" && !("accepts" in routes) + ? (routes as Record) + : { "*": routes as RouteConfig }; + + for (const [pattern, config] of Object.entries(normalizedRoutes)) { + const parsed = this.parseRoutePattern(pattern); + this.compiledRoutes.push({ + verb: parsed.verb, + regex: parsed.regex, + config, + }); + } + } + + /** + * Get the underlying x402ResourceServer instance. + * + * @returns The underlying x402ResourceServer instance + */ + get server(): x402ResourceServer { + return this.ResourceServer; + } + + /** + * Get the routes configuration. + * + * @returns The routes configuration + */ + get routes(): RoutesConfig { + return this.routesConfig; + } + + /** + * Initialize the HTTP resource server. + * + * This method initializes the underlying resource server (fetching facilitator support) + * and then validates that all route payment configurations have corresponding + * registered schemes and facilitator support. + * + * @throws RouteConfigurationError if any route's payment options don't have + * corresponding registered schemes or facilitator support + * + * @example + * ```typescript + * const httpServer = new x402HTTPResourceServer(server, routes); + * await httpServer.initialize(); + * ``` + */ + async initialize(): Promise { + // First, initialize the underlying resource server (fetches facilitator support) + await this.ResourceServer.initialize(); + + // Then validate route configuration + const errors = this.validateRouteConfiguration(); + if (errors.length > 0) { + throw new RouteConfigurationError(errors); + } + } + + /** + * Register a custom paywall provider for generating HTML + * + * @param provider - PaywallProvider instance + * @returns This service instance for chaining + */ + registerPaywallProvider(provider: PaywallProvider): this { + this.paywallProvider = provider; + return this; + } + + /** + * Register a hook that runs on every request to a protected route, before payment processing. + * Hooks are executed in order of registration. The first hook to return a non-void result wins. + * + * @param hook - The request hook function + * @returns The x402HTTPResourceServer instance for chaining + */ + onProtectedRequest(hook: ProtectedRequestHook): this { + this.protectedRequestHooks.push(hook); + return this; + } + + /** + * Process HTTP request and return response instructions + * This is the main entry point for framework middleware + * + * @param context - HTTP request context + * @param paywallConfig - Optional paywall configuration + * @returns Process result indicating next action for middleware + */ + async processHTTPRequest( + context: HTTPRequestContext, + paywallConfig?: PaywallConfig, + ): Promise { + const { adapter, path, method } = context; + + // Find matching route + const routeConfig = this.getRouteConfig(path, method); + if (!routeConfig) { + return { type: "no-payment-required" }; // No payment required for this route + } + + // Execute request hooks before any payment processing + for (const hook of this.protectedRequestHooks) { + const result = await hook(context, routeConfig); + if (result && "grantAccess" in result) { + return { type: "no-payment-required" }; + } + if (result && "abort" in result) { + return { + type: "payment-error", + response: { + status: 403, + headers: { "Content-Type": "application/json" }, + body: { error: result.reason }, + }, + }; + } + } + + // Normalize accepts field to array of payment options + const paymentOptions = this.normalizePaymentOptions(routeConfig); + + // Check for payment header (v1 or v2) + const paymentPayload = this.extractPayment(adapter); + + // Create resource info, using config override if provided + const resourceInfo = { + url: routeConfig.resource || context.adapter.getUrl(), + description: routeConfig.description || "", + mimeType: routeConfig.mimeType || "", + }; + + // Build requirements from all payment options + // (this method handles resolving dynamic functions internally) + let requirements = await this.ResourceServer.buildPaymentRequirementsFromOptions( + paymentOptions, + context, + ); + + let extensions = routeConfig.extensions; + if (extensions) { + extensions = this.ResourceServer.enrichExtensions(extensions, context); + } + + // createPaymentRequiredResponse already handles extension enrichment in the core layer + const paymentRequired = await this.ResourceServer.createPaymentRequiredResponse( + requirements, + resourceInfo, + !paymentPayload ? "Payment required" : undefined, + extensions, + ); + + // If no payment provided + if (!paymentPayload) { + // Resolve custom unpaid response body if provided + const unpaidBody = routeConfig.unpaidResponseBody + ? await routeConfig.unpaidResponseBody(context) + : undefined; + + return { + type: "payment-error", + response: this.createHTTPResponse( + paymentRequired, + this.isWebBrowser(adapter), + paywallConfig, + routeConfig.customPaywallHtml, + unpaidBody, + ), + }; + } + + // Verify payment + try { + const matchingRequirements = this.ResourceServer.findMatchingRequirements( + paymentRequired.accepts, + paymentPayload, + ); + + if (!matchingRequirements) { + const errorResponse = await this.ResourceServer.createPaymentRequiredResponse( + requirements, + resourceInfo, + "No matching payment requirements", + routeConfig.extensions, + ); + return { + type: "payment-error", + response: this.createHTTPResponse(errorResponse, false, paywallConfig), + }; + } + + const verifyResult = await this.ResourceServer.verifyPayment( + paymentPayload, + matchingRequirements, + ); + + if (!verifyResult.isValid) { + const errorResponse = await this.ResourceServer.createPaymentRequiredResponse( + requirements, + resourceInfo, + verifyResult.invalidReason, + routeConfig.extensions, + ); + return { + type: "payment-error", + response: this.createHTTPResponse(errorResponse, false, paywallConfig), + }; + } + + // Payment is valid, return data needed for settlement + return { + type: "payment-verified", + paymentPayload, + paymentRequirements: matchingRequirements, + declaredExtensions: routeConfig.extensions, + }; + } catch (error) { + const errorResponse = await this.ResourceServer.createPaymentRequiredResponse( + requirements, + resourceInfo, + error instanceof Error ? error.message : "Payment verification failed", + routeConfig.extensions, + ); + return { + type: "payment-error", + response: this.createHTTPResponse(errorResponse, false, paywallConfig), + }; + } + } + + /** + * Process settlement after successful response + * + * @param paymentPayload - The verified payment payload + * @param requirements - The matching payment requirements + * @param declaredExtensions - Optional declared extensions (for per-key enrichment) + * @returns ProcessSettleResultResponse - SettleResponse with headers if success or errorReason if failure + */ + async processSettlement( + paymentPayload: PaymentPayload, + requirements: PaymentRequirements, + declaredExtensions?: Record, + ): Promise { + try { + const settleResponse = await this.ResourceServer.settlePayment( + paymentPayload, + requirements, + declaredExtensions, + ); + + if (!settleResponse.success) { + return { + ...settleResponse, + success: false, + errorReason: settleResponse.errorReason || "Settlement failed", + errorMessage: + settleResponse.errorMessage || settleResponse.errorReason || "Settlement failed", + }; + } + + return { + ...settleResponse, + success: true, + headers: this.createSettlementHeaders(settleResponse), + requirements, + }; + } catch (error) { + if (error instanceof SettleError) { + return { + success: false, + errorReason: error.errorReason || error.message, + errorMessage: error.errorMessage || error.errorReason || error.message, + payer: error.payer, + network: error.network, + transaction: error.transaction, + }; + } + return { + success: false, + errorReason: error instanceof Error ? error.message : "Settlement failed", + errorMessage: error instanceof Error ? error.message : "Settlement failed", + network: requirements.network as Network, + transaction: "", + }; + } + } + + /** + * Check if a request requires payment based on route configuration + * + * @param context - HTTP request context + * @returns True if the route requires payment, false otherwise + */ + requiresPayment(context: HTTPRequestContext): boolean { + const routeConfig = this.getRouteConfig(context.path, context.method); + return routeConfig !== undefined; + } + + /** + * Normalizes a RouteConfig's accepts field into an array of PaymentOptions + * Handles both single PaymentOption and array formats + * + * @param routeConfig - Route configuration + * @returns Array of payment options + */ + private normalizePaymentOptions(routeConfig: RouteConfig): PaymentOption[] { + return Array.isArray(routeConfig.accepts) ? routeConfig.accepts : [routeConfig.accepts]; + } + + /** + * Validates that all payment options in routes have corresponding registered schemes + * and facilitator support. + * + * @returns Array of validation errors (empty if all routes are valid) + */ + private validateRouteConfiguration(): RouteValidationError[] { + const errors: RouteValidationError[] = []; + + // Normalize routes to array of [pattern, config] pairs + const normalizedRoutes = + typeof this.routesConfig === "object" && !("accepts" in this.routesConfig) + ? Object.entries(this.routesConfig as Record) + : [["*", this.routesConfig as RouteConfig] as [string, RouteConfig]]; + + for (const [pattern, config] of normalizedRoutes) { + const paymentOptions = this.normalizePaymentOptions(config); + + for (const option of paymentOptions) { + // Check 1: Is scheme registered? + if (!this.ResourceServer.hasRegisteredScheme(option.network, option.scheme)) { + errors.push({ + routePattern: pattern, + scheme: option.scheme, + network: option.network, + reason: "missing_scheme", + message: `Route "${pattern}": No scheme implementation registered for "${option.scheme}" on network "${option.network}"`, + }); + // Skip facilitator check if scheme isn't registered + continue; + } + + // Check 2: Does facilitator support this scheme/network combination? + const supportedKind = this.ResourceServer.getSupportedKind( + x402Version, + option.network, + option.scheme, + ); + + if (!supportedKind) { + errors.push({ + routePattern: pattern, + scheme: option.scheme, + network: option.network, + reason: "missing_facilitator", + message: `Route "${pattern}": Facilitator does not support scheme "${option.scheme}" on network "${option.network}"`, + }); + } + } + } + + return errors; + } + + /** + * Get route configuration for a request + * + * @param path - Request path + * @param method - HTTP method + * @returns Route configuration or undefined if no match + */ + private getRouteConfig(path: string, method: string): RouteConfig | undefined { + const normalizedPath = this.normalizePath(path); + const upperMethod = method.toUpperCase(); + + const matchingRoute = this.compiledRoutes.find( + route => + route.regex.test(normalizedPath) && (route.verb === "*" || route.verb === upperMethod), + ); + + return matchingRoute?.config; + } + + /** + * Extract payment from HTTP headers (handles v1 and v2) + * + * @param adapter - HTTP adapter + * @returns Decoded payment payload or null + */ + private extractPayment(adapter: HTTPAdapter): PaymentPayload | null { + // Check v2 header first (PAYMENT-SIGNATURE) + const header = adapter.getHeader("payment-signature") || adapter.getHeader("PAYMENT-SIGNATURE"); + + if (header) { + try { + return decodePaymentSignatureHeader(header); + } catch (error) { + console.warn("Failed to decode PAYMENT-SIGNATURE header:", error); + } + } + + return null; + } + + /** + * Check if request is from a web browser + * + * @param adapter - HTTP adapter + * @returns True if request appears to be from a browser + */ + private isWebBrowser(adapter: HTTPAdapter): boolean { + const accept = adapter.getAcceptHeader(); + const userAgent = adapter.getUserAgent(); + return accept.includes("text/html") && userAgent.includes("Mozilla"); + } + + /** + * Create HTTP response instructions from payment required + * + * @param paymentRequired - Payment requirements + * @param isWebBrowser - Whether request is from browser + * @param paywallConfig - Paywall configuration + * @param customHtml - Custom HTML template + * @param unpaidResponse - Optional custom response (content type and body) for unpaid API requests + * @returns Response instructions + */ + private createHTTPResponse( + paymentRequired: PaymentRequired, + isWebBrowser: boolean, + paywallConfig?: PaywallConfig, + customHtml?: string, + unpaidResponse?: UnpaidResponseResult, + ): HTTPResponseInstructions { + // Use 412 Precondition Failed for permit2_allowance_required error + // This signals client needs to approve Permit2 before retrying + const status = paymentRequired.error === "permit2_allowance_required" ? 412 : 402; + + if (isWebBrowser) { + const html = this.generatePaywallHTML(paymentRequired, paywallConfig, customHtml); + return { + status, + headers: { "Content-Type": "text/html" }, + body: html, + isHtml: true, + }; + } + + const response = this.createHTTPPaymentRequiredResponse(paymentRequired); + + // Use callback result if provided, otherwise default to JSON with empty object + const contentType = unpaidResponse ? unpaidResponse.contentType : "application/json"; + const body = unpaidResponse ? unpaidResponse.body : {}; + + return { + status, + headers: { + "Content-Type": contentType, + ...response.headers, + }, + body, + }; + } + + /** + * Create HTTP payment required response (v1 puts in body, v2 puts in header) + * + * @param paymentRequired - Payment required object + * @returns Headers and body for the HTTP response + */ + private createHTTPPaymentRequiredResponse(paymentRequired: PaymentRequired): { + headers: Record; + } { + return { + headers: { + "PAYMENT-REQUIRED": encodePaymentRequiredHeader(paymentRequired), + }, + }; + } + + /** + * Create settlement response headers + * + * @param settleResponse - Settlement response + * @returns Headers to add to response + */ + private createSettlementHeaders(settleResponse: SettleResponse): Record { + const encoded = encodePaymentResponseHeader(settleResponse); + return { "PAYMENT-RESPONSE": encoded }; + } + + /** + * Parse route pattern into verb and regex + * + * @param pattern - Route pattern like "GET /api/*" or "/api/[id]" + * @returns Parsed pattern with verb and regex + */ + private parseRoutePattern(pattern: string): { verb: string; regex: RegExp } { + const [verb, path] = pattern.includes(" ") ? pattern.split(/\s+/) : ["*", pattern]; + + const regex = new RegExp( + `^${ + path + .replace(/[$()+.?^{|}]/g, "\\$&") // Escape regex special chars + .replace(/\*/g, ".*?") // Wildcards + .replace(/\[([^\]]+)\]/g, "[^/]+") // Parameters + .replace(/\//g, "\\/") // Escape slashes + }$`, + "i", + ); + + return { verb: verb.toUpperCase(), regex }; + } + + /** + * Normalize path for matching + * + * @param path - Raw path from request + * @returns Normalized path + */ + private normalizePath(path: string): string { + const pathWithoutQuery = path.split(/[?#]/)[0]; + + let decodedOrRawPath: string; + try { + decodedOrRawPath = decodeURIComponent(pathWithoutQuery); + } catch { + decodedOrRawPath = pathWithoutQuery; + } + + return decodedOrRawPath + .replace(/\\/g, "/") + .replace(/\/+/g, "/") + .replace(/(.+?)\/+$/, "$1"); + } + + /** + * Generate paywall HTML for browser requests + * + * @param paymentRequired - Payment required response + * @param paywallConfig - Optional paywall configuration + * @param customHtml - Optional custom HTML template + * @returns HTML string + */ + private generatePaywallHTML( + paymentRequired: PaymentRequired, + paywallConfig?: PaywallConfig, + customHtml?: string, + ): string { + if (customHtml) { + return customHtml; + } + + // Use custom paywall provider if set + if (this.paywallProvider) { + return this.paywallProvider.generateHtml(paymentRequired, paywallConfig); + } + + // Try to use @x402/paywall if available (optional dependency) + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const paywall = require("@x402/paywall"); + const displayAmount = this.getDisplayAmount(paymentRequired); + const resource = paymentRequired.resource; + + return paywall.getPaywallHtml({ + amount: displayAmount, + paymentRequired, + currentUrl: resource?.url || paywallConfig?.currentUrl || "", + testnet: paywallConfig?.testnet ?? true, + appName: paywallConfig?.appName, + appLogo: paywallConfig?.appLogo, + sessionTokenEndpoint: paywallConfig?.sessionTokenEndpoint, + }); + } catch { + // @x402/paywall not installed, fall back to basic HTML + } + + // Fallback: Basic HTML paywall + const resource = paymentRequired.resource; + const displayAmount = this.getDisplayAmount(paymentRequired); + + return ` + + + + Payment Required + + + + +
+ ${paywallConfig?.appLogo ? `${paywallConfig.appName || ` : ""} +

Payment Required

+ ${resource ? `

Resource: ${resource.description || resource.url}

` : ""} +

Amount: $${displayAmount.toFixed(2)} USDC

+
+ +

+ Note: Install @x402/paywall for full wallet connection and payment UI. +

+
+
+ + + `; + } + + /** + * Extract display amount from payment requirements. + * + * @param paymentRequired - The payment required object + * @returns The display amount in decimal format + */ + private getDisplayAmount(paymentRequired: PaymentRequired): number { + const accepts = paymentRequired.accepts; + if (accepts && accepts.length > 0) { + const firstReq = accepts[0]; + if ("amount" in firstReq) { + // V2 format + return parseFloat(firstReq.amount) / 1000000; // Assuming USDC with 6 decimals + } + } + return 0; + } +} diff --git a/typescript/packages/core/src/index.ts b/typescript/packages/core/src/index.ts new file mode 100644 index 0000000..3942c73 --- /dev/null +++ b/typescript/packages/core/src/index.ts @@ -0,0 +1 @@ +export const x402Version = 2; diff --git a/typescript/packages/core/src/schemas/index.ts b/typescript/packages/core/src/schemas/index.ts new file mode 100644 index 0000000..fde7a4b --- /dev/null +++ b/typescript/packages/core/src/schemas/index.ts @@ -0,0 +1,367 @@ +import { z } from "zod"; + +// ============================================================================ +// Reusable Primitive Schemas +// ============================================================================ + +/** + * Non-empty string schema - a string with at least one character. + * Used for required string fields that cannot be empty. + */ +export const NonEmptyString = z.string().min(1); +export type NonEmptyString = z.infer; + +/** + * Any record schema - an object with unknown keys and values. + * Used for scheme-specific payloads and other extensible objects. + */ +export const Any = z.record(z.unknown()); +export type Any = z.infer; + +/** + * Optional any record schema - an optional object with unknown keys and values. + * Used for optional extension fields like `extra` and `extensions`. + */ +export const OptionalAny = z.record(z.unknown()).optional(); +export type OptionalAny = z.infer; + +// ============================================================================ +// Network Schemas +// ============================================================================ + +/** + * Network identifier schema for V1 - loose validation. + * V1 accepts any non-empty string for backwards compatibility. + */ +export const NetworkSchemaV1 = NonEmptyString; +export type NetworkV1 = z.infer; + +/** + * Network identifier schema for V2 - CAIP-2 format validation. + * V2 requires minimum length of 3 and a colon separator (e.g., "eip155:84532", "solana:devnet"). + */ +export const NetworkSchemaV2 = z + .string() + .min(3) + .refine(val => val.includes(":"), { + message: "Network must be in CAIP-2 format (e.g., 'eip155:84532')", + }); +export type NetworkV2 = z.infer; + +/** + * Union network schema - accepts either V1 or V2 format. + */ +export const NetworkSchema = z.union([NetworkSchemaV1, NetworkSchemaV2]); +export type Network = z.infer; + +// ============================================================================ +// Shared Schemas +// ============================================================================ + +/** + * ResourceInfo schema for V2 - describes the protected resource. + */ +export const ResourceInfoSchema = z.object({ + url: NonEmptyString, + description: z.string().optional(), + mimeType: z.string().optional(), +}); +export type ResourceInfo = z.infer; + +// ============================================================================ +// V1 Schemas +// ============================================================================ + +/** + * PaymentRequirements schema for V1. + * V1 includes resource info directly in the requirements object. + */ +export const PaymentRequirementsV1Schema = z.object({ + scheme: NonEmptyString, + network: NetworkSchemaV1, + maxAmountRequired: NonEmptyString, + resource: NonEmptyString, // URL string in V1 + description: z.string(), + mimeType: z.string().optional(), + outputSchema: Any.optional().nullable(), + payTo: NonEmptyString, + maxTimeoutSeconds: z.number().positive(), + asset: NonEmptyString, + extra: OptionalAny, +}); +export type PaymentRequirementsV1 = z.infer; + +/** + * PaymentRequired (402 response) schema for V1. + * Contains payment requirements when a resource requires payment. + */ +export const PaymentRequiredV1Schema = z.object({ + x402Version: z.literal(1), + error: z.string().optional(), + accepts: z.array(PaymentRequirementsV1Schema).min(1), +}); +export type PaymentRequiredV1 = z.infer; + +/** + * PaymentPayload schema for V1. + * Contains the payment data sent by the client. + */ +export const PaymentPayloadV1Schema = z.object({ + x402Version: z.literal(1), + scheme: NonEmptyString, + network: NetworkSchemaV1, + payload: Any, +}); +export type PaymentPayloadV1 = z.infer; + +// ============================================================================ +// V2 Schemas +// ============================================================================ + +/** + * PaymentRequirements schema for V2. + * V2 uses "amount" instead of "maxAmountRequired" and doesn't include resource info. + */ +export const PaymentRequirementsV2Schema = z.object({ + scheme: NonEmptyString, + network: NetworkSchemaV2, + amount: NonEmptyString, + asset: NonEmptyString, + payTo: NonEmptyString, + maxTimeoutSeconds: z.number().positive(), + extra: OptionalAny, +}); +export type PaymentRequirementsV2 = z.infer; + +/** + * PaymentRequired (402 response) schema for V2. + * Contains payment requirements when a resource requires payment. + */ +export const PaymentRequiredV2Schema = z.object({ + x402Version: z.literal(2), + error: z.string().optional(), + resource: ResourceInfoSchema, + accepts: z.array(PaymentRequirementsV2Schema).min(1), + extensions: OptionalAny, +}); +export type PaymentRequiredV2 = z.infer; + +/** + * PaymentPayload schema for V2. + * Contains the payment data sent by the client. + */ +export const PaymentPayloadV2Schema = z.object({ + x402Version: z.literal(2), + resource: ResourceInfoSchema.optional(), + accepted: PaymentRequirementsV2Schema, + payload: Any, + extensions: OptionalAny, +}); +export type PaymentPayloadV2 = z.infer; + +// ============================================================================ +// Union Schemas (V1 | V2) +// ============================================================================ + +/** + * PaymentRequirements union schema - accepts either V1 or V2 format. + * Use this when you need to handle both versions. + */ +export const PaymentRequirementsSchema = z.union([ + PaymentRequirementsV1Schema, + PaymentRequirementsV2Schema, +]); +export type PaymentRequirements = z.infer; + +/** + * PaymentRequired union schema - accepts either V1 or V2 format. + * Uses discriminated union on x402Version for efficient parsing. + */ +export const PaymentRequiredSchema = z.discriminatedUnion("x402Version", [ + PaymentRequiredV1Schema, + PaymentRequiredV2Schema, +]); +export type PaymentRequired = z.infer; + +/** + * PaymentPayload union schema - accepts either V1 or V2 format. + * Uses discriminated union on x402Version for efficient parsing. + */ +export const PaymentPayloadSchema = z.discriminatedUnion("x402Version", [ + PaymentPayloadV1Schema, + PaymentPayloadV2Schema, +]); +export type PaymentPayload = z.infer; + +// ============================================================================ +// Validation Functions +// ============================================================================ + +/** + * Validates a PaymentRequired object (V1 or V2). + * + * @param value - The value to validate + * @returns A result object with success status and data or error + */ +export function parsePaymentRequired( + value: unknown, +): z.SafeParseReturnType { + return PaymentRequiredSchema.safeParse(value); +} + +/** + * Validates a PaymentRequired object and throws on error. + * + * @param value - The value to validate + * @returns The validated PaymentRequired + * @throws ZodError if validation fails + */ +export function validatePaymentRequired(value: unknown): PaymentRequired { + return PaymentRequiredSchema.parse(value); +} + +/** + * Type guard for PaymentRequired (V1 or V2). + * + * @param value - The value to check + * @returns True if the value is a valid PaymentRequired + */ +export function isPaymentRequired(value: unknown): value is PaymentRequired { + return PaymentRequiredSchema.safeParse(value).success; +} + +/** + * Validates a PaymentRequirements object (V1 or V2). + * + * @param value - The value to validate + * @returns A result object with success status and data or error + */ +export function parsePaymentRequirements( + value: unknown, +): z.SafeParseReturnType { + return PaymentRequirementsSchema.safeParse(value); +} + +/** + * Validates a PaymentRequirements object and throws on error. + * + * @param value - The value to validate + * @returns The validated PaymentRequirements + * @throws ZodError if validation fails + */ +export function validatePaymentRequirements(value: unknown): PaymentRequirements { + return PaymentRequirementsSchema.parse(value); +} + +/** + * Type guard for PaymentRequirements (V1 or V2). + * + * @param value - The value to check + * @returns True if the value is a valid PaymentRequirements + */ +export function isPaymentRequirements(value: unknown): value is PaymentRequirements { + return PaymentRequirementsSchema.safeParse(value).success; +} + +/** + * Validates a PaymentPayload object (V1 or V2). + * + * @param value - The value to validate + * @returns A result object with success status and data or error + */ +export function parsePaymentPayload( + value: unknown, +): z.SafeParseReturnType { + return PaymentPayloadSchema.safeParse(value); +} + +/** + * Validates a PaymentPayload object and throws on error. + * + * @param value - The value to validate + * @returns The validated PaymentPayload + * @throws ZodError if validation fails + */ +export function validatePaymentPayload(value: unknown): PaymentPayload { + return PaymentPayloadSchema.parse(value); +} + +/** + * Type guard for PaymentPayload (V1 or V2). + * + * @param value - The value to check + * @returns True if the value is a valid PaymentPayload + */ +export function isPaymentPayload(value: unknown): value is PaymentPayload { + return PaymentPayloadSchema.safeParse(value).success; +} + +// ============================================================================ +// Version-Specific Type Guards +// ============================================================================ + +/** + * Type guard for PaymentRequiredV1. + * + * @param value - The value to check + * @returns True if the value is a valid PaymentRequiredV1 + */ +export function isPaymentRequiredV1(value: unknown): value is PaymentRequiredV1 { + return PaymentRequiredV1Schema.safeParse(value).success; +} + +/** + * Type guard for PaymentRequiredV2. + * + * @param value - The value to check + * @returns True if the value is a valid PaymentRequiredV2 + */ +export function isPaymentRequiredV2(value: unknown): value is PaymentRequiredV2 { + return PaymentRequiredV2Schema.safeParse(value).success; +} + +/** + * Type guard for PaymentRequirementsV1. + * + * @param value - The value to check + * @returns True if the value is a valid PaymentRequirementsV1 + */ +export function isPaymentRequirementsV1(value: unknown): value is PaymentRequirementsV1 { + return PaymentRequirementsV1Schema.safeParse(value).success; +} + +/** + * Type guard for PaymentRequirementsV2. + * + * @param value - The value to check + * @returns True if the value is a valid PaymentRequirementsV2 + */ +export function isPaymentRequirementsV2(value: unknown): value is PaymentRequirementsV2 { + return PaymentRequirementsV2Schema.safeParse(value).success; +} + +/** + * Type guard for PaymentPayloadV1. + * + * @param value - The value to check + * @returns True if the value is a valid PaymentPayloadV1 + */ +export function isPaymentPayloadV1(value: unknown): value is PaymentPayloadV1 { + return PaymentPayloadV1Schema.safeParse(value).success; +} + +/** + * Type guard for PaymentPayloadV2. + * + * @param value - The value to check + * @returns True if the value is a valid PaymentPayloadV2 + */ +export function isPaymentPayloadV2(value: unknown): value is PaymentPayloadV2 { + return PaymentPayloadV2Schema.safeParse(value).success; +} + +// ============================================================================ +// Re-export zod for convenience +// ============================================================================ + +export { z } from "zod"; diff --git a/typescript/packages/core/src/server/index.ts b/typescript/packages/core/src/server/index.ts new file mode 100644 index 0000000..ada9a83 --- /dev/null +++ b/typescript/packages/core/src/server/index.ts @@ -0,0 +1,24 @@ +export { x402ResourceServer } from "./x402ResourceServer"; +export type { ResourceConfig, ResourceInfo, SettleResultContext } from "./x402ResourceServer"; + +export { HTTPFacilitatorClient } from "../http/httpFacilitatorClient"; +export type { FacilitatorClient, FacilitatorConfig } from "../http/httpFacilitatorClient"; + +export { x402HTTPResourceServer, RouteConfigurationError } from "../http/x402HTTPResourceServer"; +export type { + HTTPRequestContext, + HTTPResponseInstructions, + HTTPProcessResult, + PaywallConfig, + PaywallProvider, + RouteConfig, + CompiledRoute, + HTTPAdapter, + RoutesConfig, + UnpaidResponseBody, + UnpaidResponseResult, + ProcessSettleResultResponse, + ProcessSettleSuccessResponse, + ProcessSettleFailureResponse, + RouteValidationError, +} from "../http/x402HTTPResourceServer"; diff --git a/typescript/packages/core/src/server/x402ResourceServer.ts b/typescript/packages/core/src/server/x402ResourceServer.ts new file mode 100644 index 0000000..1358c9c --- /dev/null +++ b/typescript/packages/core/src/server/x402ResourceServer.ts @@ -0,0 +1,931 @@ +import { + SettleError, + SettleResponse, + VerifyResponse, + SupportedResponse, + SupportedKind, +} from "../types/facilitator"; +import { PaymentPayload, PaymentRequirements, PaymentRequired } from "../types/payments"; +import { SchemeNetworkServer } from "../types/mechanisms"; +import { Price, Network, ResourceServerExtension, VerifyError } from "../types"; +import { deepEqual, findByNetworkAndScheme } from "../utils"; +import { FacilitatorClient, HTTPFacilitatorClient } from "../http/httpFacilitatorClient"; +import { x402Version } from ".."; + +/** + * Configuration for a protected resource + * Only contains payment-specific configuration, not resource metadata + */ +export interface ResourceConfig { + scheme: string; + payTo: string; // Payment recipient address + price: Price; + network: Network; + maxTimeoutSeconds?: number; + extra?: Record; // Scheme-specific additional data +} + +/** + * Resource information for PaymentRequired response + */ +export interface ResourceInfo { + url: string; + description: string; + mimeType: string; +} + +/** + * Lifecycle Hook Context Interfaces + */ + +export interface PaymentRequiredContext { + requirements: PaymentRequirements[]; + resourceInfo: ResourceInfo; + error?: string; + paymentRequiredResponse: PaymentRequired; +} + +export interface VerifyContext { + paymentPayload: PaymentPayload; + requirements: PaymentRequirements; +} + +export interface VerifyResultContext extends VerifyContext { + result: VerifyResponse; +} + +export interface VerifyFailureContext extends VerifyContext { + error: Error; +} + +export interface SettleContext { + paymentPayload: PaymentPayload; + requirements: PaymentRequirements; +} + +export interface SettleResultContext extends SettleContext { + result: SettleResponse; +} + +export interface SettleFailureContext extends SettleContext { + error: Error; +} + +/** + * Lifecycle Hook Type Definitions + */ + +export type BeforeVerifyHook = ( + context: VerifyContext, +) => Promise; + +export type AfterVerifyHook = (context: VerifyResultContext) => Promise; + +export type OnVerifyFailureHook = ( + context: VerifyFailureContext, +) => Promise; + +export type BeforeSettleHook = ( + context: SettleContext, +) => Promise; + +export type AfterSettleHook = (context: SettleResultContext) => Promise; + +export type OnSettleFailureHook = ( + context: SettleFailureContext, +) => Promise; + +/** + * Core x402 protocol server for resource protection + * Transport-agnostic implementation of the x402 payment protocol + */ +export class x402ResourceServer { + private facilitatorClients: FacilitatorClient[]; + private registeredServerSchemes: Map> = new Map(); + private supportedResponsesMap: Map>> = + new Map(); + private facilitatorClientsMap: Map>> = + new Map(); + private registeredExtensions: Map = new Map(); + + private beforeVerifyHooks: BeforeVerifyHook[] = []; + private afterVerifyHooks: AfterVerifyHook[] = []; + private onVerifyFailureHooks: OnVerifyFailureHook[] = []; + private beforeSettleHooks: BeforeSettleHook[] = []; + private afterSettleHooks: AfterSettleHook[] = []; + private onSettleFailureHooks: OnSettleFailureHook[] = []; + + /** + * Creates a new x402ResourceServer instance. + * + * @param facilitatorClients - Optional facilitator client(s) for payment processing + */ + constructor(facilitatorClients?: FacilitatorClient | FacilitatorClient[]) { + // Normalize facilitator clients to array + if (!facilitatorClients) { + // No clients provided, create a default HTTP client + this.facilitatorClients = [new HTTPFacilitatorClient()]; + } else if (Array.isArray(facilitatorClients)) { + // Array of clients provided + this.facilitatorClients = + facilitatorClients.length > 0 ? facilitatorClients : [new HTTPFacilitatorClient()]; + } else { + // Single client provided + this.facilitatorClients = [facilitatorClients]; + } + } + + /** + * Register a scheme/network server implementation. + * + * @param network - The network identifier + * @param server - The scheme/network server implementation + * @returns The x402ResourceServer instance for chaining + */ + register(network: Network, server: SchemeNetworkServer): x402ResourceServer { + if (!this.registeredServerSchemes.has(network)) { + this.registeredServerSchemes.set(network, new Map()); + } + + const serverByScheme = this.registeredServerSchemes.get(network)!; + if (!serverByScheme.has(server.scheme)) { + serverByScheme.set(server.scheme, server); + } + + return this; + } + + /** + * Check if a scheme is registered for a given network. + * + * @param network - The network identifier + * @param scheme - The payment scheme name + * @returns True if the scheme is registered for the network, false otherwise + */ + hasRegisteredScheme(network: Network, scheme: string): boolean { + return !!findByNetworkAndScheme(this.registeredServerSchemes, scheme, network); + } + + /** + * Registers a resource service extension that can enrich extension declarations. + * + * @param extension - The extension to register + * @returns The x402ResourceServer instance for chaining + */ + registerExtension(extension: ResourceServerExtension): this { + this.registeredExtensions.set(extension.key, extension); + return this; + } + + /** + * Check if an extension is registered. + * + * @param key - The extension key + * @returns True if the extension is registered + */ + hasExtension(key: string): boolean { + return this.registeredExtensions.has(key); + } + + /** + * Get all registered extensions. + * + * @returns Array of registered extensions + */ + getExtensions(): ResourceServerExtension[] { + return Array.from(this.registeredExtensions.values()); + } + + /** + * Enriches declared extensions using registered extension hooks. + * + * @param declaredExtensions - Extensions declared on the route + * @param transportContext - Transport-specific context (HTTP, A2A, MCP, etc.) + * @returns Enriched extensions map + */ + enrichExtensions( + declaredExtensions: Record, + transportContext: unknown, + ): Record { + const enriched: Record = {}; + + for (const [key, declaration] of Object.entries(declaredExtensions)) { + const extension = this.registeredExtensions.get(key); + + if (extension?.enrichDeclaration) { + enriched[key] = extension.enrichDeclaration(declaration, transportContext); + } else { + enriched[key] = declaration; + } + } + + return enriched; + } + + /** + * Register a hook to execute before payment verification. + * Can abort verification by returning { abort: true, reason: string } + * + * @param hook - The hook function to register + * @returns The x402ResourceServer instance for chaining + */ + onBeforeVerify(hook: BeforeVerifyHook): x402ResourceServer { + this.beforeVerifyHooks.push(hook); + return this; + } + + /** + * Register a hook to execute after successful payment verification. + * + * @param hook - The hook function to register + * @returns The x402ResourceServer instance for chaining + */ + onAfterVerify(hook: AfterVerifyHook): x402ResourceServer { + this.afterVerifyHooks.push(hook); + return this; + } + + /** + * Register a hook to execute when payment verification fails. + * Can recover from failure by returning { recovered: true, result: VerifyResponse } + * + * @param hook - The hook function to register + * @returns The x402ResourceServer instance for chaining + */ + onVerifyFailure(hook: OnVerifyFailureHook): x402ResourceServer { + this.onVerifyFailureHooks.push(hook); + return this; + } + + /** + * Register a hook to execute before payment settlement. + * Can abort settlement by returning { abort: true, reason: string } + * + * @param hook - The hook function to register + * @returns The x402ResourceServer instance for chaining + */ + onBeforeSettle(hook: BeforeSettleHook): x402ResourceServer { + this.beforeSettleHooks.push(hook); + return this; + } + + /** + * Register a hook to execute after successful payment settlement. + * + * @param hook - The hook function to register + * @returns The x402ResourceServer instance for chaining + */ + onAfterSettle(hook: AfterSettleHook): x402ResourceServer { + this.afterSettleHooks.push(hook); + return this; + } + + /** + * Register a hook to execute when payment settlement fails. + * Can recover from failure by returning { recovered: true, result: SettleResponse } + * + * @param hook - The hook function to register + * @returns The x402ResourceServer instance for chaining + */ + onSettleFailure(hook: OnSettleFailureHook): x402ResourceServer { + this.onSettleFailureHooks.push(hook); + return this; + } + + /** + * Initialize by fetching supported kinds from all facilitators + * Creates mappings for supported responses and facilitator clients + * Earlier facilitators in the array get precedence + */ + async initialize(): Promise { + // Clear existing mappings + this.supportedResponsesMap.clear(); + this.facilitatorClientsMap.clear(); + + // Fetch supported kinds from all facilitator clients + // Process in order to give precedence to earlier facilitators + for (const facilitatorClient of this.facilitatorClients) { + try { + const supported = await facilitatorClient.getSupported(); + + // Process each supported kind (now flat array with version in each element) + for (const kind of supported.kinds) { + const x402Version = kind.x402Version; + + // Get or create version map for supported responses + if (!this.supportedResponsesMap.has(x402Version)) { + this.supportedResponsesMap.set(x402Version, new Map()); + } + const responseVersionMap = this.supportedResponsesMap.get(x402Version)!; + + // Get or create version map for facilitator clients + if (!this.facilitatorClientsMap.has(x402Version)) { + this.facilitatorClientsMap.set(x402Version, new Map()); + } + const clientVersionMap = this.facilitatorClientsMap.get(x402Version)!; + + // Get or create network map for responses + if (!responseVersionMap.has(kind.network)) { + responseVersionMap.set(kind.network, new Map()); + } + const responseNetworkMap = responseVersionMap.get(kind.network)!; + + // Get or create network map for clients + if (!clientVersionMap.has(kind.network)) { + clientVersionMap.set(kind.network, new Map()); + } + const clientNetworkMap = clientVersionMap.get(kind.network)!; + + // Only store if not already present (gives precedence to earlier facilitators) + if (!responseNetworkMap.has(kind.scheme)) { + responseNetworkMap.set(kind.scheme, supported); + clientNetworkMap.set(kind.scheme, facilitatorClient); + } + } + } catch (error) { + // Log error but continue with other facilitators + console.warn(`Failed to fetch supported kinds from facilitator: ${error}`); + } + } + } + + /** + * Get supported kind for a specific version, network, and scheme + * + * @param x402Version - The x402 version + * @param network - The network identifier + * @param scheme - The payment scheme + * @returns The supported kind or undefined if not found + */ + getSupportedKind( + x402Version: number, + network: Network, + scheme: string, + ): SupportedKind | undefined { + const versionMap = this.supportedResponsesMap.get(x402Version); + if (!versionMap) return undefined; + + const supportedResponse = findByNetworkAndScheme(versionMap, scheme, network); + if (!supportedResponse) return undefined; + + // Find the specific kind from the response (kinds are flat array with version in each element) + return supportedResponse.kinds.find( + kind => + kind.x402Version === x402Version && kind.network === network && kind.scheme === scheme, + ); + } + + /** + * Get facilitator extensions for a specific version, network, and scheme + * + * @param x402Version - The x402 version + * @param network - The network identifier + * @param scheme - The payment scheme + * @returns The facilitator extensions or empty array if not found + */ + getFacilitatorExtensions(x402Version: number, network: Network, scheme: string): string[] { + const versionMap = this.supportedResponsesMap.get(x402Version); + if (!versionMap) return []; + + const supportedResponse = findByNetworkAndScheme(versionMap, scheme, network); + return supportedResponse?.extensions || []; + } + + /** + * Build payment requirements for a protected resource + * + * @param resourceConfig - Configuration for the protected resource + * @returns Array of payment requirements + */ + async buildPaymentRequirements(resourceConfig: ResourceConfig): Promise { + const requirements: PaymentRequirements[] = []; + + // Find the matching server implementation + const scheme = resourceConfig.scheme; + const SchemeNetworkServer = findByNetworkAndScheme( + this.registeredServerSchemes, + scheme, + resourceConfig.network, + ); + + if (!SchemeNetworkServer) { + // Fallback to placeholder implementation if no server registered + // TODO: Remove this fallback once implementations are registered + console.warn( + `No server implementation registered for scheme: ${scheme}, network: ${resourceConfig.network}`, + ); + return requirements; + } + + // Find the matching supported kind from facilitator + const supportedKind = this.getSupportedKind( + x402Version, + resourceConfig.network, + SchemeNetworkServer.scheme, + ); + + if (!supportedKind) { + throw new Error( + `Facilitator does not support ${SchemeNetworkServer.scheme} on ${resourceConfig.network}. ` + + `Make sure to call initialize() to fetch supported kinds from facilitators.`, + ); + } + + // Get facilitator extensions for this combination + const facilitatorExtensions = this.getFacilitatorExtensions( + x402Version, + resourceConfig.network, + SchemeNetworkServer.scheme, + ); + + // Parse the price using the scheme's price parser + const parsedPrice = await SchemeNetworkServer.parsePrice( + resourceConfig.price, + resourceConfig.network, + ); + + // Build base payment requirements from resource config + const baseRequirements: PaymentRequirements = { + scheme: SchemeNetworkServer.scheme, + network: resourceConfig.network, + amount: parsedPrice.amount, + asset: parsedPrice.asset, + payTo: resourceConfig.payTo, + maxTimeoutSeconds: resourceConfig.maxTimeoutSeconds || 300, // Default 5 minutes + extra: { + ...parsedPrice.extra, + ...resourceConfig.extra, // Merge user-provided extra + }, + }; + + // Delegate to the implementation for scheme-specific enhancements + // Note: enhancePaymentRequirements expects x402Version in the kind, so we add it back + const requirement = await SchemeNetworkServer.enhancePaymentRequirements( + baseRequirements, + { + ...supportedKind, + x402Version, + }, + facilitatorExtensions, + ); + + requirements.push(requirement); + return requirements; + } + + /** + * Build payment requirements from multiple payment options + * This method handles resolving dynamic payTo/price functions and builds requirements for each option + * + * @param paymentOptions - Array of payment options to convert + * @param context - HTTP request context for resolving dynamic functions + * @returns Array of payment requirements (one per option) + */ + async buildPaymentRequirementsFromOptions( + paymentOptions: Array<{ + scheme: string; + payTo: string | ((context: TContext) => string | Promise); + price: Price | ((context: TContext) => Price | Promise); + network: Network; + maxTimeoutSeconds?: number; + }>, + context: TContext, + ): Promise { + const allRequirements: PaymentRequirements[] = []; + + for (const option of paymentOptions) { + // Resolve dynamic payTo and price if they are functions + const resolvedPayTo = + typeof option.payTo === "function" ? await option.payTo(context) : option.payTo; + const resolvedPrice = + typeof option.price === "function" ? await option.price(context) : option.price; + + const resourceConfig: ResourceConfig = { + scheme: option.scheme, + payTo: resolvedPayTo, + price: resolvedPrice, + network: option.network, + maxTimeoutSeconds: option.maxTimeoutSeconds, + }; + + // Use existing buildPaymentRequirements for each option + const requirements = await this.buildPaymentRequirements(resourceConfig); + allRequirements.push(...requirements); + } + + return allRequirements; + } + + /** + * Create a payment required response + * + * @param requirements - Payment requirements + * @param resourceInfo - Resource information + * @param error - Error message + * @param extensions - Optional declared extensions (for per-key enrichment) + * @returns Payment required response object + */ + async createPaymentRequiredResponse( + requirements: PaymentRequirements[], + resourceInfo: ResourceInfo, + error?: string, + extensions?: Record, + ): Promise { + // V2 response with resource at top level + let response: PaymentRequired = { + x402Version: 2, + error, + resource: resourceInfo, + accepts: requirements as PaymentRequirements[], + }; + + // Add extensions if provided + if (extensions && Object.keys(extensions).length > 0) { + response.extensions = extensions; + } + + // Let declared extensions add data to PaymentRequired response + if (extensions) { + for (const [key, declaration] of Object.entries(extensions)) { + const extension = this.registeredExtensions.get(key); + if (extension?.enrichPaymentRequiredResponse) { + try { + const context: PaymentRequiredContext = { + requirements, + resourceInfo, + error, + paymentRequiredResponse: response, + }; + const extensionData = await extension.enrichPaymentRequiredResponse( + declaration, + context, + ); + if (extensionData !== undefined) { + if (!response.extensions) { + response.extensions = {}; + } + response.extensions[key] = extensionData; + } + } catch (error) { + console.error( + `Error in enrichPaymentRequiredResponse hook for extension ${key}:`, + error, + ); + } + } + } + } + + return response; + } + + /** + * Verify a payment against requirements + * + * @param paymentPayload - The payment payload to verify + * @param requirements - The payment requirements + * @returns Verification response + */ + async verifyPayment( + paymentPayload: PaymentPayload, + requirements: PaymentRequirements, + ): Promise { + const context: VerifyContext = { + paymentPayload, + requirements, + }; + + // Execute beforeVerify hooks + for (const hook of this.beforeVerifyHooks) { + try { + const result = await hook(context); + if (result && "abort" in result && result.abort) { + return { + isValid: false, + invalidReason: result.reason, + invalidMessage: result.message, + }; + } + } catch (error) { + throw new VerifyError(400, { + isValid: false, + invalidReason: "before_verify_hook_error", + invalidMessage: error instanceof Error ? error.message : "", + }); + } + } + + try { + // Find the facilitator that supports this payment type + const facilitatorClient = this.getFacilitatorClient( + paymentPayload.x402Version, + requirements.network, + requirements.scheme, + ); + + let verifyResult: VerifyResponse; + + if (!facilitatorClient) { + // Fallback: try all facilitators if no specific support found + let lastError: Error | undefined; + + for (const client of this.facilitatorClients) { + try { + verifyResult = await client.verify(paymentPayload, requirements); + break; + } catch (error) { + lastError = error as Error; + } + } + + if (!verifyResult!) { + throw ( + lastError || + new Error( + `No facilitator supports ${requirements.scheme} on ${requirements.network} for v${paymentPayload.x402Version}`, + ) + ); + } + } else { + // Use the specific facilitator that supports this payment + verifyResult = await facilitatorClient.verify(paymentPayload, requirements); + } + + // Execute afterVerify hooks + const resultContext: VerifyResultContext = { + ...context, + result: verifyResult, + }; + + for (const hook of this.afterVerifyHooks) { + await hook(resultContext); + } + + return verifyResult; + } catch (error) { + const failureContext: VerifyFailureContext = { + ...context, + error: error as Error, + }; + + // Execute onVerifyFailure hooks + for (const hook of this.onVerifyFailureHooks) { + const result = await hook(failureContext); + if (result && "recovered" in result && result.recovered) { + return result.result; + } + } + + throw error; + } + } + + /** + * Settle a verified payment + * + * @param paymentPayload - The payment payload to settle + * @param requirements - The payment requirements + * @param declaredExtensions - Optional declared extensions (for per-key enrichment) + * @returns Settlement response + */ + async settlePayment( + paymentPayload: PaymentPayload, + requirements: PaymentRequirements, + declaredExtensions?: Record, + ): Promise { + const context: SettleContext = { + paymentPayload, + requirements, + }; + + // Execute beforeSettle hooks + for (const hook of this.beforeSettleHooks) { + try { + const result = await hook(context); + if (result && "abort" in result && result.abort) { + throw new SettleError(400, { + success: false, + errorReason: result.reason, + errorMessage: result.message, + transaction: "", + network: requirements.network, + }); + } + } catch (error) { + throw new SettleError(400, { + success: false, + errorReason: "before_settle_hook_error", + errorMessage: error instanceof Error ? error.message : "", + transaction: "", + network: requirements.network, + }); + } + } + + try { + // Find the facilitator that supports this payment type + const facilitatorClient = this.getFacilitatorClient( + paymentPayload.x402Version, + requirements.network, + requirements.scheme, + ); + + let settleResult: SettleResponse; + + if (!facilitatorClient) { + // Fallback: try all facilitators if no specific support found + let lastError: Error | undefined; + + for (const client of this.facilitatorClients) { + try { + settleResult = await client.settle(paymentPayload, requirements); + break; + } catch (error) { + lastError = error as Error; + } + } + + if (!settleResult!) { + throw ( + lastError || + new Error( + `No facilitator supports ${requirements.scheme} on ${requirements.network} for v${paymentPayload.x402Version}`, + ) + ); + } + } else { + // Use the specific facilitator that supports this payment + settleResult = await facilitatorClient.settle(paymentPayload, requirements); + } + + // Execute afterSettle hooks + const resultContext: SettleResultContext = { + ...context, + result: settleResult, + }; + + for (const hook of this.afterSettleHooks) { + await hook(resultContext); + } + + // Let declared extensions add data to settlement response + if (declaredExtensions) { + for (const [key, declaration] of Object.entries(declaredExtensions)) { + const extension = this.registeredExtensions.get(key); + if (extension?.enrichSettlementResponse) { + try { + const extensionData = await extension.enrichSettlementResponse( + declaration, + resultContext, + ); + if (extensionData !== undefined) { + if (!settleResult.extensions) { + settleResult.extensions = {}; + } + settleResult.extensions[key] = extensionData; + } + } catch (error) { + console.error(`Error in enrichSettlementResponse hook for extension ${key}:`, error); + } + } + } + } + + return settleResult; + } catch (error) { + const failureContext: SettleFailureContext = { + ...context, + error: error as Error, + }; + + // Execute onSettleFailure hooks + for (const hook of this.onSettleFailureHooks) { + const result = await hook(failureContext); + if (result && "recovered" in result && result.recovered) { + return result.result; + } + } + + throw error; + } + } + + /** + * Find matching payment requirements for a payment + * + * @param availableRequirements - Array of available payment requirements + * @param paymentPayload - The payment payload + * @returns Matching payment requirements or undefined + */ + findMatchingRequirements( + availableRequirements: PaymentRequirements[], + paymentPayload: PaymentPayload, + ): PaymentRequirements | undefined { + switch (paymentPayload.x402Version) { + case 2: + // For v2, match by accepted requirements + return availableRequirements.find(paymentRequirements => + deepEqual(paymentRequirements, paymentPayload.accepted), + ); + case 1: + // For v1, match by scheme and network + return availableRequirements.find( + req => + req.scheme === paymentPayload.accepted.scheme && + req.network === paymentPayload.accepted.network, + ); + default: + throw new Error( + `Unsupported x402 version: ${(paymentPayload as PaymentPayload).x402Version}`, + ); + } + } + + /** + * Process a payment request + * + * @param paymentPayload - Optional payment payload if provided + * @param resourceConfig - Configuration for the protected resource + * @param resourceInfo - Information about the resource being accessed + * @param extensions - Optional extensions to include in the response + * @returns Processing result + */ + async processPaymentRequest( + paymentPayload: PaymentPayload | null, + resourceConfig: ResourceConfig, + resourceInfo: ResourceInfo, + extensions?: Record, + ): Promise<{ + success: boolean; + requiresPayment?: PaymentRequired; + verificationResult?: VerifyResponse; + settlementResult?: SettleResponse; + error?: string; + }> { + const requirements = await this.buildPaymentRequirements(resourceConfig); + + if (!paymentPayload) { + return { + success: false, + requiresPayment: await this.createPaymentRequiredResponse( + requirements, + resourceInfo, + "Payment required", + extensions, + ), + }; + } + + // Find matching requirements + const matchingRequirements = this.findMatchingRequirements(requirements, paymentPayload); + if (!matchingRequirements) { + return { + success: false, + requiresPayment: await this.createPaymentRequiredResponse( + requirements, + resourceInfo, + "No matching payment requirements found", + extensions, + ), + }; + } + + // Verify payment + const verificationResult = await this.verifyPayment(paymentPayload, matchingRequirements); + if (!verificationResult.isValid) { + return { + success: false, + error: verificationResult.invalidReason, + verificationResult, + }; + } + + // Payment verified, ready for settlement + return { + success: true, + verificationResult, + }; + } + + /** + * Get facilitator client for a specific version, network, and scheme + * + * @param x402Version - The x402 version + * @param network - The network identifier + * @param scheme - The payment scheme + * @returns The facilitator client or undefined if not found + */ + private getFacilitatorClient( + x402Version: number, + network: Network, + scheme: string, + ): FacilitatorClient | undefined { + const versionMap = this.facilitatorClientsMap.get(x402Version); + if (!versionMap) return undefined; + + // Use findByNetworkAndScheme for pattern matching + return findByNetworkAndScheme(versionMap, scheme, network); + } +} + +export default x402ResourceServer; diff --git a/typescript/packages/core/src/types/extensions.ts b/typescript/packages/core/src/types/extensions.ts new file mode 100644 index 0000000..57da33a --- /dev/null +++ b/typescript/packages/core/src/types/extensions.ts @@ -0,0 +1,40 @@ +import type { PaymentRequiredContext, SettleResultContext } from "../server/x402ResourceServer"; + +// Re-export context types from x402ResourceServer for convenience +export type { PaymentRequiredContext, SettleResultContext }; + +export interface ResourceServerExtension { + key: string; + /** + * Enrich extension declaration with extension-specific data. + * + * @param declaration - Extension declaration from route config + * @param transportContext - Transport-specific context (HTTP, A2A, MCP, etc.) + * @returns Enriched extension declaration + */ + enrichDeclaration?: (declaration: unknown, transportContext: unknown) => unknown; + /** + * Called when generating a 402 PaymentRequired response. + * Return extension data to add to extensions[key], or undefined to skip. + * + * @param declaration - Extension declaration from route config + * @param context - PaymentRequired context containing response and requirements + * @returns Extension data to add to response.extensions[key] + */ + enrichPaymentRequiredResponse?: ( + declaration: unknown, + context: PaymentRequiredContext, + ) => Promise; + /** + * Called after successful payment settlement. + * Return extension data to add to response.extensions[key], or undefined to skip. + * + * @param declaration - Extension declaration from route config + * @param context - Settlement result context containing payment payload, requirements, and result + * @returns Extension data to add to response.extensions[key] + */ + enrichSettlementResponse?: ( + declaration: unknown, + context: SettleResultContext, + ) => Promise; +} diff --git a/typescript/packages/core/src/types/facilitator.ts b/typescript/packages/core/src/types/facilitator.ts new file mode 100644 index 0000000..b37c9e8 --- /dev/null +++ b/typescript/packages/core/src/types/facilitator.ts @@ -0,0 +1,101 @@ +import { PaymentPayload, PaymentRequirements } from "./payments"; +import { Network } from "./"; + +export type VerifyRequest = { + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; +}; + +export type VerifyResponse = { + isValid: boolean; + invalidReason?: string; + invalidMessage?: string; + payer?: string; + extensions?: Record; +}; + +export type SettleRequest = { + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; +}; + +export type SettleResponse = { + success: boolean; + errorReason?: string; + errorMessage?: string; + payer?: string; + transaction: string; + network: Network; + extensions?: Record; +}; + +export type SupportedKind = { + x402Version: number; + scheme: string; + network: Network; + extra?: Record; +}; + +export type SupportedResponse = { + kinds: SupportedKind[]; + extensions: string[]; + signers: Record; // CAIP family pattern → Signer addresses +}; + +/** + * Error thrown when payment verification fails. + */ +export class VerifyError extends Error { + readonly invalidReason?: string; + readonly invalidMessage?: string; + readonly payer?: string; + readonly statusCode: number; + + /** + * Creates a VerifyError from a failed verification response. + * + * @param statusCode - HTTP status code from the facilitator + * @param response - The verify response containing error details + */ + constructor(statusCode: number, response: VerifyResponse) { + const reason = response.invalidReason || "unknown reason"; + const message = response.invalidMessage; + super(message ? `${reason}: ${message}` : reason); + this.name = "VerifyError"; + this.statusCode = statusCode; + this.invalidReason = response.invalidReason; + this.invalidMessage = response.invalidMessage; + this.payer = response.payer; + } +} + +/** + * Error thrown when payment settlement fails. + */ +export class SettleError extends Error { + readonly errorReason?: string; + readonly errorMessage?: string; + readonly payer?: string; + readonly transaction: string; + readonly network: Network; + readonly statusCode: number; + + /** + * Creates a SettleError from a failed settlement response. + * + * @param statusCode - HTTP status code from the facilitator + * @param response - The settle response containing error details + */ + constructor(statusCode: number, response: SettleResponse) { + const reason = response.errorReason || "unknown reason"; + const message = response.errorMessage; + super(message ? `${reason}: ${message}` : reason); + this.name = "SettleError"; + this.statusCode = statusCode; + this.errorReason = response.errorReason; + this.errorMessage = response.errorMessage; + this.payer = response.payer; + this.transaction = response.transaction; + this.network = response.network; + } +} diff --git a/typescript/packages/core/src/types/index.ts b/typescript/packages/core/src/types/index.ts new file mode 100644 index 0000000..d27c45b --- /dev/null +++ b/typescript/packages/core/src/types/index.ts @@ -0,0 +1,37 @@ +export type { + VerifyRequest, + VerifyResponse, + SettleRequest, + SettleResponse, + SupportedResponse, +} from "./facilitator"; +export { VerifyError, SettleError } from "./facilitator"; +export type { + PaymentRequirements, + PaymentPayload, + PaymentRequired, + ResourceInfo, +} from "./payments"; +export type { + SchemeNetworkClient, + SchemeNetworkFacilitator, + SchemeNetworkServer, + MoneyParser, + PaymentPayloadResult, +} from "./mechanisms"; +export type { PaymentRequirementsV1, PaymentRequiredV1, PaymentPayloadV1 } from "./v1"; +export type { + ResourceServerExtension, + PaymentRequiredContext, + SettleResultContext, +} from "./extensions"; + +export type Network = `${string}:${string}`; + +export type Money = string | number; +export type AssetAmount = { + asset: string; + amount: string; + extra?: Record; +}; +export type Price = Money | AssetAmount; diff --git a/typescript/packages/core/src/types/mechanisms.ts b/typescript/packages/core/src/types/mechanisms.ts new file mode 100644 index 0000000..c1838d0 --- /dev/null +++ b/typescript/packages/core/src/types/mechanisms.ts @@ -0,0 +1,141 @@ +import { SettleResponse, VerifyResponse } from "./facilitator"; +import { PaymentRequirements } from "./payments"; +import { PaymentPayload } from "./payments"; +import { Price, Network, AssetAmount } from "."; + +/** + * Money parser function that converts a numeric amount to an AssetAmount + * Receives the amount as a decimal number (e.g., 1.50 for $1.50) + * Returns null to indicate "cannot handle this amount", causing fallback to next parser + * Always returns a Promise for consistency - use async/await + * + * @param amount - The decimal amount (e.g., 1.50) + * @param network - The network identifier for context + * @returns AssetAmount or null to try next parser + */ +export type MoneyParser = (amount: number, network: Network) => Promise; + +/** + * Result of createPaymentPayload - the core payload fields. + * Contains the x402 version and the scheme-specific payload data. + */ +export type PaymentPayloadResult = Pick; + +export interface SchemeNetworkClient { + readonly scheme: string; + + createPaymentPayload( + x402Version: number, + paymentRequirements: PaymentRequirements, + ): Promise; +} + +export interface SchemeNetworkFacilitator { + readonly scheme: string; + + /** + * CAIP family pattern that this facilitator supports. + * Used to group signers by blockchain family in the supported response. + * + * @example + * // EVM facilitators + * readonly caipFamily = "eip155:*"; + * + * @example + * // SVM facilitators + * readonly caipFamily = "solana:*"; + */ + readonly caipFamily: string; + + /** + * Get mechanism-specific extra data needed for the supported kinds endpoint. + * This method is called when building the facilitator's supported response. + * + * @param network - The network identifier for context + * @returns Extra data object or undefined if no extra data is needed + * + * @example + * // EVM schemes return undefined (no extra data needed) + * getExtra(network: Network): undefined { + * return undefined; + * } + * + * @example + * // SVM schemes return feePayer address + * getExtra(network: Network): Record | undefined { + * return { feePayer: this.signer.address }; + * } + */ + getExtra(network: Network): Record | undefined; + + /** + * Get signer addresses used by this facilitator for a given network. + * These are included in the supported response to help clients understand + * which addresses might sign/pay for transactions. + * + * Supports multiple addresses for load balancing, key rotation, and high availability. + * + * @param network - The network identifier + * @returns Array of signer addresses (wallet addresses, fee payer addresses, etc.) + * + * @example + * // EVM facilitator + * getSigners(network: string): string[] { + * return [...this.signer.getAddresses()]; + * } + * + * @example + * // SVM facilitator + * getSigners(network: string): string[] { + * return [...this.signer.getAddresses()]; + * } + */ + getSigners(network: string): string[]; + + verify(payload: PaymentPayload, requirements: PaymentRequirements): Promise; + settle(payload: PaymentPayload, requirements: PaymentRequirements): Promise; +} + +export interface SchemeNetworkServer { + readonly scheme: string; + + /** + * Convert a user-friendly price to the scheme's specific amount and asset format + * Always returns a Promise for consistency + * + * @param price - User-friendly price (e.g., "$0.10", "0.10", { amount: "100000", asset: "USDC" }) + * @param network - The network identifier for context + * @returns Promise that resolves to the converted amount, asset identifier, and any extra metadata + * + * @example + * // For EVM networks with USDC: + * await parsePrice("$0.10", "eip155:8453") => { amount: "100000", asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" } + * + * // For custom schemes: + * await parsePrice("10 points", "custom:network") => { amount: "10", asset: "points" } + */ + parsePrice(price: Price, network: Network): Promise; + + /** + * Build payment requirements for this scheme/network combination + * + * @param paymentRequirements - Base payment requirements with amount/asset already set + * @param supportedKind - The supported kind from facilitator's /supported endpoint + * @param supportedKind.x402Version - The x402 version + * @param supportedKind.scheme - The payment scheme + * @param supportedKind.network - The network identifier + * @param supportedKind.extra - Optional extra metadata + * @param facilitatorExtensions - Extensions supported by the facilitator + * @returns Enhanced payment requirements ready to be sent to clients + */ + enhancePaymentRequirements( + paymentRequirements: PaymentRequirements, + supportedKind: { + x402Version: number; + scheme: string; + network: Network; + extra?: Record; + }, + facilitatorExtensions: string[], + ): Promise; +} diff --git a/typescript/packages/core/src/types/payments.ts b/typescript/packages/core/src/types/payments.ts new file mode 100644 index 0000000..c4b9e17 --- /dev/null +++ b/typescript/packages/core/src/types/payments.ts @@ -0,0 +1,33 @@ +import { Network } from "./"; + +export interface ResourceInfo { + url: string; + description: string; + mimeType: string; +} + +export type PaymentRequirements = { + scheme: string; + network: Network; + asset: string; + amount: string; + payTo: string; + maxTimeoutSeconds: number; + extra: Record; +}; + +export type PaymentRequired = { + x402Version: number; + error?: string; + resource: ResourceInfo; + accepts: PaymentRequirements[]; + extensions?: Record; +}; + +export type PaymentPayload = { + x402Version: number; + resource: ResourceInfo; + accepted: PaymentRequirements; + payload: Record; + extensions?: Record; +}; diff --git a/typescript/packages/core/src/types/v1/index.ts b/typescript/packages/core/src/types/v1/index.ts new file mode 100644 index 0000000..da67499 --- /dev/null +++ b/typescript/packages/core/src/types/v1/index.ts @@ -0,0 +1,59 @@ +import { Network } from "../"; + +// Payments +export type PaymentRequirementsV1 = { + scheme: string; + network: Network; + maxAmountRequired: string; + resource: string; + description: string; + mimeType: string; + outputSchema: Record; + payTo: string; + maxTimeoutSeconds: number; + asset: string; + extra: Record; +}; + +export type PaymentRequiredV1 = { + x402Version: 1; + error?: string; + accepts: PaymentRequirementsV1[]; +}; + +export type PaymentPayloadV1 = { + x402Version: 1; + scheme: string; + network: Network; + payload: Record; +}; + +// Facilitator Requests/Responses +export type VerifyRequestV1 = { + paymentPayload: PaymentPayloadV1; + paymentRequirements: PaymentRequirementsV1; +}; + +export type SettleRequestV1 = { + paymentPayload: PaymentPayloadV1; + paymentRequirements: PaymentRequirementsV1; +}; + +export type SettleResponseV1 = { + success: boolean; + errorReason?: string; + errorMessage?: string; + payer?: string; + transaction: string; + network: Network; +}; + +export type SupportedResponseV1 = { + kinds: { + x402Version: number; + scheme: string; + network: Network; + extra?: Record; + }[]; + // NO extensions field - V1 doesn't support extensions +}; diff --git a/typescript/packages/core/src/utils/index.ts b/typescript/packages/core/src/utils/index.ts new file mode 100644 index 0000000..9cfbce4 --- /dev/null +++ b/typescript/packages/core/src/utils/index.ts @@ -0,0 +1,158 @@ +import { Network } from "../types"; + +/** + * Scheme data structure for facilitator storage + */ +export interface SchemeData { + facilitator: T; + networks: Set; + pattern: Network; +} + +export const findSchemesByNetwork = ( + map: Map>, + network: Network, +): Map | undefined => { + // Direct match first + let implementationsByScheme = map.get(network); + + if (!implementationsByScheme) { + // Try pattern matching for registered network patterns + for (const [registeredNetworkPattern, implementations] of map.entries()) { + // Convert the registered network pattern to a regex + // e.g., "eip155:*" becomes /^eip155:.*$/ + const pattern = registeredNetworkPattern + .replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // Escape special regex chars except * + .replace(/\\\*/g, ".*"); // Replace escaped * with .* + + const regex = new RegExp(`^${pattern}$`); + + if (regex.test(network)) { + implementationsByScheme = implementations; + break; + } + } + } + + return implementationsByScheme; +}; + +export const findByNetworkAndScheme = ( + map: Map>, + scheme: string, + network: Network, +): T | undefined => { + return findSchemesByNetwork(map, network)?.get(scheme); +}; + +/** + * Finds a facilitator by scheme and network using pattern matching. + * Works with new SchemeData storage structure. + * + * @param schemeMap - Map of scheme names to SchemeData + * @param scheme - The scheme to find + * @param network - The network to match against + * @returns The facilitator if found, undefined otherwise + */ +export const findFacilitatorBySchemeAndNetwork = ( + schemeMap: Map>, + scheme: string, + network: Network, +): T | undefined => { + const schemeData = schemeMap.get(scheme); + if (!schemeData) return undefined; + + // Check if network is in the stored networks set + if (schemeData.networks.has(network)) { + return schemeData.facilitator; + } + + // Try pattern matching + const patternRegex = new RegExp("^" + schemeData.pattern.replace("*", ".*") + "$"); + if (patternRegex.test(network)) { + return schemeData.facilitator; + } + + return undefined; +}; + +export const Base64EncodedRegex = /^[A-Za-z0-9+/]*={0,2}$/; + +/** + * Encodes a string to base64 format + * + * @param data - The string to be encoded to base64 + * @returns The base64 encoded string + */ +export function safeBase64Encode(data: string): string { + if (typeof globalThis !== "undefined" && typeof globalThis.btoa === "function") { + const bytes = new TextEncoder().encode(data); + const binaryString = Array.from(bytes, byte => String.fromCharCode(byte)).join(""); + return globalThis.btoa(binaryString); + } + return Buffer.from(data, "utf8").toString("base64"); +} + +/** + * Decodes a base64 string back to its original format + * + * @param data - The base64 encoded string to be decoded + * @returns The decoded string in UTF-8 format + */ +export function safeBase64Decode(data: string): string { + if (typeof globalThis !== "undefined" && typeof globalThis.atob === "function") { + const binaryString = globalThis.atob(data); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + const decoder = new TextDecoder("utf-8"); + return decoder.decode(bytes); + } + return Buffer.from(data, "base64").toString("utf-8"); +} + +/** + * Deep equality comparison for payment requirements + * Uses a normalized JSON.stringify for consistent comparison + * + * @param obj1 - First object to compare + * @param obj2 - Second object to compare + * @returns True if objects are deeply equal + */ +export function deepEqual(obj1: unknown, obj2: unknown): boolean { + // Normalize and stringify both objects for comparison + // This handles nested objects, arrays, and different property orders + const normalize = (obj: unknown): string => { + // Handle primitives and null/undefined + if (obj === null || obj === undefined) return JSON.stringify(obj); + if (typeof obj !== "object") return JSON.stringify(obj); + + // Handle arrays + if (Array.isArray(obj)) { + return JSON.stringify( + obj.map(item => + typeof item === "object" && item !== null ? JSON.parse(normalize(item)) : item, + ), + ); + } + + // Handle objects - sort keys and recursively normalize values + const sorted: Record = {}; + Object.keys(obj as Record) + .sort() + .forEach(key => { + const value = (obj as Record)[key]; + sorted[key] = + typeof value === "object" && value !== null ? JSON.parse(normalize(value)) : value; + }); + return JSON.stringify(sorted); + }; + + try { + return normalize(obj1) === normalize(obj2); + } catch { + // Fallback to simple comparison if normalization fails + return JSON.stringify(obj1) === JSON.stringify(obj2); + } +} diff --git a/typescript/packages/core/test/integrations/core.test.ts b/typescript/packages/core/test/integrations/core.test.ts new file mode 100644 index 0000000..7e9185b --- /dev/null +++ b/typescript/packages/core/test/integrations/core.test.ts @@ -0,0 +1,181 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { x402Client, x402HTTPClient } from "../../src/client"; +import { x402Facilitator } from "../../src/facilitator"; +import { + HTTPAdapter, + HTTPResponseInstructions, + x402HTTPResourceServer, + x402ResourceServer, +} from "../../src/server"; +import { + buildCashPaymentRequirements, + CashFacilitatorClient, + CashSchemeNetworkClient, + CashSchemeNetworkFacilitator, + CashSchemeNetworkServer, +} from "../mocks"; +import { Network, PaymentPayload, PaymentRequirements } from "../../src/types"; + +describe("Core Integration Tests", () => { + describe("x402Client / x402ResourceServer / x402Facilitator - Cash Flow", () => { + let client: x402Client; + let server: x402ResourceServer; + + beforeEach(async () => { + client = new x402Client().register("x402:cash", new CashSchemeNetworkClient("John")); + + const facilitator = new x402Facilitator().register( + "x402:cash", + new CashSchemeNetworkFacilitator(), + ); + + const facilitatorClient = new CashFacilitatorClient(facilitator); + server = new x402ResourceServer(facilitatorClient); + server.register("x402:cash", new CashSchemeNetworkServer()); + await server.initialize(); // Initialize to fetch supported kinds + }); + + it("server should successfully verify and settle a cash payment from a client", async () => { + // Server - builds PaymentRequired response + const accepts = [buildCashPaymentRequirements("Company Co.", "USD", "1")]; + const resource = { + url: "https://company.co", + description: "Company Co. resource", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + // Client - responds with PaymentPayload response + const paymentPayload = await client.createPaymentPayload(paymentRequired); + + // Server - maps payment payload to payment requirements + const accepted = server.findMatchingRequirements(accepts, paymentPayload); + expect(accepted).toBeDefined(); + + const verifyResponse = await server.verifyPayment(paymentPayload, accepted!); + expect(verifyResponse.isValid).toBe(true); + + // Server does work here + + const settleResponse = await server.settlePayment(paymentPayload, accepted!); + expect(settleResponse.success).toBe(true); + }); + }); + + describe("x402HTTPClient / x402HTTPResourceServer / x402Facilitator - Cash Flow", () => { + let client: x402HTTPClient; + let httpServer: x402HTTPResourceServer; + + const routes = { + "/api/protected": { + accepts: { + scheme: "cash", + payTo: "merchant@example.com", + price: "$0.10", + network: "x402:cash" as Network, + }, + description: "Access to protected API", + mimeType: "application/json", + }, + }; + + const mockAdapter: HTTPAdapter = { + getHeader: (name: string) => { + // Return payment header if requested + if (name === "x-payment") { + return "base64EncodedPaymentHere"; + } + return undefined; + }, + getMethod: () => "GET", + getPath: () => "/api/protected", + getUrl: () => "https://example.com/api/protected", + getAcceptHeader: () => "application/json", + getUserAgent: () => "TestClient/1.0", + }; + + beforeEach(async () => { + const facilitator = new x402Facilitator().register( + "x402:cash", + new CashSchemeNetworkFacilitator(), + ); + + const facilitatorClient = new CashFacilitatorClient(facilitator); + + const paymentClient = new x402Client().register( + "x402:cash", + new CashSchemeNetworkClient("John"), + ); + client = new x402HTTPClient(paymentClient) as x402HTTPClient; + + // Create resource server and register schemes + const ResourceServer = new x402ResourceServer(facilitatorClient); + ResourceServer.register("x402:cash", new CashSchemeNetworkServer()); + await ResourceServer.initialize(); // Initialize to fetch supported kinds + + // Create HTTP server with the resource server + httpServer = new x402HTTPResourceServer(ResourceServer, routes); + }); + + it("middleware should successfully verify and settle a cash payment from an http client", async () => { + // Middleware creates a PaymentRequired response + const context = { + adapter: mockAdapter, + path: "/api/protected", + method: "GET", + }; + // No payment made, get PaymentRequired response & header + const httpProcessResult = (await httpServer.processHTTPRequest(context))!; + + expect(httpProcessResult.type).toBe("payment-error"); + + const initial402Response = ( + httpProcessResult as { type: "payment-error"; response: HTTPResponseInstructions } + ).response; + + expect(initial402Response).toBeDefined(); + expect(initial402Response.status).toBe(402); + expect(initial402Response.headers).toBeDefined(); + expect(initial402Response.headers["PAYMENT-REQUIRED"]).toBeDefined(); + expect(initial402Response.isHtml).toBeFalsy(); + expect(initial402Response.body).toEqual({}); + + // Client responds to PaymentRequired and submits a request with a PaymentPayload + const paymentRequired = client.getPaymentRequiredResponse( + name => initial402Response.headers[name], + initial402Response.body, + ); + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const requestHeaders = await client.encodePaymentSignatureHeader(paymentPayload); + + // Middleware handles PAYMENT-SIGNATURE request + context.adapter.getHeader = (name: string) => { + if (name === "PAYMENT-SIGNATURE") { + return requestHeaders["PAYMENT-SIGNATURE"]; + } + return undefined; + }; + const httpProcessResult2 = await httpServer.processHTTPRequest(context); + + // No need to reason respond, can continue with request + expect(httpProcessResult2.type).toBe("payment-verified"); + const { + paymentPayload: verifiedPaymentPayload, + paymentRequirements: verifiedPaymentRequirements, + } = httpProcessResult2 as { + type: "payment-verified"; + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + }; + + const settlementResult = await httpServer.processSettlement( + verifiedPaymentPayload, + verifiedPaymentRequirements, + ); + expect(settlementResult.success).toBe(true); + if (settlementResult.success) { + expect(settlementResult.headers["PAYMENT-RESPONSE"]).toBeDefined(); + } + }); + }); +}); diff --git a/typescript/packages/core/test/integrations/dynamic.test.ts b/typescript/packages/core/test/integrations/dynamic.test.ts new file mode 100644 index 0000000..7e8f10a --- /dev/null +++ b/typescript/packages/core/test/integrations/dynamic.test.ts @@ -0,0 +1,1233 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { + x402HTTPResourceServer, + HTTPAdapter, + HTTPRequestContext, +} from "../../src/http/x402HTTPResourceServer"; +import { x402ResourceServer } from "../../src/server/x402ResourceServer"; +import { decodePaymentRequiredHeader } from "../../src/http"; +import { CashFacilitatorClient, CashSchemeNetworkServer } from "../mocks"; +import { x402Facilitator } from "../../src/facilitator"; +import { CashSchemeNetworkFacilitator } from "../mocks/cash"; +import { Network, Price } from "../../src/types"; + +/** + * Enhanced HTTP Adapter that simulates a real HTTP framework + * with query params, body parsing, and custom headers + */ +class MockHTTPAdapter implements HTTPAdapter { + private headers: Record; + private _queryParams: Record; + private _body: Record; + private _path: string; + private _method: string; + + /** + * + * @param config + * @param config.path + * @param config.method + * @param config.headers + * @param config.queryParams + * @param config.body + */ + constructor(config: { + path: string; + method: string; + headers?: Record; + queryParams?: Record; + body?: Record; + }) { + this._path = config.path; + this._method = config.method; + this.headers = config.headers || {}; + this._queryParams = config.queryParams || {}; + this._body = config.body || {}; + } + + /** + * + * @param name + */ + getHeader(name: string): string | undefined { + return this.headers[name.toLowerCase()]; + } + + /** + * + */ + getMethod(): string { + return this._method; + } + + /** + * + */ + getPath(): string { + return this._path; + } + + /** + * + */ + getUrl(): string { + const queryString = + Object.keys(this._queryParams).length > 0 + ? "?" + + Object.entries(this._queryParams) + .map(([k, v]) => `${k}=${v}`) + .join("&") + : ""; + return `https://example.com${this._path}${queryString}`; + } + + /** + * + */ + getAcceptHeader(): string { + return this.headers["accept"] || "application/json"; + } + + /** + * + */ + getUserAgent(): string { + return this.headers["user-agent"] || "TestClient/1.0"; + } + + // Implement optional HTTPAdapter methods + /** + * + */ + getQueryParams(): Record { + return this._queryParams; + } + + /** + * + * @param name + */ + getQueryParam(name: string): string | string[] | undefined { + return this._queryParams[name]; + } + + /** + * + */ + getBody(): unknown { + return this._body; + } +} + +// Extend HTTPRequestContext with helper to access adapter methods +interface ExtendedHTTPRequestContext extends HTTPRequestContext { + adapter: MockHTTPAdapter; +} + +describe("Dynamic Pricing & PayTo Integration Tests", () => { + let ResourceServer: x402ResourceServer; + + beforeEach(async () => { + const facilitator = new x402Facilitator().register( + "x402:cash", + new CashSchemeNetworkFacilitator(), + ); + + const facilitatorClient = new CashFacilitatorClient(facilitator); + ResourceServer = new x402ResourceServer(facilitatorClient); + ResourceServer.register("x402:cash", new CashSchemeNetworkServer()); + await ResourceServer.initialize(); + }); + + describe("Dynamic Pricing - Query Parameters", () => { + it("should extract tier from query params and adjust price", async () => { + const routes = { + "GET /api/data": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + payTo: "merchant@example.com", + price: async (_context: HTTPRequestContext) => { + // Extract tier from query params + const tier = _context.adapter.getQueryParam?.("tier"); + + // Tiered pricing based on query param + if (tier === "premium") return "$0.01" as Price; + if (tier === "business") return "$0.05" as Price; + return "$0.10" as Price; // Default tier + }, + }, + description: "Tiered API access", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + // Test 1: Premium tier + const premiumAdapter = new MockHTTPAdapter({ + path: "/api/data", + method: "GET", + queryParams: { tier: "premium" }, + }); + + const premiumContext: ExtendedHTTPRequestContext = { + adapter: premiumAdapter, + path: "/api/data", + method: "GET", + }; + + const premiumResult = await httpServer.processHTTPRequest(premiumContext); + + expect(premiumResult.type).toBe("payment-error"); // No payment provided + if (premiumResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + premiumResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("0.01"); // Premium price + } + + // Test 2: Business tier + const businessAdapter = new MockHTTPAdapter({ + path: "/api/data", + method: "GET", + queryParams: { tier: "business" }, + }); + + const businessContext: ExtendedHTTPRequestContext = { + adapter: businessAdapter, + path: "/api/data", + method: "GET", + }; + + const businessResult = await httpServer.processHTTPRequest(businessContext); + + if (businessResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + businessResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("0.05"); // Business price + } + + // Test 3: Default tier (no query param) + const defaultAdapter = new MockHTTPAdapter({ + path: "/api/data", + method: "GET", + queryParams: {}, + }); + + const defaultContext: ExtendedHTTPRequestContext = { + adapter: defaultAdapter, + path: "/api/data", + method: "GET", + }; + + const defaultResult = await httpServer.processHTTPRequest(defaultContext); + + if (defaultResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + defaultResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("0.10"); // Default price + } + }); + + it("should use query params for pagination-based pricing", async () => { + const routes = { + "GET /api/items": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + payTo: "merchant@example.com", + price: async (_context: HTTPRequestContext) => { + const limit = parseInt((context.adapter.getQueryParam?.("limit") as string) || "10"); + + // Price scales with requested items + const basePrice = 0.01; + const pricePerItem = 0.001; + const totalPrice = basePrice + limit * pricePerItem; + + return `$${totalPrice.toFixed(3)}` as Price; + }, + }, + description: "Paginated data access", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + // Request 100 items + const adapter = new MockHTTPAdapter({ + path: "/api/items", + method: "GET", + queryParams: { limit: "100" }, + }); + + const context: ExtendedHTTPRequestContext = { + adapter, + path: "/api/items", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + if (result.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + result.response.headers["PAYMENT-REQUIRED"], + ); + // 0.01 + (100 * 0.001) = 0.11 + expect(paymentRequired.accepts[0].amount).toBe("0.110"); + } + }); + }); + + describe("Dynamic Pricing - Request Body", () => { + it("should extract data from request body for pricing", async () => { + const routes = { + "POST /api/compute": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + payTo: "compute@example.com", + price: async (_context: HTTPRequestContext) => { + const body = _context.adapter.getBody?.() as Record | undefined; + + const complexity = (body?.complexity as string) || "low"; + const duration = (body?.estimatedDuration as number) || 1; + + // Price based on computational complexity + let baseRate = 0.1; + if (complexity === "high") baseRate = 1.0; + else if (complexity === "medium") baseRate = 0.5; + + const totalPrice = baseRate * duration; + return `$${totalPrice.toFixed(2)}` as Price; + }, + }, + description: "Compute service", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + // Test 1: High complexity, 5 minute job + const highComplexityAdapter = new MockHTTPAdapter({ + path: "/api/compute", + method: "POST", + body: { + complexity: "high", + estimatedDuration: 5, + task: "render_video", + }, + }); + + const highContext: ExtendedHTTPRequestContext = { + adapter: highComplexityAdapter, + path: "/api/compute", + method: "POST", + }; + + const highResult = await httpServer.processHTTPRequest(highContext); + + if (highResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + highResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("5.00"); // 1.00 * 5 + } + + // Test 2: Low complexity, 2 minute job + const lowComplexityAdapter = new MockHTTPAdapter({ + path: "/api/compute", + method: "POST", + body: { + complexity: "low", + estimatedDuration: 2, + }, + }); + + const lowContext: ExtendedHTTPRequestContext = { + adapter: lowComplexityAdapter, + path: "/api/compute", + method: "POST", + }; + + const lowResult = await httpServer.processHTTPRequest(lowContext); + + if (lowResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + lowResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("0.20"); // 0.10 * 2 + } + }); + + it("should price based on request payload size", async () => { + const routes = { + "POST /api/process": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + payTo: "processor@example.com", + price: async (_context: HTTPRequestContext) => { + const body = _context.adapter.getBody?.() as Record | undefined; + + // Get data array size + const dataArray = (body?.data as unknown[]) || []; + const itemCount = dataArray.length; + + // $0.01 per item, minimum $0.05 + const price = Math.max(0.05, itemCount * 0.01); + return `$${price.toFixed(2)}` as Price; + }, + }, + description: "Batch processing", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter({ + path: "/api/process", + method: "POST", + body: { + data: Array(50).fill({ item: "test" }), // 50 items + }, + }); + + const context: ExtendedHTTPRequestContext = { + adapter, + path: "/api/process", + method: "POST", + }; + + const result = await httpServer.processHTTPRequest(context); + + if (result.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + result.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("0.50"); // 50 * 0.01 + } + }); + }); + + describe("Dynamic Pricing - Headers", () => { + it("should extract API key from headers and apply tier-based pricing", async () => { + // Simulate a user database + const userDatabase: Record = { + key_premium_123: { tier: "premium", rateLimit: 1000 }, + key_standard_456: { tier: "standard", rateLimit: 100 }, + key_free_789: { tier: "free", rateLimit: 10 }, + }; + + const routes = { + "GET /api/resource": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + payTo: "api@example.com", + price: async (_context: HTTPRequestContext) => { + const apiKey = _context.adapter.getHeader("x-api-key"); + + if (!apiKey) { + return "$1.00" as Price; // No API key = highest price + } + + const user = userDatabase[apiKey]; + if (!user) { + return "$1.00" as Price; // Unknown key = highest price + } + + // Tier-based pricing + const tierPrices: Record = { + premium: "$0.01", + standard: "$0.10", + free: "$0.50", + }; + + return (tierPrices[user.tier] as Price) || ("$1.00" as Price); + }, + }, + description: "API with tier-based access", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + // Test 1: Premium user + const premiumAdapter = new MockHTTPAdapter({ + path: "/api/resource", + method: "GET", + headers: { "x-api-key": "key_premium_123" }, + }); + + const premiumResult = await httpServer.processHTTPRequest({ + adapter: premiumAdapter, + path: "/api/resource", + method: "GET", + }); + + if (premiumResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + premiumResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("0.01"); + } + + // Test 2: Standard user + const standardAdapter = new MockHTTPAdapter({ + path: "/api/resource", + method: "GET", + headers: { "x-api-key": "key_standard_456" }, + }); + + const standardResult = await httpServer.processHTTPRequest({ + adapter: standardAdapter, + path: "/api/resource", + method: "GET", + }); + + if (standardResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + standardResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("0.10"); + } + + // Test 3: No API key + const noKeyAdapter = new MockHTTPAdapter({ + path: "/api/resource", + method: "GET", + headers: {}, + }); + + const noKeyResult = await httpServer.processHTTPRequest({ + adapter: noKeyAdapter, + path: "/api/resource", + method: "GET", + }); + + if (noKeyResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + noKeyResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("1.00"); // Highest price + } + }); + + it("should use Content-Length header for size-based pricing", async () => { + const routes = { + "POST /api/upload": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + payTo: "storage@example.com", + price: async (_context: HTTPRequestContext) => { + const contentLength = _context.adapter.getHeader("content-length"); + const sizeInBytes = parseInt(contentLength || "0"); + const sizeInMB = sizeInBytes / (1024 * 1024); + + // $0.10 per MB, minimum $0.05 + const price = Math.max(0.05, sizeInMB * 0.1); + return `$${price.toFixed(2)}` as Price; + }, + }, + description: "File storage", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + // Upload 10MB file + const adapter = new MockHTTPAdapter({ + path: "/api/upload", + method: "POST", + headers: { + "content-length": (10 * 1024 * 1024).toString(), // 10MB + "content-type": "application/octet-stream", + }, + }); + + const context: ExtendedHTTPRequestContext = { + adapter, + path: "/api/upload", + method: "POST", + }; + + const result = await httpServer.processHTTPRequest(context); + + if (result.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + result.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("1.00"); // 10 * 0.10 + } + }); + + it("should use custom headers for feature flags", async () => { + const routes = { + "GET /api/advanced": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + payTo: "service@example.com", + price: async (_context: HTTPRequestContext) => { + const enableAI = _context.adapter.getHeader("x-enable-ai"); + const enableCache = _context.adapter.getHeader("x-enable-cache"); + + let price = 0.1; // Base price + + if (enableAI === "true") price += 0.5; // AI features cost extra + if (enableCache === "false") price += 0.2; // No cache = more compute + + return `$${price.toFixed(2)}` as Price; + }, + }, + description: "Advanced API with optional features", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + // With AI, with cache + const aiCacheAdapter = new MockHTTPAdapter({ + path: "/api/advanced", + method: "GET", + headers: { + "x-enable-ai": "true", + "x-enable-cache": "true", + }, + }); + + const aiCacheResult = await httpServer.processHTTPRequest({ + adapter: aiCacheAdapter, + path: "/api/advanced", + method: "GET", + }); + + if (aiCacheResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + aiCacheResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("0.60"); // 0.10 + 0.50 + } + + // No AI, no cache + const basicAdapter = new MockHTTPAdapter({ + path: "/api/advanced", + method: "GET", + headers: { + "x-enable-ai": "false", + "x-enable-cache": "false", + }, + }); + + const basicResult = await httpServer.processHTTPRequest({ + adapter: basicAdapter, + path: "/api/advanced", + method: "GET", + }); + + if (basicResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + basicResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("0.30"); // 0.10 + 0.20 + } + }); + }); + + describe("Dynamic PayTo - Headers", () => { + it("should route payment to different addresses based on header", async () => { + const routes = { + "POST /api/process": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + price: "$0.50" as Price, + payTo: async (context: HTTPRequestContext) => { + const region = context.adapter.getHeader("x-region"); + + // Route to different payment addresses by region + const paymentAddresses: Record = { + us: "merchant-us@example.com", + eu: "merchant-eu@example.com", + asia: "merchant-asia@example.com", + }; + + return paymentAddresses[region || "us"] || "merchant-default@example.com"; + }, + }, + description: "Regional payment routing", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + // Test US region + const usAdapter = new MockHTTPAdapter({ + path: "/api/process", + method: "POST", + headers: { "x-region": "us" }, + }); + + const usResult = await httpServer.processHTTPRequest({ + adapter: usAdapter, + path: "/api/process", + method: "POST", + }); + + if (usResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + usResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].payTo).toBe("merchant-us@example.com"); + } + + // Test EU region + const euAdapter = new MockHTTPAdapter({ + path: "/api/process", + method: "POST", + headers: { "x-region": "eu" }, + }); + + const euResult = await httpServer.processHTTPRequest({ + adapter: euAdapter, + path: "/api/process", + method: "POST", + }); + + if (euResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + euResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].payTo).toBe("merchant-eu@example.com"); + } + }); + + it("should use Authorization header to determine payment recipient", async () => { + const routes = { + "GET /api/user-content": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + price: "$0.25" as Price, + payTo: async (context: HTTPRequestContext) => { + const authHeader = context.adapter.getHeader("authorization"); + + if (!authHeader) { + return "anonymous@example.com"; + } + + // Parse Bearer token (simplified) + const token = authHeader.replace("Bearer ", ""); + + // Simulate decoding user ID from token + if (token.includes("creator")) { + return "creator-123@example.com"; + } + if (token.includes("consumer")) { + return "platform@example.com"; + } + + return "default@example.com"; + }, + }, + description: "User-generated content access", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + // Creator content - pay creator directly + const creatorAdapter = new MockHTTPAdapter({ + path: "/api/user-content", + method: "GET", + headers: { authorization: "Bearer creator_token_abc" }, + }); + + const creatorResult = await httpServer.processHTTPRequest({ + adapter: creatorAdapter, + path: "/api/user-content", + method: "GET", + }); + + if (creatorResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + creatorResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].payTo).toBe("creator-123@example.com"); + } + + // Consumer - pay platform + const consumerAdapter = new MockHTTPAdapter({ + path: "/api/user-content", + method: "GET", + headers: { authorization: "Bearer consumer_token_xyz" }, + }); + + const consumerResult = await httpServer.processHTTPRequest({ + adapter: consumerAdapter, + path: "/api/user-content", + method: "GET", + }); + + if (consumerResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + consumerResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].payTo).toBe("platform@example.com"); + } + }); + }); + + describe("Dynamic PayTo - Request Body", () => { + it("should route to different service providers based on request", async () => { + const routes = { + "POST /api/inference": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + price: "$1.00" as Price, + payTo: async (context: HTTPRequestContext) => { + const body = context.adapter.getBody?.() as Record | undefined; + + const modelId = body?.modelId as string; + + // Route to different GPU providers based on model + const modelProviders: Record = { + "gpt-4": "openai-provider@example.com", + "claude-3": "anthropic-provider@example.com", + "llama-3": "meta-provider@example.com", + }; + + return modelProviders[modelId] || "default-provider@example.com"; + }, + }, + description: "AI model inference routing", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + // GPT-4 request + const gptAdapter = new MockHTTPAdapter({ + path: "/api/inference", + method: "POST", + body: { + modelId: "gpt-4", + prompt: "Hello, world!", + }, + }); + + const gptResult = await httpServer.processHTTPRequest({ + adapter: gptAdapter, + path: "/api/inference", + method: "POST", + }); + + if (gptResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + gptResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].payTo).toBe("openai-provider@example.com"); + } + + // Claude request + const claudeAdapter = new MockHTTPAdapter({ + path: "/api/inference", + method: "POST", + body: { + modelId: "claude-3", + prompt: "Hello, world!", + }, + }); + + const claudeResult = await httpServer.processHTTPRequest({ + adapter: claudeAdapter, + path: "/api/inference", + method: "POST", + }); + + if (claudeResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + claudeResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].payTo).toBe("anthropic-provider@example.com"); + } + }); + }); + + describe("Combined Dynamic Pricing & PayTo", () => { + it("should use both query params and headers for complex routing", async () => { + const routes = { + "GET /api/premium-data": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + payTo: async (context: HTTPRequestContext) => { + // Route based on data source (query param) + const source = context.adapter.getQueryParam?.("source") as string | undefined; + + if (source === "blockchain") return "blockchain-provider@example.com"; + if (source === "market") return "market-data-provider@example.com"; + return "default-provider@example.com"; + }, + price: async (_context: HTTPRequestContext) => { + // Price based on subscription level (header) and data range (query) + const subscription = _context.adapter.getHeader("x-subscription"); + const range = (_context.adapter.getQueryParam?.("range") as string) || "1d"; + + let basePrice = subscription === "pro" ? 0.1 : 0.5; + + // Price multiplier based on data range + const rangeMultipliers: Record = { + "1d": 1, + "7d": 3, + "30d": 10, + "1y": 50, + }; + + const multiplier = rangeMultipliers[range] || 1; + const finalPrice = basePrice * multiplier; + + return `$${finalPrice.toFixed(2)}` as Price; + }, + }, + description: "Premium data API with complex pricing", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + // Pro subscription, 30-day data, blockchain source + const adapter = new MockHTTPAdapter({ + path: "/api/premium-data", + method: "GET", + headers: { "x-subscription": "pro" }, + queryParams: { + source: "blockchain", + range: "30d", + }, + }); + + const result = await httpServer.processHTTPRequest({ + adapter, + path: "/api/premium-data", + method: "GET", + }); + + if (result.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + result.response.headers["PAYMENT-REQUIRED"], + ); + + // Verify dynamic payTo + expect(paymentRequired.accepts[0].payTo).toBe("blockchain-provider@example.com"); + + // Verify dynamic price: 0.10 * 10 = 1.00 + expect(paymentRequired.accepts[0].amount).toBe("1.00"); + } + + // Free subscription, 7-day data, market source + const freeAdapter = new MockHTTPAdapter({ + path: "/api/premium-data", + method: "GET", + headers: { "x-subscription": "free" }, + queryParams: { + source: "market", + range: "7d", + }, + }); + + const freeResult = await httpServer.processHTTPRequest({ + adapter: freeAdapter, + path: "/api/premium-data", + method: "GET", + }); + + if (freeResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + freeResult.response.headers["PAYMENT-REQUIRED"], + ); + + // Verify dynamic payTo + expect(paymentRequired.accepts[0].payTo).toBe("market-data-provider@example.com"); + + // Verify dynamic price: 0.50 * 3 = 1.50 + expect(paymentRequired.accepts[0].amount).toBe("1.50"); + } + }); + }); + + describe("Real-world Middleware Scenarios", () => { + it("should support rate-limiting based pricing", async () => { + // Simulate rate limit tracking + const requestCounts: Record = {}; + + const routes = { + "GET /api/data": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + payTo: "api@example.com", + price: async (_context: HTTPRequestContext) => { + const apiKey = _context.adapter.getHeader("x-api-key") || "anonymous"; + + // Track requests + requestCounts[apiKey] = (requestCounts[apiKey] || 0) + 1; + + const requestCount = requestCounts[apiKey]; + + // First 100 requests: cheap + if (requestCount <= 100) return "$0.01" as Price; + // Next 900 requests: medium + if (requestCount <= 1000) return "$0.05" as Price; + // Over 1000: expensive (encourage upgrade) + return "$0.50" as Price; + }, + }, + description: "Rate-limited API", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter({ + path: "/api/data", + method: "GET", + headers: { "x-api-key": "test-key-123" }, + }); + + // First request + const result1 = await httpServer.processHTTPRequest({ + adapter, + path: "/api/data", + method: "GET", + }); + + if (result1.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + result1.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("0.01"); + } + + // Simulate 100 more requests + requestCounts["test-key-123"] = 101; + + const result101 = await httpServer.processHTTPRequest({ + adapter, + path: "/api/data", + method: "GET", + }); + + if (result101.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + result101.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("0.05"); + } + }); + + it("should support load-balancing across payment addresses", async () => { + // Simulate load balancer state + let currentServerIndex = 0; + const servers = [ + { address: "server1@example.com", load: 0 }, + { address: "server2@example.com", load: 0 }, + { address: "server3@example.com", load: 0 }, + ]; + + const routes = { + "POST /api/task": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + price: "$0.10" as Price, + payTo: async (_context: HTTPRequestContext) => { + // Round-robin load balancing + const server = servers[currentServerIndex]; + currentServerIndex = (currentServerIndex + 1) % servers.length; + server.load++; + + return server.address; + }, + }, + description: "Load-balanced task processing", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter({ + path: "/api/task", + method: "POST", + }); + + // Make 3 requests + const results = []; + for (let i = 0; i < 3; i++) { + const result = await httpServer.processHTTPRequest({ + adapter, + path: "/api/task", + method: "POST", + }); + results.push(result); + } + + // Should have routed to all 3 servers + const payTos = results.map(r => { + if (r.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + r.response.headers["PAYMENT-REQUIRED"], + ); + return paymentRequired.accepts[0].payTo; + } + return null; + }); + + expect(payTos).toEqual(["server1@example.com", "server2@example.com", "server3@example.com"]); + + expect(servers[0].load).toBe(1); + expect(servers[1].load).toBe(1); + expect(servers[2].load).toBe(1); + }); + }); + + describe("Time-based Dynamic Pricing", () => { + it("should apply surge pricing during peak hours", async () => { + const routes = { + "GET /api/resource": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + payTo: "service@example.com", + price: async (_context: HTTPRequestContext) => { + // Check if client provides a timestamp header (for testing) + const timestampHeader = _context.adapter.getHeader("x-test-time"); + const hour = timestampHeader + ? new Date(parseInt(timestampHeader)).getHours() + : new Date().getHours(); + + // Peak hours (9 AM - 5 PM): surge pricing + const isPeakHour = hour >= 9 && hour < 17; + + return isPeakHour ? ("$0.20" as Price) : ("$0.10" as Price); + }, + }, + description: "Surge pricing API", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + // Peak hour - use local time noon (12 PM) + const peakDate = new Date(); + peakDate.setHours(12, 0, 0, 0); // Set to noon local time + + const peakAdapter = new MockHTTPAdapter({ + path: "/api/resource", + method: "GET", + headers: { + "x-test-time": peakDate.getTime().toString(), + }, + }); + + const peakResult = await httpServer.processHTTPRequest({ + adapter: peakAdapter, + path: "/api/resource", + method: "GET", + }); + + if (peakResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + peakResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("0.20"); // Surge price + } + + // Off-peak hour - use local time 10 PM (22:00) + const offPeakDate = new Date(); + offPeakDate.setHours(22, 0, 0, 0); // Set to 10 PM local time + + const offPeakAdapter = new MockHTTPAdapter({ + path: "/api/resource", + method: "GET", + headers: { + "x-test-time": offPeakDate.getTime().toString(), + }, + }); + + const offPeakResult = await httpServer.processHTTPRequest({ + adapter: offPeakAdapter, + path: "/api/resource", + method: "GET", + }); + + if (offPeakResult.type === "payment-error") { + const paymentRequired = decodePaymentRequiredHeader( + offPeakResult.response.headers["PAYMENT-REQUIRED"], + ); + expect(paymentRequired.accepts[0].amount).toBe("0.10"); // Normal price + } + }); + }); + + describe("Access to Full Request Context", () => { + it("should have access to all context properties in dynamic functions", async () => { + let capturedContext: HTTPRequestContext | null = null; + + const routes = { + "POST /api/test": { + accepts: { + scheme: "cash", + network: "x402:cash" as Network, + payTo: async (context: HTTPRequestContext) => { + capturedContext = context; + return "test@example.com"; + }, + price: "$1.00" as Price, + }, + description: "Context capture test", + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter({ + path: "/api/test", + method: "POST", + headers: { + "x-custom-header": "custom-value", + authorization: "Bearer token123", + }, + queryParams: { + param1: "value1", + }, + body: { + data: "test", + }, + }); + + const context: ExtendedHTTPRequestContext = { + adapter, + path: "/api/test", + method: "POST", + }; + + await httpServer.processHTTPRequest(context); + + // Verify context was captured + expect(capturedContext).toBeDefined(); + expect(capturedContext?.path).toBe("/api/test"); + expect(capturedContext?.method).toBe("POST"); + expect(capturedContext?.adapter).toBe(adapter); + + // Verify adapter methods are accessible + expect(capturedContext?.adapter.getHeader("x-custom-header")).toBe("custom-value"); + expect(capturedContext?.adapter.getMethod()).toBe("POST"); + expect(capturedContext?.adapter.getPath()).toBe("/api/test"); + }); + }); +}); diff --git a/typescript/packages/core/test/mocks/cash/index.ts b/typescript/packages/core/test/mocks/cash/index.ts new file mode 100644 index 0000000..c84b512 --- /dev/null +++ b/typescript/packages/core/test/mocks/cash/index.ts @@ -0,0 +1,302 @@ +import { x402Facilitator } from "../../../../src/facilitator"; +import { FacilitatorClient } from "../../../../src/server"; +import { + SettleResponse, + SupportedResponse, + VerifyResponse, +} from "../../../../src/types/facilitator"; +import { + SchemeNetworkClient, + SchemeNetworkFacilitator, + SchemeNetworkServer, +} from "../../../../src/types/mechanisms"; +import { PaymentPayload, PaymentRequirements } from "../../../../src/types/payments"; +import { Price, AssetAmount, Network } from "../../../../src/types"; + +/** + * + */ +export class CashSchemeNetworkClient implements SchemeNetworkClient { + readonly scheme = "cash"; + + /** + * Creates a new CashClient instance. + * + * @param payer - The address of the payer + */ + constructor(private readonly payer: string) {} + + /** + * Creates a payment payload for the cash scheme. + * + * @param x402Version - The x402 protocol version + * @param requirements - The payment requirements + * @returns Promise resolving to the payment payload + */ + createPaymentPayload( + x402Version: number, + requirements: PaymentRequirements, + ): Promise { + return Promise.resolve({ + x402Version: 2, + scheme: requirements.scheme, + network: requirements.network, + payload: { + signature: `~${this.payer}`, + validUntil: (Date.now() + requirements.maxTimeoutSeconds).toString(), + name: this.payer, + }, + accepted: requirements, + }); + } +} + +/** + * + */ +export class CashSchemeNetworkFacilitator implements SchemeNetworkFacilitator { + readonly scheme = "cash"; + + /** + * Get mechanism-specific extra data for the supported kinds endpoint. + * For the mock cash scheme, return empty object. + * + * @param _ - The network identifier + * @returns Empty extra data object + */ + getExtra(_: string): Record | undefined { + return {}; + } + + /** + * Verifies a payment payload against requirements. + * + * @param payload - The payment payload to verify + * @param requirements - The payment requirements to verify against + * @returns Promise resolving to the verification response + */ + verify(payload: PaymentPayload, requirements: PaymentRequirements): Promise { + // Requirements parameter is not used in this implementation + void requirements; + if (payload.payload.signature !== `~${payload.payload.name}`) { + return Promise.resolve({ + isValid: false, + invalidReason: "invalid_signature", + payer: undefined, + }); + } + + if (payload.payload.validUntil < Date.now().toString()) { + return Promise.resolve({ + isValid: false, + invalidReason: "expired_signature", + payer: undefined, + }); + } + + return Promise.resolve({ + isValid: true, + invalidReason: undefined, + payer: payload.payload.signature, + }); + } + + /** + * Settles a payment based on the payload and requirements. + * + * @param payload - The payment payload to settle + * @param requirements - The payment requirements for settlement + * @returns Promise resolving to the settlement response + */ + async settle( + payload: PaymentPayload, + requirements: PaymentRequirements, + ): Promise { + const verifyResponse = await this.verify(payload, requirements); + if (!verifyResponse.isValid) { + return { + success: false, + errorReason: verifyResponse.invalidReason, + payer: verifyResponse.payer, + transaction: "", + network: requirements.network, + }; + } + + return { + success: true, + errorReason: undefined, + transaction: `${payload.payload.name} transferred ${requirements.amount} ${requirements.asset} to ${requirements.payTo}`, + network: requirements.network, + payer: payload.payload.signature, + }; + } +} + +/** + * Creates a payment receipt for the cash scheme. + * + * @param payTo - The recipient address + * @param asset - The asset being paid + * @param amount - The amount being paid + * @returns The payment receipt object + */ +export function buildCashPaymentRequirements( + payTo: string, + asset: string, + amount: string, +): PaymentRequirements { + return { + scheme: "cash", + network: "x402:cash", + asset: asset, + amount: amount, + payTo: payTo, + maxTimeoutSeconds: 1000, + extra: {}, + }; +} + +/** + * + */ +export class CashSchemeNetworkServer implements SchemeNetworkServer { + readonly scheme = "cash"; + + /** + * Parses a price into asset amount format. + * + * @param price - The price to parse + * @param network - The network identifier + * @returns Promise resolving to the parsed asset amount + */ + async parsePrice(price: Price, network: Network): Promise { + // Network parameter is not used in this implementation + void network; + // Handle pre-parsed price object + if (typeof price === "object" && price !== null && "amount" in price) { + return { + amount: price.amount, + asset: price.asset || "USD", + extra: {}, + }; + } + + // Parse string prices like "$10" or "10 USD" + if (typeof price === "string") { + const cleanPrice = price + .replace(/^\$/, "") + .replace(/\s+USD$/i, "") + .trim(); + return { + amount: cleanPrice, + asset: "USD", + extra: {}, + }; + } + + // Handle number input + if (typeof price === "number") { + return { + amount: price.toString(), + asset: "USD", + extra: {}, + }; + } + + throw new Error(`Invalid price format: ${price}`); + } + + /** + * Enhances payment requirements with cash-specific details. + * + * @param paymentRequirements - Base payment requirements + * @param supportedKind - The supported kind from facilitator + * @param supportedKind.x402Version - The x402 version + * @param supportedKind.scheme - The payment scheme + * @param supportedKind.network - The network identifier + * @param supportedKind.extra - Optional extra metadata + * @param facilitatorExtensions - Extensions supported by facilitator + * @returns Promise resolving to enhanced payment requirements + */ + async enhancePaymentRequirements( + paymentRequirements: PaymentRequirements, + supportedKind: { + x402Version: number; + scheme: string; + network: Network; + extra?: Record; + }, + facilitatorExtensions: string[], + ): Promise { + // Cash scheme doesn't need any special enhancements + // Parameters are not used in this implementation + void supportedKind; + void facilitatorExtensions; + return paymentRequirements; + } +} + +/** + * + */ +export class CashFacilitatorClient implements FacilitatorClient { + readonly scheme = "cash"; + readonly network = "x402:cash"; + readonly x402Version = 2; + + /** + * Registers a facilitator for the cash scheme. + * + * @param facilitator - The cash facilitator to register + */ + constructor(private readonly facilitator: x402Facilitator) {} + + /** + * Verifies a payment payload against requirements. + * + * @param paymentPayload - The payment payload to verify + * @param paymentRequirements - The payment requirements to verify against + * @returns Promise resolving to the verification response + */ + verify( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + ): Promise { + return this.facilitator.verify(paymentPayload, paymentRequirements); + } + + /** + * Settles a payment based on the payload and requirements. + * + * @param paymentPayload - The payment payload to settle + * @param paymentRequirements - The payment requirements for settlement + * @returns Promise resolving to the settlement response + */ + settle( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + ): Promise { + return this.facilitator.settle(paymentPayload, paymentRequirements); + } + + /** + * Gets supported payment kinds and extensions. + * + * @returns Promise resolving to the supported response + */ + getSupported(): Promise { + return Promise.resolve({ + kinds: [ + { + x402Version: this.x402Version, + scheme: this.scheme, + network: this.network, + extra: {}, + }, + ], + extensions: [], + signers: {}, + }); + } +} diff --git a/typescript/packages/core/test/mocks/generic/MockFacilitatorClient.ts b/typescript/packages/core/test/mocks/generic/MockFacilitatorClient.ts new file mode 100644 index 0000000..1dda1e3 --- /dev/null +++ b/typescript/packages/core/test/mocks/generic/MockFacilitatorClient.ts @@ -0,0 +1,134 @@ +import { FacilitatorClient } from "../../../src/http/httpFacilitatorClient"; +import { + SupportedResponse, + VerifyResponse, + SettleResponse, + VerifyRequest, + SettleRequest, +} from "../../../src/types/facilitator"; +import { PaymentPayload, PaymentRequirements } from "../../../src/types/payments"; + +/** + * Mock facilitator client for testing. + * Allows configuration of responses and tracking of calls. + */ +export class MockFacilitatorClient implements FacilitatorClient { + private supportedResponse: SupportedResponse; + private verifyResponseOrError: VerifyResponse | Error; + private settleResponseOrError: SettleResponse | Error; + + // Call tracking + public verifyCalls: Array<{ payload: PaymentPayload; requirements: PaymentRequirements }> = []; + public settleCalls: Array<{ payload: PaymentPayload; requirements: PaymentRequirements }> = []; + public getSupportedCalls: number = 0; + + /** + * + * @param supportedResponse + * @param verifyResponse + * @param settleResponse + */ + constructor( + supportedResponse: SupportedResponse, + verifyResponse: VerifyResponse | Error = { isValid: true }, + settleResponse: SettleResponse | Error = { success: true }, + ) { + this.supportedResponse = supportedResponse; + this.verifyResponseOrError = verifyResponse; + this.settleResponseOrError = settleResponse; + } + + /** + * + */ + async getSupported(): Promise { + this.getSupportedCalls++; + return this.supportedResponse; + } + + /** + * + */ + async verify(request: VerifyRequest): Promise; + /** + * + */ + async verify(payload: PaymentPayload, requirements: PaymentRequirements): Promise; + /** + * + * @param payloadOrRequest + * @param requirements + */ + async verify( + payloadOrRequest: PaymentPayload | VerifyRequest, + requirements?: PaymentRequirements, + ): Promise { + const payload = "payload" in payloadOrRequest ? payloadOrRequest.payload : payloadOrRequest; + const reqs = + requirements || + ("requirements" in payloadOrRequest ? payloadOrRequest.requirements : undefined)!; + + this.verifyCalls.push({ payload, requirements: reqs }); + + if (this.verifyResponseOrError instanceof Error) { + throw this.verifyResponseOrError; + } + return this.verifyResponseOrError; + } + + /** + * + */ + async settle(request: SettleRequest): Promise; + /** + * + */ + async settle(payload: PaymentPayload, requirements: PaymentRequirements): Promise; + /** + * + * @param payloadOrRequest + * @param requirements + */ + async settle( + payloadOrRequest: PaymentPayload | SettleRequest, + requirements?: PaymentRequirements, + ): Promise { + const payload = "payload" in payloadOrRequest ? payloadOrRequest.payload : payloadOrRequest; + const reqs = + requirements || + ("requirements" in payloadOrRequest ? payloadOrRequest.requirements : undefined)!; + + this.settleCalls.push({ payload, requirements: reqs }); + + if (this.settleResponseOrError instanceof Error) { + throw this.settleResponseOrError; + } + return this.settleResponseOrError; + } + + // Helper methods for test configuration + /** + * + * @param response + */ + setVerifyResponse(response: VerifyResponse | Error): void { + this.verifyResponseOrError = response; + } + + /** + * + * @param response + */ + setSettleResponse(response: SettleResponse | Error): void { + this.settleResponseOrError = response; + } + + /** + * + */ + reset(): void { + this.verifyCalls = []; + this.settleCalls = []; + this.getSupportedCalls = 0; + } +} diff --git a/typescript/packages/core/test/mocks/generic/MockSchemeClient.ts b/typescript/packages/core/test/mocks/generic/MockSchemeClient.ts new file mode 100644 index 0000000..96c5988 --- /dev/null +++ b/typescript/packages/core/test/mocks/generic/MockSchemeClient.ts @@ -0,0 +1,65 @@ +import { SchemeNetworkClient } from "../../../src/types/mechanisms"; +import { PaymentPayload, PaymentRequirements } from "../../../src/types/payments"; + +/** + * Mock scheme network client for testing. + */ +export class MockSchemeNetworkClient implements SchemeNetworkClient { + public readonly scheme: string; + private payloadResult: Pick | Error; + + // Call tracking + public createPaymentPayloadCalls: Array<{ + x402Version: number; + requirements: PaymentRequirements; + }> = []; + + /** + * + * @param scheme + * @param payloadResult + */ + constructor( + scheme: string, + payloadResult?: Pick | Error, + ) { + this.scheme = scheme; + this.payloadResult = payloadResult || { + x402Version: 2, + payload: { signature: "mock_signature", from: "mock_address" }, + }; + } + + /** + * + * @param x402Version + * @param paymentRequirements + */ + async createPaymentPayload( + x402Version: number, + paymentRequirements: PaymentRequirements, + ): Promise> { + this.createPaymentPayloadCalls.push({ x402Version, requirements: paymentRequirements }); + + if (this.payloadResult instanceof Error) { + throw this.payloadResult; + } + return this.payloadResult; + } + + // Helper methods for test configuration + /** + * + * @param result + */ + setPayloadResult(result: Pick | Error): void { + this.payloadResult = result; + } + + /** + * + */ + reset(): void { + this.createPaymentPayloadCalls = []; + } +} diff --git a/typescript/packages/core/test/mocks/generic/MockSchemeServer.ts b/typescript/packages/core/test/mocks/generic/MockSchemeServer.ts new file mode 100644 index 0000000..8ec46cf --- /dev/null +++ b/typescript/packages/core/test/mocks/generic/MockSchemeServer.ts @@ -0,0 +1,98 @@ +import { SchemeNetworkServer } from "../../../src/types/mechanisms"; +import { AssetAmount, Network, Price } from "../../../src/types"; +import { PaymentRequirements } from "../../../src/types/payments"; + +/** + * Mock scheme network server for testing. + */ +export class MockSchemeNetworkServer implements SchemeNetworkServer { + public readonly scheme: string; + private parsePriceResult: AssetAmount | Error; + private enhanceResult: PaymentRequirements | Error | null = null; + + // Call tracking + public parsePriceCalls: Array<{ price: Price; network: Network }> = []; + public enhanceCalls: Array<{ requirements: PaymentRequirements }> = []; + + /** + * + * @param scheme + * @param parsePriceResult + */ + constructor( + scheme: string, + parsePriceResult: AssetAmount = { amount: "1000000", asset: "USDC", extra: {} }, + ) { + this.scheme = scheme; + this.parsePriceResult = parsePriceResult; + } + + /** + * + * @param price + * @param network + */ + async parsePrice(price: Price, network: Network): Promise { + this.parsePriceCalls.push({ price, network }); + + if (this.parsePriceResult instanceof Error) { + throw this.parsePriceResult; + } + return this.parsePriceResult; + } + + /** + * + * @param paymentRequirements + * @param supportedKind + * @param supportedKind.x402Version + * @param supportedKind.scheme + * @param supportedKind.network + * @param supportedKind.extra + * @param facilitatorExtensions + */ + async enhancePaymentRequirements( + paymentRequirements: PaymentRequirements, + _supportedKind: { + x402Version: number; + scheme: string; + network: Network; + extra?: Record; + }, + _facilitatorExtensions: string[], + ): Promise { + this.enhanceCalls.push({ requirements: paymentRequirements }); + + if (this.enhanceResult instanceof Error) { + throw this.enhanceResult; + } + + // If no custom result, return the input + return this.enhanceResult || paymentRequirements; + } + + // Helper methods for test configuration + /** + * + * @param result + */ + setParsePriceResult(result: AssetAmount | Error): void { + this.parsePriceResult = result; + } + + /** + * + * @param result + */ + setEnhanceResult(result: PaymentRequirements | Error): void { + this.enhanceResult = result; + } + + /** + * + */ + reset(): void { + this.parsePriceCalls = []; + this.enhanceCalls = []; + } +} diff --git a/typescript/packages/core/test/mocks/generic/index.ts b/typescript/packages/core/test/mocks/generic/index.ts new file mode 100644 index 0000000..e82365c --- /dev/null +++ b/typescript/packages/core/test/mocks/generic/index.ts @@ -0,0 +1,11 @@ +export { MockFacilitatorClient } from "./MockFacilitatorClient"; +export { MockSchemeNetworkServer } from "./MockSchemeServer"; +export { MockSchemeNetworkClient } from "./MockSchemeClient"; +export { + buildPaymentRequired, + buildPaymentRequirements, + buildPaymentPayload, + buildVerifyResponse, + buildSettleResponse, + buildSupportedResponse, +} from "./testDataBuilders"; diff --git a/typescript/packages/core/test/mocks/generic/testDataBuilders.ts b/typescript/packages/core/test/mocks/generic/testDataBuilders.ts new file mode 100644 index 0000000..d899bd4 --- /dev/null +++ b/typescript/packages/core/test/mocks/generic/testDataBuilders.ts @@ -0,0 +1,128 @@ +import { PaymentRequired, PaymentPayload, PaymentRequirements } from "../../../src/types/payments"; +import { VerifyResponse, SettleResponse, SupportedResponse } from "../../../src/types/facilitator"; +import { Network } from "../../../src/types"; + +/** + * Test data builders for creating test fixtures. + */ + +/** + * + * @param overrides + */ +export function buildPaymentRequirements( + overrides?: Partial, +): PaymentRequirements { + return { + scheme: "test-scheme", + network: "test:network" as Network, + amount: "1000000", + asset: "TEST_ASSET", + payTo: "test_recipient", + maxTimeoutSeconds: 300, + extra: {}, + ...overrides, + }; +} + +/** + * + * @param overrides + */ +export function buildPaymentRequired(overrides?: Partial): PaymentRequired { + return { + x402Version: 2, + resource: { + url: "https://example.com/resource", + description: "Test resource", + mimeType: "application/json", + }, + accepts: [buildPaymentRequirements()], + ...overrides, + }; +} + +/** + * + * @param overrides + */ +export function buildPaymentPayload(overrides?: Partial): PaymentPayload { + return { + x402Version: 2, + payload: { + signature: "test_signature", + from: "test_sender", + }, + accepted: buildPaymentRequirements(), + resource: { + url: "https://example.com/resource", + description: "Test resource", + mimeType: "application/json", + }, + ...overrides, + }; +} + +/** + * + * @param overrides + */ +export function buildVerifyResponse(overrides?: Partial): VerifyResponse { + return { + isValid: true, + ...overrides, + }; +} + +/** + * + * @param overrides + */ +export function buildSettleResponse(overrides?: Partial): SettleResponse { + return { + success: true, + transaction: "0xTestTransaction", + network: "test:network" as Network, + ...overrides, + }; +} + +/** + * Builds a supported response for testing. + * Uses flat array format with x402Version in each kind for backward compatibility. + * + * Args: + * overrides: Partial overrides for the supported response + * + * Returns: + * A complete SupportedResponse object with test defaults + */ +export function buildSupportedResponse(overrides?: Partial): SupportedResponse { + const base: SupportedResponse = { + kinds: [ + { + x402Version: 2, + scheme: "test-scheme", + network: "test:network" as Network, + extra: {}, + }, + ], + extensions: [], + signers: {}, + }; + + // If overrides are provided, merge them + if (overrides) { + if (overrides.kinds !== undefined) { + base.kinds = overrides.kinds; + } + if (overrides.extensions !== undefined) { + base.extensions = overrides.extensions; + } + if (overrides.signers !== undefined) { + base.signers = overrides.signers; + } + } + + return base; +} diff --git a/typescript/packages/core/test/mocks/index.ts b/typescript/packages/core/test/mocks/index.ts new file mode 100644 index 0000000..b49c388 --- /dev/null +++ b/typescript/packages/core/test/mocks/index.ts @@ -0,0 +1,21 @@ +// Generic mocks for unit testing +export { MockFacilitatorClient } from "./generic/MockFacilitatorClient"; +export { MockSchemeNetworkServer } from "./generic/MockSchemeServer"; +export { MockSchemeNetworkClient } from "./generic/MockSchemeClient"; +export { + buildPaymentRequired, + buildPaymentRequirements, + buildPaymentPayload, + buildVerifyResponse, + buildSettleResponse, + buildSupportedResponse, +} from "./generic/testDataBuilders"; + +// Real cash implementation for integration and unit tests +export { + buildCashPaymentRequirements, + CashFacilitatorClient, + CashSchemeNetworkClient, + CashSchemeNetworkFacilitator, + CashSchemeNetworkServer, +} from "./cash"; diff --git a/typescript/packages/core/test/unit/client/x402Client.test.ts b/typescript/packages/core/test/unit/client/x402Client.test.ts new file mode 100644 index 0000000..40d9c37 --- /dev/null +++ b/typescript/packages/core/test/unit/client/x402Client.test.ts @@ -0,0 +1,595 @@ +import { describe, it, expect } from "vitest"; +import { x402Client } from "../../../src/client/x402Client"; +import { PaymentPolicy } from "../../../src/client/x402Client"; +import { MockSchemeNetworkClient } from "../../mocks"; +import { buildPaymentRequired, buildPaymentRequirements } from "../../mocks"; +import { Network, PaymentRequirements } from "../../../src/types"; + +describe("x402Client", () => { + describe("Construction", () => { + it("should create instance with default selector", () => { + const client = new x402Client(); + + expect(client).toBeDefined(); + }); + + it("should use custom payment requirements selector", async () => { + let selectorCalled = false; + const customSelector = (version: number, reqs: PaymentRequirements[]) => { + selectorCalled = true; + return reqs[reqs.length - 1]; // Choose last instead of first + }; + + const client = new x402Client(customSelector); + const mockClient = new MockSchemeNetworkClient("test-scheme"); + client.register("test:network" as Network, mockClient); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + buildPaymentRequirements({ + scheme: "test-scheme", + network: "test:network" as Network, + amount: "100", + }), + buildPaymentRequirements({ + scheme: "test-scheme", + network: "test:network" as Network, + amount: "200", + }), + ], + }); + + await client.createPaymentPayload(paymentRequired); + + expect(selectorCalled).toBe(true); + }); + + it("should use default selector that chooses first requirement", async () => { + const client = new x402Client(); + const mockClient = new MockSchemeNetworkClient("test-scheme"); + client.register("test:network" as Network, mockClient); + + const firstReq = buildPaymentRequirements({ + scheme: "test-scheme", + network: "test:network" as Network, + amount: "100", + }); + const secondReq = buildPaymentRequirements({ + scheme: "test-scheme", + network: "test:network" as Network, + amount: "200", + }); + + const paymentRequired = buildPaymentRequired({ + accepts: [firstReq, secondReq], + }); + + await client.createPaymentPayload(paymentRequired); + + // Should have called createPaymentPayload with first requirement + expect(mockClient.createPaymentPayloadCalls.length).toBe(1); + expect(mockClient.createPaymentPayloadCalls[0].requirements).toEqual(firstReq); + }); + }); + + describe("fromConfig", () => { + it("should create client from config", () => { + const mockClient1 = new MockSchemeNetworkClient("scheme1"); + const mockClient2 = new MockSchemeNetworkClient("scheme2"); + + const client = x402Client.fromConfig({ + schemes: [ + { network: "network1" as Network, client: mockClient1 }, + { network: "network2" as Network, client: mockClient2, x402Version: 1 }, + ], + }); + + expect(client).toBeDefined(); + }); + + it("should register v1 schemes correctly", async () => { + const mockClient = new MockSchemeNetworkClient("v1-scheme", { + x402Version: 1, + payload: { signature: "v1_sig" }, + }); + + const client = x402Client.fromConfig({ + schemes: [{ network: "base-sepolia" as Network, client: mockClient, x402Version: 1 }], + }); + + const paymentRequired = buildPaymentRequired({ + x402Version: 1, + accepts: [ + buildPaymentRequirements({ + scheme: "v1-scheme", + network: "base-sepolia" as Network, + }), + ], + }); + + const result = await client.createPaymentPayload(paymentRequired); + + expect(result.x402Version).toBe(1); + }); + + it("should register policies in order", async () => { + const executionOrder: number[] = []; + const policy1: PaymentPolicy = (version, reqs) => { + executionOrder.push(1); + return reqs; + }; + const policy2: PaymentPolicy = (version, reqs) => { + executionOrder.push(2); + return reqs; + }; + + const mockClient = new MockSchemeNetworkClient("test-scheme"); + + const client = x402Client.fromConfig({ + schemes: [{ network: "test:network" as Network, client: mockClient }], + policies: [policy1, policy2], + }); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + buildPaymentRequirements({ scheme: "test-scheme", network: "test:network" as Network }), + ], + }); + + await client.createPaymentPayload(paymentRequired); + + expect(executionOrder).toEqual([1, 2]); + }); + + it("should use custom selector from config", async () => { + let customSelectorCalled = false; + const customSelector = (version: number, reqs: PaymentRequirements[]) => { + customSelectorCalled = true; + return reqs[0]; + }; + + const mockClient = new MockSchemeNetworkClient("test-scheme"); + + const client = x402Client.fromConfig({ + schemes: [{ network: "test:network" as Network, client: mockClient }], + paymentRequirementsSelector: customSelector, + }); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + buildPaymentRequirements({ scheme: "test-scheme", network: "test:network" as Network }), + ], + }); + + await client.createPaymentPayload(paymentRequired); + + expect(customSelectorCalled).toBe(true); + }); + }); + + describe("register", () => { + it("should register scheme for v2", () => { + const client = new x402Client(); + const mockClient = new MockSchemeNetworkClient("test-scheme"); + + const result = client.register("test:network" as Network, mockClient); + + expect(result).toBe(client); // Chaining + }); + + it("should allow multiple schemes for same network", async () => { + const client = new x402Client(); + const exactClient = new MockSchemeNetworkClient("exact"); + const intentClient = new MockSchemeNetworkClient("intent"); + + client + .register("eip155:8453" as Network, exactClient) + .register("eip155:8453" as Network, intentClient); + + const paymentRequired = buildPaymentRequired({ + accepts: [buildPaymentRequirements({ scheme: "exact", network: "eip155:8453" as Network })], + }); + + await client.createPaymentPayload(paymentRequired); + + expect(exactClient.createPaymentPayloadCalls.length).toBe(1); + }); + + it("should allow same scheme on multiple networks", async () => { + const client = new x402Client(); + const evmClient = new MockSchemeNetworkClient("exact"); + const svmClient = new MockSchemeNetworkClient("exact"); + + client + .register("eip155:8453" as Network, evmClient) + .register("solana:mainnet" as Network, svmClient); + + // Should be able to create payload for either network + const evmPaymentRequired = buildPaymentRequired({ + accepts: [buildPaymentRequirements({ scheme: "exact", network: "eip155:8453" as Network })], + }); + + await client.createPaymentPayload(evmPaymentRequired); + + expect(evmClient.createPaymentPayloadCalls.length).toBe(1); + }); + }); + + describe("registerV1", () => { + it("should register scheme for v1", () => { + const client = new x402Client(); + const mockClient = new MockSchemeNetworkClient("exact"); + + const result = client.registerV1("base-sepolia", mockClient); + + expect(result).toBe(client); + }); + }); + + describe("registerPolicy", () => { + it("should add policy to policy chain", () => { + const client = new x402Client(); + const policy: PaymentPolicy = (_version, _reqs) => _reqs; + + const result = client.registerPolicy(policy); + + expect(result).toBe(client); + }); + + it("should return this for chaining", () => { + const client = new x402Client(); + const policy1: PaymentPolicy = (v, r) => r; + const policy2: PaymentPolicy = (v, r) => r; + + const result = client.registerPolicy(policy1).registerPolicy(policy2); + + expect(result).toBe(client); + }); + }); + + describe("createPaymentPayload", () => { + describe("Happy path", () => { + it("should create payment payload from PaymentRequired", async () => { + const client = new x402Client(); + const mockClient = new MockSchemeNetworkClient("exact"); + client.register("eip155:8453" as Network, mockClient); + + const paymentRequired = buildPaymentRequired({ + x402Version: 2, + resource: { url: "https://example.com", description: "Test", mimeType: "text/plain" }, + accepts: [ + buildPaymentRequirements({ scheme: "exact", network: "eip155:8453" as Network }), + ], + extensions: { testExtension: true }, + }); + + const result = await client.createPaymentPayload(paymentRequired); + + expect(result.x402Version).toBe(2); + expect(result.payload).toBeDefined(); + expect(result.resource).toEqual(paymentRequired.resource); + expect(result.extensions).toEqual({ testExtension: true }); + expect(result.accepted).toBeDefined(); + }); + + it("should call scheme client's createPaymentPayload", async () => { + const client = new x402Client(); + const mockClient = new MockSchemeNetworkClient("exact"); + client.register("eip155:8453" as Network, mockClient); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + buildPaymentRequirements({ scheme: "exact", network: "eip155:8453" as Network }), + ], + }); + + await client.createPaymentPayload(paymentRequired); + + expect(mockClient.createPaymentPayloadCalls.length).toBe(1); + expect(mockClient.createPaymentPayloadCalls[0].x402Version).toBe(2); + }); + }); + + describe("Error cases", () => { + it("should throw if no client registered for x402 version", async () => { + const client = new x402Client(); + + const paymentRequired = buildPaymentRequired({ + x402Version: 2, + accepts: [ + buildPaymentRequirements({ scheme: "exact", network: "eip155:8453" as Network }), + ], + }); + + await expect( + async () => await client.createPaymentPayload(paymentRequired), + ).rejects.toThrow("No client registered for x402 version: 2"); + }); + + it("should throw if no matching scheme/network client found", async () => { + const client = new x402Client(); + const mockClient = new MockSchemeNetworkClient("exact"); + client.register("eip155:8453" as Network, mockClient); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + buildPaymentRequirements({ + scheme: "exact", + network: "solana:mainnet" as Network, // Different network + }), + ], + }); + + await expect( + async () => await client.createPaymentPayload(paymentRequired), + ).rejects.toThrow("No network/scheme registered"); + }); + + it("should throw if PaymentRequired has empty accepts array", async () => { + const client = new x402Client(); + const mockClient = new MockSchemeNetworkClient("exact"); + client.register("eip155:8453" as Network, mockClient); + + const paymentRequired = buildPaymentRequired({ + accepts: [], + }); + + await expect( + async () => await client.createPaymentPayload(paymentRequired), + ).rejects.toThrow(); + }); + }); + + describe("Policy application", () => { + it("should filter requirements based on policy", async () => { + const client = new x402Client(); + const mockClient = new MockSchemeNetworkClient("exact"); + client.register("eip155:8453" as Network, mockClient); + + // Policy that prefers cheap options + const cheapPolicy: PaymentPolicy = (version, reqs) => + reqs.filter(r => BigInt(r.amount) < BigInt("500000")); + + client.registerPolicy(cheapPolicy); + + const expensiveReq = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + const cheapReq = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "100000", + }); + + const paymentRequired = buildPaymentRequired({ + accepts: [expensiveReq, cheapReq], + }); + + await client.createPaymentPayload(paymentRequired); + + // Should have selected cheap option + expect(mockClient.createPaymentPayloadCalls[0].requirements.amount).toBe("100000"); + }); + + it("should apply multiple policies in order", async () => { + const client = new x402Client(); + const mockClient = new MockSchemeNetworkClient("exact"); + client.register("eip155:*" as Network, mockClient); + + const executionOrder: number[] = []; + + const policy1: PaymentPolicy = (_version, reqs) => { + executionOrder.push(1); + return reqs.filter(r => r.network.startsWith("eip155:")); + }; + + const policy2: PaymentPolicy = (_version, reqs) => { + executionOrder.push(2); + return reqs.filter(r => BigInt(r.amount) < BigInt("500000")); + }; + + client.registerPolicy(policy1).registerPolicy(policy2); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + buildPaymentRequirements({ + scheme: "exact", + network: "solana:mainnet" as Network, + amount: "100000", + }), + buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }), + buildPaymentRequirements({ + scheme: "exact", + network: "eip155:1" as Network, + amount: "100000", + }), + ], + }); + + await client.createPaymentPayload(paymentRequired); + + expect(executionOrder).toEqual([1, 2]); + // Should have filtered to EIP-155 networks, then to cheap option + expect(mockClient.createPaymentPayloadCalls[0].requirements.network).toBe("eip155:1"); + }); + + it("should throw if all requirements filtered out by policies", async () => { + const client = new x402Client(); + const mockClient = new MockSchemeNetworkClient("exact"); + client.register("eip155:8453" as Network, mockClient); + + // Policy that filters everything out + const rejectAllPolicy: PaymentPolicy = (_version, _reqs) => []; + + client.registerPolicy(rejectAllPolicy); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + buildPaymentRequirements({ scheme: "exact", network: "eip155:8453" as Network }), + ], + }); + + await expect( + async () => await client.createPaymentPayload(paymentRequired), + ).rejects.toThrow("All payment requirements were filtered out by policies"); + }); + }); + + describe("Scheme filtering", () => { + it("should only select requirements for registered schemes", async () => { + const client = new x402Client(); + const exactClient = new MockSchemeNetworkClient("exact"); + + // Only register exact scheme + client.register("eip155:8453" as Network, exactClient); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + buildPaymentRequirements({ scheme: "intent", network: "eip155:8453" as Network }), + buildPaymentRequirements({ scheme: "exact", network: "eip155:8453" as Network }), + buildPaymentRequirements({ scheme: "other", network: "eip155:8453" as Network }), + ], + }); + + await client.createPaymentPayload(paymentRequired); + + // Should have selected exact scheme + expect(exactClient.createPaymentPayloadCalls[0].requirements.scheme).toBe("exact"); + }); + + it("should throw if no registered scheme matches any requirement", async () => { + const client = new x402Client(); + const mockClient = new MockSchemeNetworkClient("exact"); + + client.register("eip155:8453" as Network, mockClient); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + // All requirements are for networks we don't support + buildPaymentRequirements({ scheme: "exact", network: "solana:mainnet" as Network }), + buildPaymentRequirements({ scheme: "intent", network: "eip155:8453" as Network }), + ], + }); + + await expect( + async () => await client.createPaymentPayload(paymentRequired), + ).rejects.toThrow("No network/scheme registered"); + }); + }); + + describe("Network pattern matching", () => { + it("should match wildcard network patterns", async () => { + const client = new x402Client(); + const evmClient = new MockSchemeNetworkClient("exact"); + + // Register with wildcard + client.register("eip155:*" as Network, evmClient); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + buildPaymentRequirements({ scheme: "exact", network: "eip155:8453" as Network }), + ], + }); + + const result = await client.createPaymentPayload(paymentRequired); + + expect(result).toBeDefined(); + expect(evmClient.createPaymentPayloadCalls.length).toBe(1); + }); + + it("should handle exact network matches", async () => { + const client = new x402Client(); + const mockClient = new MockSchemeNetworkClient("exact"); + + client.register("eip155:8453" as Network, mockClient); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + buildPaymentRequirements({ scheme: "exact", network: "eip155:8453" as Network }), + ], + }); + + const result = await client.createPaymentPayload(paymentRequired); + + expect(result).toBeDefined(); + }); + }); + + describe("Multiple options handling", () => { + it("should select from multiple payment requirements", async () => { + const client = new x402Client(); + const exactClient = new MockSchemeNetworkClient("exact"); + + client.register("eip155:*" as Network, exactClient); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "100", + }), + buildPaymentRequirements({ + scheme: "exact", + network: "eip155:1" as Network, + amount: "200", + }), + buildPaymentRequirements({ + scheme: "exact", + network: "eip155:84532" as Network, + amount: "300", + }), + ], + }); + + await client.createPaymentPayload(paymentRequired); + + // Default selector chooses first + expect(exactClient.createPaymentPayloadCalls[0].requirements.amount).toBe("100"); + }); + + it("should respect custom selector logic", async () => { + // Selector that chooses cheapest option + const cheapestSelector = (version: number, reqs: PaymentRequirements[]) => { + return reqs.reduce((cheapest, current) => + BigInt(current.amount) < BigInt(cheapest.amount) ? current : cheapest, + ); + }; + + const client = new x402Client(cheapestSelector); + const mockClient = new MockSchemeNetworkClient("exact"); + client.register("eip155:*" as Network, mockClient); + + const paymentRequired = buildPaymentRequired({ + accepts: [ + buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }), + buildPaymentRequirements({ + scheme: "exact", + network: "eip155:1" as Network, + amount: "100000", + }), // Cheapest + buildPaymentRequirements({ + scheme: "exact", + network: "eip155:84532" as Network, + amount: "500000", + }), + ], + }); + + await client.createPaymentPayload(paymentRequired); + + expect(mockClient.createPaymentPayloadCalls[0].requirements.amount).toBe("100000"); + }); + }); + }); +}); diff --git a/typescript/packages/core/test/unit/facilitator/x402Facilitator.hooks.test.ts b/typescript/packages/core/test/unit/facilitator/x402Facilitator.hooks.test.ts new file mode 100644 index 0000000..dd3910e --- /dev/null +++ b/typescript/packages/core/test/unit/facilitator/x402Facilitator.hooks.test.ts @@ -0,0 +1,334 @@ +import { describe, it, expect } from "vitest"; +import { x402Facilitator } from "../../../src/facilitator/x402Facilitator"; +import { + PaymentPayload, + PaymentRequirements, + VerifyResponse, + SettleResponse, +} from "../../../src/types"; +import { SchemeNetworkFacilitator } from "../../../src/types/mechanisms"; + +// Mock scheme facilitator +class MockSchemeFacilitator implements SchemeNetworkFacilitator { + readonly scheme = "exact"; + + constructor( + private verifyFn?: ( + payload: PaymentPayload, + requirements: PaymentRequirements, + ) => Promise, + private settleFn?: ( + payload: PaymentPayload, + requirements: PaymentRequirements, + ) => Promise, + ) {} + + getExtra(_: string): Record | undefined { + return undefined; + } + + async verify( + payload: PaymentPayload, + requirements: PaymentRequirements, + ): Promise { + if (this.verifyFn) { + return this.verifyFn(payload, requirements); + } + return { isValid: true, payer: "0xMockPayer" }; + } + + async settle( + payload: PaymentPayload, + requirements: PaymentRequirements, + ): Promise { + if (this.settleFn) { + return this.settleFn(payload, requirements); + } + return { success: true, transaction: "0xMockTx", network: requirements.network }; + } +} + +const buildPaymentPayload = (): PaymentPayload => ({ + x402Version: 2, + payload: {}, + accepted: { + scheme: "exact", + network: "eip155:8453", + asset: "0xUSDC", + amount: "1000000", + payTo: "0xRecipient", + maxTimeoutSeconds: 300, + extra: {}, + }, + resource: { + url: "https://example.com/resource", + description: "Test resource", + mimeType: "application/json", + }, +}); + +const buildPaymentRequirements = (): PaymentRequirements => ({ + scheme: "exact", + network: "eip155:8453", + asset: "0xUSDC", + amount: "1000000", + payTo: "0xRecipient", + maxTimeoutSeconds: 300, + extra: {}, +}); + +describe("x402Facilitator - Lifecycle Hooks", () => { + describe("onBeforeVerify", () => { + it("should execute hook before verification", async () => { + const facilitator = new x402Facilitator(); + facilitator.register("eip155:8453", new MockSchemeFacilitator()); + + let hookCalled = false; + facilitator.onBeforeVerify(async context => { + hookCalled = true; + expect(context.paymentPayload).toBeDefined(); + expect(context.requirements).toBeDefined(); + }); + + await facilitator.verify(buildPaymentPayload(), buildPaymentRequirements()); + expect(hookCalled).toBe(true); + }); + + it("should abort verification when hook returns abort", async () => { + const facilitator = new x402Facilitator(); + facilitator.register("eip155:8453", new MockSchemeFacilitator()); + + facilitator.onBeforeVerify(async () => { + return { abort: true, reason: "Facilitator security check failed" }; + }); + + const result = await facilitator.verify(buildPaymentPayload(), buildPaymentRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("Facilitator security check failed"); + }); + + it("should execute multiple hooks in order", async () => { + const facilitator = new x402Facilitator(); + facilitator.register("eip155:8453", new MockSchemeFacilitator()); + + const executionOrder: number[] = []; + + facilitator + .onBeforeVerify(async () => { + executionOrder.push(1); + }) + .onBeforeVerify(async () => { + executionOrder.push(2); + }) + .onBeforeVerify(async () => { + executionOrder.push(3); + }); + + await facilitator.verify(buildPaymentPayload(), buildPaymentRequirements()); + expect(executionOrder).toEqual([1, 2, 3]); + }); + + it("should stop on first abort", async () => { + const facilitator = new x402Facilitator(); + facilitator.register("eip155:8453", new MockSchemeFacilitator()); + + const executionOrder: number[] = []; + + facilitator + .onBeforeVerify(async () => { + executionOrder.push(1); + }) + .onBeforeVerify(async () => { + executionOrder.push(2); + return { abort: true, reason: "Aborted" }; + }) + .onBeforeVerify(async () => { + executionOrder.push(3); // Should not execute + }); + + await facilitator.verify(buildPaymentPayload(), buildPaymentRequirements()); + expect(executionOrder).toEqual([1, 2]); + }); + }); + + describe("onAfterVerify", () => { + it("should execute hook after successful verification", async () => { + const facilitator = new x402Facilitator(); + facilitator.register("eip155:8453", new MockSchemeFacilitator()); + + let capturedResult: VerifyResponse | undefined; + + facilitator.onAfterVerify(async context => { + capturedResult = context.result; + }); + + const result = await facilitator.verify(buildPaymentPayload(), buildPaymentRequirements()); + + expect(result.isValid).toBe(true); + expect(capturedResult?.isValid).toBe(true); + }); + + it("should execute multiple hooks in order", async () => { + const facilitator = new x402Facilitator(); + facilitator.register("eip155:8453", new MockSchemeFacilitator()); + + const executionOrder: number[] = []; + + facilitator + .onAfterVerify(async () => { + executionOrder.push(1); + }) + .onAfterVerify(async () => { + executionOrder.push(2); + }); + + await facilitator.verify(buildPaymentPayload(), buildPaymentRequirements()); + expect(executionOrder).toEqual([1, 2]); + }); + }); + + describe("onVerifyFailure", () => { + it("should execute hook when verification fails", async () => { + const facilitator = new x402Facilitator(); + + const mockScheme = new MockSchemeFacilitator(async () => { + throw new Error("Verification failed"); + }); + facilitator.register("eip155:8453", mockScheme); + + let hookCalled = false; + let capturedError: Error | undefined; + + facilitator.onVerifyFailure(async context => { + hookCalled = true; + capturedError = context.error; + }); + + await expect( + facilitator.verify(buildPaymentPayload(), buildPaymentRequirements()), + ).rejects.toThrow("Verification failed"); + + expect(hookCalled).toBe(true); + expect(capturedError?.message).toBe("Verification failed"); + }); + + it("should recover from failure when hook returns recovered", async () => { + const facilitator = new x402Facilitator(); + + const mockScheme = new MockSchemeFacilitator(async () => { + throw new Error("Verification failed"); + }); + facilitator.register("eip155:8453", mockScheme); + + facilitator.onVerifyFailure(async () => { + return { + recovered: true, + result: { isValid: true, payer: "0xRecovered" }, + }; + }); + + const result = await facilitator.verify(buildPaymentPayload(), buildPaymentRequirements()); + + expect(result.isValid).toBe(true); + expect(result.payer).toBe("0xRecovered"); + }); + }); + + describe("onBeforeSettle", () => { + it("should abort settlement when hook returns abort", async () => { + const facilitator = new x402Facilitator(); + facilitator.register("eip155:8453", new MockSchemeFacilitator()); + + facilitator.onBeforeSettle(async () => { + return { abort: true, reason: "Gas price too high" }; + }); + + await expect( + facilitator.settle(buildPaymentPayload(), buildPaymentRequirements()), + ).rejects.toThrow("Settlement aborted: Gas price too high"); + }); + }); + + describe("onAfterSettle", () => { + it("should execute hook after successful settlement", async () => { + const facilitator = new x402Facilitator(); + facilitator.register("eip155:8453", new MockSchemeFacilitator()); + + let capturedTx: string | undefined; + + facilitator.onAfterSettle(async context => { + capturedTx = context.result.transaction; + }); + + const result = await facilitator.settle(buildPaymentPayload(), buildPaymentRequirements()); + + expect(result.success).toBe(true); + expect(capturedTx).toBe("0xMockTx"); + }); + }); + + describe("onSettleFailure", () => { + it("should execute hook when settlement fails", async () => { + const facilitator = new x402Facilitator(); + + const mockScheme = new MockSchemeFacilitator(undefined, async () => { + throw new Error("Settlement failed"); + }); + facilitator.register("eip155:8453", mockScheme); + + let hookCalled = false; + + facilitator.onSettleFailure(async () => { + hookCalled = true; + }); + + await expect( + facilitator.settle(buildPaymentPayload(), buildPaymentRequirements()), + ).rejects.toThrow("Settlement failed"); + + expect(hookCalled).toBe(true); + }); + + it("should recover from failure when hook returns recovered", async () => { + const facilitator = new x402Facilitator(); + + const mockScheme = new MockSchemeFacilitator(undefined, async () => { + throw new Error("Settlement failed"); + }); + facilitator.register("eip155:8453", mockScheme); + + facilitator.onSettleFailure(async () => { + return { + recovered: true, + result: { + success: true, + transaction: "0xFacilitatorRecovered", + network: "eip155:8453", + }, + }; + }); + + const result = await facilitator.settle(buildPaymentPayload(), buildPaymentRequirements()); + + expect(result.success).toBe(true); + expect(result.transaction).toBe("0xFacilitatorRecovered"); + }); + }); + + describe("Hook chainability", () => { + it("should allow chaining all hook registrations", () => { + const facilitator = new x402Facilitator(); + + const result = facilitator + .onBeforeVerify(async () => {}) + .onAfterVerify(async () => {}) + .onVerifyFailure(async () => {}) + .onBeforeSettle(async () => {}) + .onAfterSettle(async () => {}) + .onSettleFailure(async () => {}); + + expect(result).toBe(facilitator); + }); + }); +}); diff --git a/typescript/packages/core/test/unit/facilitator/x402Facilitator.test.ts b/typescript/packages/core/test/unit/facilitator/x402Facilitator.test.ts new file mode 100644 index 0000000..3306927 --- /dev/null +++ b/typescript/packages/core/test/unit/facilitator/x402Facilitator.test.ts @@ -0,0 +1,460 @@ +import { describe, it, expect } from "vitest"; +import { x402Facilitator } from "../../../src/facilitator/x402Facilitator"; +import { SchemeNetworkFacilitator } from "../../../src/types/mechanisms"; +import { PaymentPayload, PaymentRequirements } from "../../../src/types/payments"; +import { VerifyResponse, SettleResponse } from "../../../src/types/facilitator"; +import { Network } from "../../../src/types"; +import { buildPaymentPayload, buildPaymentRequirements } from "../../mocks"; + +// Mock facilitator implementation +/** + * + */ +class TestFacilitator implements SchemeNetworkFacilitator { + public readonly scheme: string; + public verifyCalls: Array<{ payload: PaymentPayload; requirements: PaymentRequirements }> = []; + public settleCalls: Array<{ payload: PaymentPayload; requirements: PaymentRequirements }> = []; + + /** + * + * @param scheme + * @param verifyResponse + * @param settleResponse + */ + constructor( + scheme: string, + private verifyResponse: VerifyResponse | Error = { isValid: true }, + private settleResponse: SettleResponse | Error = { + success: true, + transaction: "0xTestTx", + network: "test:network", + }, + ) { + this.scheme = scheme; + } + + getExtra(_: string): Record | undefined { + return undefined; + } + + /** + * + * @param payload + * @param requirements + */ + async verify( + payload: PaymentPayload, + requirements: PaymentRequirements, + ): Promise { + this.verifyCalls.push({ payload, requirements }); + if (this.verifyResponse instanceof Error) { + throw this.verifyResponse; + } + return this.verifyResponse; + } + + /** + * + * @param payload + * @param requirements + */ + async settle( + payload: PaymentPayload, + requirements: PaymentRequirements, + ): Promise { + this.settleCalls.push({ payload, requirements }); + if (this.settleResponse instanceof Error) { + throw this.settleResponse; + } + return this.settleResponse; + } +} + +describe("x402Facilitator", () => { + describe("Construction", () => { + it("should create empty instance", () => { + const facilitator = new x402Facilitator(); + + expect(facilitator).toBeDefined(); + expect(facilitator.getExtensions()).toEqual([]); + }); + }); + + describe("register", () => { + it("should register scheme for current version (v2)", () => { + const facilitator = new x402Facilitator(); + const testFacilitator = new TestFacilitator("test-scheme"); + + const result = facilitator.register("test:network" as Network, testFacilitator); + + expect(result).toBe(facilitator); // Chaining + }); + + it("should return this for chaining", () => { + const facilitator = new x402Facilitator(); + const scheme1 = new TestFacilitator("scheme1"); + const scheme2 = new TestFacilitator("scheme2"); + + const result = facilitator + .register("network1" as Network, scheme1) + .register("network2" as Network, scheme2); + + expect(result).toBe(facilitator); + }); + + it("should support multiple schemes per network", () => { + const facilitator = new x402Facilitator(); + const exactFacilitator = new TestFacilitator("exact"); + const intentFacilitator = new TestFacilitator("intent"); + + facilitator + .register("eip155:8453" as Network, exactFacilitator) + .register("eip155:8453" as Network, intentFacilitator); + + // Should be able to verify with both schemes + const payload1 = buildPaymentPayload({ x402Version: 2 }); + const req1 = buildPaymentRequirements({ scheme: "exact", network: "eip155:8453" as Network }); + + const payload2 = buildPaymentPayload({ x402Version: 2 }); + const req2 = buildPaymentRequirements({ + scheme: "intent", + network: "eip155:8453" as Network, + }); + + expect(() => facilitator.verify(payload1, req1)).not.toThrow(); + expect(() => facilitator.verify(payload2, req2)).not.toThrow(); + }); + + it("should support same scheme on multiple networks", () => { + const facilitator = new x402Facilitator(); + const evmFacilitator = new TestFacilitator("exact"); + const svmFacilitator = new TestFacilitator("exact"); + + facilitator + .register("eip155:8453" as Network, evmFacilitator) + .register("solana:mainnet" as Network, svmFacilitator); + + const payload1 = buildPaymentPayload({ x402Version: 2 }); + const req1 = buildPaymentRequirements({ scheme: "exact", network: "eip155:8453" as Network }); + + const payload2 = buildPaymentPayload({ x402Version: 2 }); + const req2 = buildPaymentRequirements({ + scheme: "exact", + network: "solana:mainnet" as Network, + }); + + expect(() => facilitator.verify(payload1, req1)).not.toThrow(); + expect(() => facilitator.verify(payload2, req2)).not.toThrow(); + }); + }); + + describe("registerV1", () => { + it("should register scheme for v1", () => { + const facilitator = new x402Facilitator(); + const testFacilitator = new TestFacilitator("test-scheme"); + + const result = facilitator.registerV1("test-network" as Network, testFacilitator); + + expect(result).toBe(facilitator); + }); + + it("should handle v1 payment payloads", async () => { + const facilitator = new x402Facilitator(); + const testFacilitator = new TestFacilitator("exact"); + + facilitator.registerV1("base-sepolia" as Network, testFacilitator); + + const payload = buildPaymentPayload({ x402Version: 1 }); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "base-sepolia" as Network, + }); + + const result = await facilitator.verify(payload, requirements); + + expect(result.isValid).toBe(true); + expect(testFacilitator.verifyCalls.length).toBe(1); + }); + }); + + describe("Extensions", () => { + it("should register extension", () => { + const facilitator = new x402Facilitator(); + + const result = facilitator.registerExtension("bazaar"); + + expect(result).toBe(facilitator); + expect(facilitator.getExtensions()).toEqual(["bazaar"]); + }); + + it("should register multiple extensions", () => { + const facilitator = new x402Facilitator(); + + facilitator.registerExtension("bazaar").registerExtension("sign_in_with_x"); + + expect(facilitator.getExtensions()).toEqual(["bazaar", "sign_in_with_x"]); + }); + + it("should not duplicate extensions", () => { + const facilitator = new x402Facilitator(); + + facilitator + .registerExtension("bazaar") + .registerExtension("bazaar") + .registerExtension("bazaar"); + + expect(facilitator.getExtensions()).toEqual(["bazaar"]); + }); + + it("should return copy of extensions array", () => { + const facilitator = new x402Facilitator(); + facilitator.registerExtension("bazaar"); + + const extensions = facilitator.getExtensions(); + extensions.push("modified"); + + expect(facilitator.getExtensions()).toEqual(["bazaar"]); + }); + + it("should return empty array if no extensions registered", () => { + const facilitator = new x402Facilitator(); + + expect(facilitator.getExtensions()).toEqual([]); + }); + }); + + describe("verify", () => { + it("should delegate to registered scheme facilitator", async () => { + const facilitator = new x402Facilitator(); + const testFacilitator = new TestFacilitator("exact", { isValid: true }); + + facilitator.register(["eip155:8453" as Network], testFacilitator); + + const payload = buildPaymentPayload({ x402Version: 2 }); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + const result = await facilitator.verify(payload, requirements); + + expect(result.isValid).toBe(true); + expect(testFacilitator.verifyCalls.length).toBe(1); + expect(testFacilitator.verifyCalls[0].payload).toBe(payload); + expect(testFacilitator.verifyCalls[0].requirements).toBe(requirements); + }); + + it("should use pattern matching for network", async () => { + const facilitator = new x402Facilitator(); + const testFacilitator = new TestFacilitator("exact"); + + // Register with multiple EVM networks (will auto-derive eip155:* pattern) + facilitator.register(["eip155:8453" as Network, "eip155:1" as Network], testFacilitator); + + const payload = buildPaymentPayload({ x402Version: 2 }); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + const result = await facilitator.verify(payload, requirements); + + expect(result.isValid).toBe(true); + expect(testFacilitator.verifyCalls.length).toBe(1); + }); + + it("should throw if no facilitator registered for version", async () => { + const facilitator = new x402Facilitator(); + + const payload = buildPaymentPayload({ x402Version: 2 }); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + await expect(async () => await facilitator.verify(payload, requirements)).rejects.toThrow( + "No facilitator registered for x402 version: 2", + ); + }); + + it("should throw if no facilitator registered for network/scheme", async () => { + const facilitator = new x402Facilitator(); + const testFacilitator = new TestFacilitator("exact"); + + facilitator.register(["eip155:8453" as Network], testFacilitator); + + const payload = buildPaymentPayload({ x402Version: 2 }); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "solana:mainnet" as Network, // Different network + }); + + await expect(async () => await facilitator.verify(payload, requirements)).rejects.toThrow( + "No facilitator registered for scheme: exact and network: solana:mainnet", + ); + }); + + it("should propagate errors from scheme facilitator", async () => { + const facilitator = new x402Facilitator(); + const errorFacilitator = new TestFacilitator( + "exact", + new Error("Verification failed: invalid signature"), + ); + + facilitator.register("eip155:8453" as Network, errorFacilitator); + + const payload = buildPaymentPayload({ x402Version: 2 }); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + await expect(async () => await facilitator.verify(payload, requirements)).rejects.toThrow( + "Verification failed: invalid signature", + ); + }); + }); + + describe("settle", () => { + it("should delegate to registered scheme facilitator", async () => { + const facilitator = new x402Facilitator(); + const testFacilitator = new TestFacilitator("exact", undefined, { + success: true, + transaction: "0xTestTx", + network: "eip155:8453", + }); + + facilitator.register(["eip155:8453" as Network], testFacilitator); + + const payload = buildPaymentPayload({ x402Version: 2 }); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + const result = await facilitator.settle(payload, requirements); + + expect(result.success).toBe(true); + expect(testFacilitator.settleCalls.length).toBe(1); + expect(testFacilitator.settleCalls[0].payload).toBe(payload); + expect(testFacilitator.settleCalls[0].requirements).toBe(requirements); + }); + + it("should use pattern matching for network", async () => { + const facilitator = new x402Facilitator(); + const testFacilitator = new TestFacilitator("exact"); + + facilitator.register("solana:*" as Network, testFacilitator); + + const payload = buildPaymentPayload({ x402Version: 2 }); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" as Network, + }); + + const result = await facilitator.settle(payload, requirements); + + expect(result.success).toBe(true); + expect(testFacilitator.settleCalls.length).toBe(1); + }); + + it("should throw if no facilitator registered for version", async () => { + const facilitator = new x402Facilitator(); + + const payload = buildPaymentPayload({ x402Version: 2 }); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + await expect(async () => await facilitator.settle(payload, requirements)).rejects.toThrow( + "No facilitator registered for x402 version: 2", + ); + }); + + it("should throw if no facilitator registered for network/scheme", async () => { + const facilitator = new x402Facilitator(); + const testFacilitator = new TestFacilitator("exact"); + + facilitator.register(["eip155:8453" as Network], testFacilitator); + + const payload = buildPaymentPayload({ x402Version: 2 }); + const requirements = buildPaymentRequirements({ + scheme: "intent", // Different scheme + network: "eip155:8453" as Network, + }); + + await expect(async () => await facilitator.settle(payload, requirements)).rejects.toThrow( + "No facilitator registered for scheme: intent and network: eip155:8453", + ); + }); + + it("should propagate errors from scheme facilitator", async () => { + const facilitator = new x402Facilitator(); + const errorFacilitator = new TestFacilitator( + "exact", + undefined, + new Error("Settlement failed: insufficient funds"), + ); + + facilitator.register("eip155:8453" as Network, errorFacilitator); + + const payload = buildPaymentPayload({ x402Version: 2 }); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + await expect(async () => await facilitator.settle(payload, requirements)).rejects.toThrow( + "Settlement failed: insufficient funds", + ); + }); + }); + + describe("Version support", () => { + it("should handle v1 and v2 separately", async () => { + const facilitator = new x402Facilitator(); + const v1Facilitator = new TestFacilitator("exact", { isValid: true }); + const v2Facilitator = new TestFacilitator("exact", { isValid: true }); + + facilitator.registerV1("eip155:8453" as Network, v1Facilitator); + facilitator.register("eip155:8453" as Network, v2Facilitator); + + const v1Payload = buildPaymentPayload({ x402Version: 1 }); + const v2Payload = buildPaymentPayload({ x402Version: 2 }); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + const v1Result = await facilitator.verify(v1Payload, requirements); + const v2Result = await facilitator.verify(v2Payload, requirements); + + expect(v1Result.isValid).toBe(true); + expect(v2Result.isValid).toBe(true); + }); + }); + + describe("Network pattern matching", () => { + it("should prefer exact match over pattern", async () => { + const facilitator = new x402Facilitator(); + const exactFacilitator = new TestFacilitator("exact", { isValid: true }); + const patternFacilitator = new TestFacilitator("exact", { isValid: true }); + + facilitator.register("eip155:8453" as Network, exactFacilitator); + facilitator.register("eip155:*" as Network, patternFacilitator); + + const payload = buildPaymentPayload({ x402Version: 2 }); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + const result = await facilitator.verify(payload, requirements); + + expect(result.isValid).toBe(true); + expect(exactFacilitator.verifyCalls.length).toBe(1); + expect(patternFacilitator.verifyCalls.length).toBe(0); + }); + }); +}); diff --git a/typescript/packages/core/test/unit/http/x402HTTPClient.hooks.test.ts b/typescript/packages/core/test/unit/http/x402HTTPClient.hooks.test.ts new file mode 100644 index 0000000..142d21f --- /dev/null +++ b/typescript/packages/core/test/unit/http/x402HTTPClient.hooks.test.ts @@ -0,0 +1,146 @@ +import { describe, it, expect } from "vitest"; +import { x402HTTPClient, PaymentRequiredHook } from "../../../src/http/x402HTTPClient"; +import { x402Client } from "../../../src/client/x402Client"; +import { buildPaymentRequired } from "../../mocks"; + +describe("x402HTTPClient", () => { + describe("onPaymentRequired hook", () => { + it("should return this for chaining", () => { + const client = new x402Client(); + const httpClient = new x402HTTPClient(client); + const hook: PaymentRequiredHook = async () => undefined; + + const result = httpClient.onPaymentRequired(hook); + + expect(result).toBe(httpClient); + }); + + it("should allow chaining multiple hooks", () => { + const client = new x402Client(); + const httpClient = new x402HTTPClient(client); + const hook1: PaymentRequiredHook = async () => undefined; + const hook2: PaymentRequiredHook = async () => undefined; + + const result = httpClient.onPaymentRequired(hook1).onPaymentRequired(hook2); + + expect(result).toBe(httpClient); + }); + }); + + describe("handlePaymentRequired", () => { + it("should return null when no hooks are registered", async () => { + const client = new x402Client(); + const httpClient = new x402HTTPClient(client); + const paymentRequired = buildPaymentRequired(); + + const result = await httpClient.handlePaymentRequired(paymentRequired); + + expect(result).toBeNull(); + }); + + it("should return headers when a hook provides them", async () => { + const client = new x402Client(); + const httpClient = new x402HTTPClient(client); + const expectedHeaders = { Authorization: "Bearer token123" }; + const hook: PaymentRequiredHook = async () => ({ headers: expectedHeaders }); + httpClient.onPaymentRequired(hook); + + const paymentRequired = buildPaymentRequired(); + const result = await httpClient.handlePaymentRequired(paymentRequired); + + expect(result).toEqual(expectedHeaders); + }); + + it("should return null when hook returns void", async () => { + const client = new x402Client(); + const httpClient = new x402HTTPClient(client); + const hook: PaymentRequiredHook = async () => undefined; + httpClient.onPaymentRequired(hook); + + const paymentRequired = buildPaymentRequired(); + const result = await httpClient.handlePaymentRequired(paymentRequired); + + expect(result).toBeNull(); + }); + + it("should run hooks in order and first to return headers wins", async () => { + const client = new x402Client(); + const httpClient = new x402HTTPClient(client); + const executionOrder: number[] = []; + + const hook1: PaymentRequiredHook = async () => { + executionOrder.push(1); + return { headers: { "X-Hook": "first" } }; + }; + const hook2: PaymentRequiredHook = async () => { + executionOrder.push(2); + return { headers: { "X-Hook": "second" } }; + }; + + httpClient.onPaymentRequired(hook1).onPaymentRequired(hook2); + + const paymentRequired = buildPaymentRequired(); + const result = await httpClient.handlePaymentRequired(paymentRequired); + + expect(result).toEqual({ "X-Hook": "first" }); + expect(executionOrder).toEqual([1]); // Second hook should not be called + }); + + it("should skip hooks that return void and continue to next", async () => { + const client = new x402Client(); + const httpClient = new x402HTTPClient(client); + const executionOrder: number[] = []; + + const hook1: PaymentRequiredHook = async () => { + executionOrder.push(1); + return undefined; + }; + const hook2: PaymentRequiredHook = async () => { + executionOrder.push(2); + return { headers: { "X-Hook": "second" } }; + }; + + httpClient.onPaymentRequired(hook1).onPaymentRequired(hook2); + + const paymentRequired = buildPaymentRequired(); + const result = await httpClient.handlePaymentRequired(paymentRequired); + + expect(result).toEqual({ "X-Hook": "second" }); + expect(executionOrder).toEqual([1, 2]); + }); + + it("should pass paymentRequired context to hooks", async () => { + const client = new x402Client(); + const httpClient = new x402HTTPClient(client); + const paymentRequired = buildPaymentRequired({ + resource: { url: "https://test.com", description: "Test", mimeType: "text/plain" }, + }); + + let receivedContext: unknown; + const hook: PaymentRequiredHook = async ctx => { + receivedContext = ctx; + return undefined; + }; + httpClient.onPaymentRequired(hook); + + await httpClient.handlePaymentRequired(paymentRequired); + + expect(receivedContext).toEqual({ paymentRequired }); + }); + + it("should return null when all hooks return void", async () => { + const client = new x402Client(); + const httpClient = new x402HTTPClient(client); + + const hook1: PaymentRequiredHook = async () => undefined; + const hook2: PaymentRequiredHook = async () => undefined; + + httpClient.onPaymentRequired(hook1).onPaymentRequired(hook2); + + const paymentRequired = buildPaymentRequired(); + const result = await httpClient.handlePaymentRequired(paymentRequired); + + expect(result).toBeNull(); + }); + }); +}); diff --git a/typescript/packages/core/test/unit/http/x402HTTPResourceServer.hooks.test.ts b/typescript/packages/core/test/unit/http/x402HTTPResourceServer.hooks.test.ts new file mode 100644 index 0000000..d112a32 --- /dev/null +++ b/typescript/packages/core/test/unit/http/x402HTTPResourceServer.hooks.test.ts @@ -0,0 +1,716 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { + x402HTTPResourceServer, + HTTPRequestContext, + HTTPAdapter, + RouteConfig, + ProtectedRequestHook, +} from "../../../src/http/x402HTTPResourceServer"; +import { x402ResourceServer } from "../../../src/server/x402ResourceServer"; +import { + MockFacilitatorClient, + MockSchemeNetworkServer, + buildSupportedResponse, + buildVerifyResponse, + buildSettleResponse, + buildPaymentPayload, + buildPaymentRequirements, +} from "../../mocks"; +import { + Network, + Price, + ResourceServerExtension, + SettleResultContext, + PaymentRequiredContext, +} from "../../../src/types"; +import { decodePaymentRequiredHeader, encodePaymentSignatureHeader } from "../../../src/http"; + +// Mock HTTP Adapter +class MockHTTPAdapter implements HTTPAdapter { + private headers: Record = {}; + + constructor(headers: Record = {}) { + this.headers = headers; + } + + getHeader(name: string): string | undefined { + return this.headers[name.toLowerCase()]; + } + + getMethod(): string { + return "GET"; + } + + getPath(): string { + return "/api/test"; + } + + getUrl(): string { + return "https://example.com/api/test"; + } + + getAcceptHeader(): string { + return "application/json"; + } + + getUserAgent(): string { + return "TestClient/1.0"; + } + + setHeader(name: string, value: string): void { + this.headers[name.toLowerCase()] = value; + } +} + +describe("x402HTTPResourceServer Hooks", () => { + describe("Extension Hooks", () => { + let extensionResourceServer: x402ResourceServer; + let extensionMockFacilitator: MockFacilitatorClient; + let extensionMockScheme: MockSchemeNetworkServer; + + beforeEach(async () => { + // Create a fresh ResourceServer for extension tests to avoid interference + extensionMockFacilitator = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + buildVerifyResponse({ isValid: true }), + ); + + extensionResourceServer = new x402ResourceServer(extensionMockFacilitator); + + extensionMockScheme = new MockSchemeNetworkServer("exact", { + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + extra: {}, + }); + + extensionResourceServer.register("eip155:8453" as Network, extensionMockScheme); + await extensionResourceServer.initialize(); + }); + + describe("enrichSettlementResponse", () => { + it("should enrich settlement response with extensions", async () => { + const receiptExtension: ResourceServerExtension = { + key: "receipt", + enrichSettlementResponse: async ( + _declaration: unknown, + _context: SettleResultContext, + ) => { + // Return just the extension data for this key, not the entire result + return { + receipt: "Receipt", + }; + }, + }; + + extensionResourceServer.registerExtension(receiptExtension); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + extensions: { + receipt: {}, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(extensionResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + // Set up mock to return a successful settlement + extensionMockFacilitator.setSettleResponse( + buildSettleResponse({ + success: true, + transaction: "0x40d32f49a3fa2356275083e348d53fca876df3a140d72a71cf26c9cbaab359d9", + network: "eip155:8453" as Network, + payer: "0xE33A295AF5C90A0649DFBECfDf9D604789B892e2", + }), + ); + + const result = await httpServer.processSettlement( + payload, + requirements, + routes["/api/test"].extensions, + ); + + expect(result.success).toBe(true); + if (result.success) { + // Check that extensions were added + expect((result as any).extensions).toBeDefined(); + expect((result as any).extensions.receipt).toBeDefined(); + expect((result as any).extensions.receipt.receipt).toBe("Receipt"); + expect(result.transaction).toBe( + "0x40d32f49a3fa2356275083e348d53fca876df3a140d72a71cf26c9cbaab359d9", + ); + expect(result.network).toBe("eip155:8453"); + expect(result.payer).toBe("0xE33A295AF5C90A0649DFBECfDf9D604789B892e2"); + } + }); + + it("should handle multiple extensions enriching settlement response", async () => { + const receiptExtension: ResourceServerExtension = { + key: "receipt", + enrichSettlementResponse: async (_declaration: unknown, context: SettleResultContext) => { + return { + ...context.result, + extensions: { + ...(context.result.extensions || {}), + receipt: { receipt: "Receipt" }, + }, + }; + }, + }; + + const attestationExtension: ResourceServerExtension = { + key: "attestation", + enrichSettlementResponse: async (_declaration: unknown, context: SettleResultContext) => { + return { + ...context.result, + extensions: { + ...(context.result.extensions || {}), + attestation: { attestationId: "attest-123" }, + }, + }; + }, + }; + + extensionResourceServer.registerExtension(receiptExtension); + extensionResourceServer.registerExtension(attestationExtension); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + extensions: { + receipt: {}, + attestation: {}, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(extensionResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + extensionMockFacilitator.setSettleResponse(buildSettleResponse({ success: true })); + + const result = await httpServer.processSettlement( + payload, + requirements, + routes["/api/test"].extensions, + ); + + expect(result.success).toBe(true); + if (result.success) { + expect((result as any).extensions).toBeDefined(); + expect((result as any).extensions.receipt).toBeDefined(); + expect((result as any).extensions.attestation).toBeDefined(); + } + }); + + it("should continue processing if extension hook throws error", async () => { + const errorExtension: ResourceServerExtension = { + key: "error-extension", + enrichSettlementResponse: async ( + _declaration: unknown, + _context: SettleResultContext, + ) => { + throw new Error("Extension error"); + }, + }; + + extensionResourceServer.registerExtension(errorExtension); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + extensions: { + "error-extension": {}, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(extensionResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + extensionMockFacilitator.setSettleResponse(buildSettleResponse({ success: true })); + + // Should not throw, should continue with unenriched response + const result = await httpServer.processSettlement( + payload, + requirements, + routes["/api/test"].extensions, + ); + + expect(result.success).toBe(true); + }); + }); + + describe("enrichPaymentRequiredResponse", () => { + it("should enrich PaymentRequired response before sending 402", async () => { + const paymentRequiredExtension: ResourceServerExtension = { + key: "payment-required-enricher", + enrichPaymentRequiredResponse: async ( + _declaration: unknown, + _context: PaymentRequiredContext, + ) => { + // Return just the extension data for this key, not the entire response + return { + metadata: "test-metadata", + }; + }, + }; + + extensionResourceServer.registerExtension(paymentRequiredExtension); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + extensions: { + existing: "existing-value", + "payment-required-enricher": {}, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(extensionResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/test", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("payment-error"); + if (result.type === "payment-error") { + const paymentRequiredHeader = result.response.headers["PAYMENT-REQUIRED"]; + const decoded = decodePaymentRequiredHeader(paymentRequiredHeader); + expect(decoded.extensions).toBeDefined(); + if (decoded.extensions) { + expect(decoded.extensions["payment-required-enricher"]).toBeDefined(); + expect((decoded.extensions["payment-required-enricher"] as any).metadata).toBe( + "test-metadata", + ); + } + } + }); + }); + + describe("Integration: All hooks together", () => { + it("should apply all extension hooks in sequence", async () => { + const allHooksExtension: ResourceServerExtension = { + key: "all-hooks", + enrichPaymentRequiredResponse: async ( + _declaration: unknown, + _context: PaymentRequiredContext, + ) => { + // Return just the extension data for this key + return { + paymentRequiredEnriched: true, + }; + }, + enrichSettlementResponse: async ( + _declaration: unknown, + _context: SettleResultContext, + ) => { + // Return just the extension data for this key + return { + receipt: { + receipt: "Receipt", + }, + }; + }, + }; + + extensionResourceServer.registerExtension(allHooksExtension); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + extensions: { + "all-hooks": {}, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(extensionResourceServer, routes); + + // Test payment requirements enrichment + const adapter1 = new MockHTTPAdapter(); + const context1: HTTPRequestContext = { + adapter: adapter1, + path: "/api/test", + method: "GET", + }; + + const result1 = await httpServer.processHTTPRequest(context1); + expect(result1.type).toBe("payment-error"); + if (result1.type !== "payment-error") { + throw new Error("Expected payment-error"); + } + + const paymentRequiredHeader = result1.response.headers["PAYMENT-REQUIRED"]; + const decoded = decodePaymentRequiredHeader(paymentRequiredHeader); + expect(decoded.extensions).toBeDefined(); + if (decoded.extensions) { + expect((decoded.extensions["all-hooks"] as any).paymentRequiredEnriched).toBe(true); + } + + // Test verification and settlement enrichment + // Get requirements from the PaymentRequired response + const paymentRequired = decoded; + + const payload = buildPaymentPayload({ + accepted: paymentRequired.accepts[0], + resource: paymentRequired.resource, + }); + const paymentHeader = encodePaymentSignatureHeader(payload); + + const adapter2 = new MockHTTPAdapter({ + "payment-signature": paymentHeader, + }); + + const context2: HTTPRequestContext = { + adapter: adapter2, + path: "/api/test", + method: "GET", + }; + + extensionMockFacilitator.setVerifyResponse(buildVerifyResponse({ isValid: true })); + extensionMockFacilitator.setSettleResponse( + buildSettleResponse({ + success: true, + transaction: "0x40d32f49a3fa2356275083e348d53fca876df3a140d72a71cf26c9cbaab359d9", + network: "eip155:8453" as Network, + payer: "0xE33A295AF5C90A0649DFBECfDf9D604789B892e2", + }), + ); + + const result2 = await httpServer.processHTTPRequest(context2); + expect(result2.type).toBe("payment-verified"); + + if (result2.type === "payment-verified") { + const settleResult = await httpServer.processSettlement( + result2.paymentPayload, + result2.paymentRequirements, + routes["/api/test"].extensions, + ); + + expect(settleResult.success).toBe(true); + if (settleResult.success) { + expect((settleResult as any).extensions).toBeDefined(); + expect((settleResult as any).extensions["all-hooks"]).toBeDefined(); + expect((settleResult as any).extensions["all-hooks"].receipt).toBeDefined(); + expect((settleResult as any).extensions["all-hooks"].receipt.receipt).toBe("Receipt"); + } + } + }); + }); + }); + + describe("ProtectedRequestHook", () => { + let ResourceServer: x402ResourceServer; + let mockFacilitator: MockFacilitatorClient; + let mockScheme: MockSchemeNetworkServer; + + const testNetwork = "eip155:8453" as Network; + const testRoutes = { + "/api/protected": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: testNetwork, + }, + }, + }; + + beforeEach(async () => { + mockFacilitator = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: testNetwork }], + }), + buildVerifyResponse({ isValid: true }), + ); + + ResourceServer = new x402ResourceServer(mockFacilitator); + + mockScheme = new MockSchemeNetworkServer("exact", { + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + extra: {}, + }); + + ResourceServer.register(testNetwork, mockScheme); + await ResourceServer.initialize(); + }); + + describe("hook registration", () => { + it("should return this for chaining", () => { + const httpServer = new x402HTTPResourceServer(ResourceServer, testRoutes); + const hook: ProtectedRequestHook = async () => {}; + + const result = httpServer.onProtectedRequest(hook); + + expect(result).toBe(httpServer); + }); + }); + + describe("hook returning void", () => { + it("should continue to payment processing when hook returns void", async () => { + const httpServer = new x402HTTPResourceServer(ResourceServer, testRoutes); + const hook = vi.fn().mockResolvedValue(undefined); + + httpServer.onProtectedRequest(hook); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/protected", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(hook).toHaveBeenCalled(); + expect(result.type).toBe("payment-error"); // No payment provided + if (result.type === "payment-error") { + expect(result.response.status).toBe(402); + } + }); + }); + + describe("hook returning grantAccess", () => { + it("should grant access without payment when hook returns grantAccess", async () => { + const httpServer = new x402HTTPResourceServer(ResourceServer, testRoutes); + const hook = vi.fn().mockResolvedValue({ grantAccess: true }); + + httpServer.onProtectedRequest(hook); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/protected", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(hook).toHaveBeenCalled(); + expect(result.type).toBe("no-payment-required"); + }); + }); + + describe("hook returning abort", () => { + it("should return 403 when hook returns abort", async () => { + const httpServer = new x402HTTPResourceServer(ResourceServer, testRoutes); + const hook = vi.fn().mockResolvedValue({ abort: true, reason: "Access denied" }); + + httpServer.onProtectedRequest(hook); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/protected", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(hook).toHaveBeenCalled(); + expect(result.type).toBe("payment-error"); + if (result.type === "payment-error") { + expect(result.response.status).toBe(403); + expect(result.response.body).toEqual({ error: "Access denied" }); + } + }); + }); + + describe("multiple hooks", () => { + it("should stop at first hook returning grantAccess", async () => { + const httpServer = new x402HTTPResourceServer(ResourceServer, testRoutes); + const hook1 = vi.fn().mockResolvedValue({ grantAccess: true }); + const hook2 = vi.fn().mockResolvedValue({ abort: true, reason: "Should not reach" }); + + httpServer.onProtectedRequest(hook1); + httpServer.onProtectedRequest(hook2); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/protected", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(hook1).toHaveBeenCalled(); + expect(hook2).not.toHaveBeenCalled(); + expect(result.type).toBe("no-payment-required"); + }); + + it("should stop at first hook returning abort", async () => { + const httpServer = new x402HTTPResourceServer(ResourceServer, testRoutes); + const hook1 = vi.fn().mockResolvedValue({ abort: true, reason: "Blocked" }); + const hook2 = vi.fn().mockResolvedValue({ grantAccess: true }); + + httpServer.onProtectedRequest(hook1); + httpServer.onProtectedRequest(hook2); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/protected", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(hook1).toHaveBeenCalled(); + expect(hook2).not.toHaveBeenCalled(); + expect(result.type).toBe("payment-error"); + if (result.type === "payment-error") { + expect(result.response.status).toBe(403); + } + }); + + it("should continue through hooks returning void", async () => { + const httpServer = new x402HTTPResourceServer(ResourceServer, testRoutes); + const hook1 = vi.fn().mockResolvedValue(undefined); + const hook2 = vi.fn().mockResolvedValue(undefined); + const hook3 = vi.fn().mockResolvedValue({ grantAccess: true }); + + httpServer.onProtectedRequest(hook1); + httpServer.onProtectedRequest(hook2); + httpServer.onProtectedRequest(hook3); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/protected", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(hook1).toHaveBeenCalled(); + expect(hook2).toHaveBeenCalled(); + expect(hook3).toHaveBeenCalled(); + expect(result.type).toBe("no-payment-required"); + }); + }); + + describe("hook arguments", () => { + it("should receive HTTPRequestContext", async () => { + const httpServer = new x402HTTPResourceServer(ResourceServer, testRoutes); + let receivedContext: HTTPRequestContext | undefined; + + const hook: ProtectedRequestHook = async context => { + receivedContext = context; + }; + + httpServer.onProtectedRequest(hook); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/protected", + method: "GET", + }; + + await httpServer.processHTTPRequest(context); + + expect(receivedContext).toBeDefined(); + expect(receivedContext?.path).toBe("/api/protected"); + expect(receivedContext?.method).toBe("GET"); + expect(receivedContext?.adapter).toBe(adapter); + }); + + it("should receive RouteConfig", async () => { + const httpServer = new x402HTTPResourceServer(ResourceServer, testRoutes); + let receivedRouteConfig: RouteConfig | undefined; + + const hook: ProtectedRequestHook = async (_context, routeConfig) => { + receivedRouteConfig = routeConfig; + }; + + httpServer.onProtectedRequest(hook); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/protected", + method: "GET", + }; + + await httpServer.processHTTPRequest(context); + + expect(receivedRouteConfig).toBeDefined(); + expect(receivedRouteConfig?.accepts).toBeDefined(); + }); + }); + + describe("hooks on unprotected routes", () => { + it("should not call hooks for routes without payment config", async () => { + const httpServer = new x402HTTPResourceServer(ResourceServer, testRoutes); + const hook = vi.fn().mockResolvedValue({ grantAccess: true }); + + httpServer.onProtectedRequest(hook); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/public", // Not in testRoutes + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(hook).not.toHaveBeenCalled(); + expect(result.type).toBe("no-payment-required"); + }); + }); + }); +}); diff --git a/typescript/packages/core/test/unit/http/x402HTTPResourceServer.initialize.test.ts b/typescript/packages/core/test/unit/http/x402HTTPResourceServer.initialize.test.ts new file mode 100644 index 0000000..839a0f0 --- /dev/null +++ b/typescript/packages/core/test/unit/http/x402HTTPResourceServer.initialize.test.ts @@ -0,0 +1,298 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { x402ResourceServer } from "../../../src/server/x402ResourceServer"; +import { + x402HTTPResourceServer, + RouteConfigurationError, +} from "../../../src/http/x402HTTPResourceServer"; +import { RoutesConfig } from "../../../src/http/x402HTTPResourceServer"; +import { Network } from "../../../src/types"; +import { + MockFacilitatorClient, + MockSchemeNetworkServer, + buildSupportedResponse, +} from "../../mocks"; + +describe("x402HTTPResourceServer.initialize", () => { + let server: x402ResourceServer; + let mockClient: MockFacilitatorClient; + let mockScheme: MockSchemeNetworkServer; + + const testNetwork = "eip155:84532" as Network; + const testScheme = "exact"; + + beforeEach(() => { + mockScheme = new MockSchemeNetworkServer(testScheme); + }); + + describe("with properly configured server", () => { + beforeEach(() => { + mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: testScheme, network: testNetwork }], + }), + ); + server = new x402ResourceServer(mockClient); + server.register(testNetwork, mockScheme); + }); + + it("should initialize successfully with valid routes", async () => { + const routes: RoutesConfig = { + "GET /api/data": { + accepts: { + scheme: testScheme, + payTo: "0x123", + price: "$0.01", + network: testNetwork, + }, + description: "Test endpoint", + }, + }; + + const httpServer = new x402HTTPResourceServer(server, routes); + + await expect(httpServer.initialize()).resolves.not.toThrow(); + }); + + it("should initialize with array of payment options", async () => { + const routes: RoutesConfig = { + "GET /api/data": { + accepts: [ + { + scheme: testScheme, + payTo: "0x123", + price: "$0.01", + network: testNetwork, + }, + ], + description: "Test endpoint", + }, + }; + + const httpServer = new x402HTTPResourceServer(server, routes); + + await expect(httpServer.initialize()).resolves.not.toThrow(); + }); + + it("should initialize with single route config format", async () => { + const routes: RoutesConfig = { + accepts: { + scheme: testScheme, + payTo: "0x123", + price: "$0.01", + network: testNetwork, + }, + description: "Test endpoint", + }; + + const httpServer = new x402HTTPResourceServer(server, routes); + + await expect(httpServer.initialize()).resolves.not.toThrow(); + }); + }); + + describe("with missing scheme registration", () => { + beforeEach(() => { + mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: testScheme, network: testNetwork }], + }), + ); + server = new x402ResourceServer(mockClient); + // Note: NOT registering the scheme + }); + + it("should throw RouteConfigurationError for unregistered scheme", async () => { + const routes: RoutesConfig = { + "GET /api/data": { + accepts: { + scheme: testScheme, + payTo: "0x123", + price: "$0.01", + network: testNetwork, + }, + description: "Test endpoint", + }, + }; + + const httpServer = new x402HTTPResourceServer(server, routes); + + await expect(httpServer.initialize()).rejects.toThrow(RouteConfigurationError); + + try { + await httpServer.initialize(); + } catch (error) { + expect(error).toBeInstanceOf(RouteConfigurationError); + const configError = error as RouteConfigurationError; + expect(configError.errors).toHaveLength(1); + expect(configError.errors[0].reason).toBe("missing_scheme"); + expect(configError.errors[0].routePattern).toBe("GET /api/data"); + expect(configError.errors[0].message).toContain("No scheme implementation registered"); + } + }); + }); + + describe("with missing facilitator support", () => { + beforeEach(() => { + mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "other-scheme", network: testNetwork }], + }), + ); + server = new x402ResourceServer(mockClient); + server.register(testNetwork, mockScheme); + }); + + it("should throw RouteConfigurationError for unsupported facilitator", async () => { + const routes: RoutesConfig = { + "POST /api/payment": { + accepts: { + scheme: testScheme, + payTo: "0x123", + price: "$0.01", + network: testNetwork, + }, + description: "Test endpoint", + }, + }; + + const httpServer = new x402HTTPResourceServer(server, routes); + + await expect(httpServer.initialize()).rejects.toThrow(RouteConfigurationError); + + try { + await httpServer.initialize(); + } catch (error) { + expect(error).toBeInstanceOf(RouteConfigurationError); + const configError = error as RouteConfigurationError; + expect(configError.errors).toHaveLength(1); + expect(configError.errors[0].reason).toBe("missing_facilitator"); + expect(configError.errors[0].routePattern).toBe("POST /api/payment"); + expect(configError.errors[0].message).toContain("Facilitator does not support"); + } + }); + }); + + describe("with multiple routes and payment options", () => { + const solanaNetwork = "solana:mainnet" as Network; + let solanaScheme: MockSchemeNetworkServer; + + beforeEach(() => { + solanaScheme = new MockSchemeNetworkServer("exact"); + mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [ + { x402Version: 2, scheme: testScheme, network: testNetwork }, + { x402Version: 2, scheme: "exact", network: solanaNetwork }, + ], + }), + ); + server = new x402ResourceServer(mockClient); + server.register(testNetwork, mockScheme); + server.register(solanaNetwork, solanaScheme); + }); + + it("should validate all routes and payment options", async () => { + const routes: RoutesConfig = { + "GET /api/data": { + accepts: [ + { + scheme: testScheme, + payTo: "0x123", + price: "$0.01", + network: testNetwork, + }, + { + scheme: "exact", + payTo: "solana_address", + price: "$0.01", + network: solanaNetwork, + }, + ], + description: "Multi-chain endpoint", + }, + "POST /api/other": { + accepts: { + scheme: testScheme, + payTo: "0x456", + price: "$0.02", + network: testNetwork, + }, + description: "Another endpoint", + }, + }; + + const httpServer = new x402HTTPResourceServer(server, routes); + + await expect(httpServer.initialize()).resolves.not.toThrow(); + }); + + it("should collect errors from multiple routes", async () => { + const unsupportedNetwork = "unsupported:network" as Network; + + const routes: RoutesConfig = { + "GET /api/valid": { + accepts: { + scheme: testScheme, + payTo: "0x123", + price: "$0.01", + network: testNetwork, + }, + description: "Valid endpoint", + }, + "GET /api/invalid": { + accepts: { + scheme: testScheme, + payTo: "0x456", + price: "$0.01", + network: unsupportedNetwork, + }, + description: "Invalid endpoint", + }, + }; + + const httpServer = new x402HTTPResourceServer(server, routes); + + try { + await httpServer.initialize(); + expect.fail("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(RouteConfigurationError); + const configError = error as RouteConfigurationError; + expect(configError.errors).toHaveLength(1); + expect(configError.errors[0].routePattern).toBe("GET /api/invalid"); + } + }); + }); + + describe("RouteConfigurationError", () => { + it("should have formatted error message", async () => { + mockClient = new MockFacilitatorClient(buildSupportedResponse()); + server = new x402ResourceServer(mockClient); + + const routes: RoutesConfig = { + "GET /api/test": { + accepts: { + scheme: "exact", + payTo: "0x123", + price: "$0.01", + network: "eip155:84532" as Network, + }, + description: "Test", + }, + }; + + const httpServer = new x402HTTPResourceServer(server, routes); + + try { + await httpServer.initialize(); + expect.fail("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(RouteConfigurationError); + const configError = error as RouteConfigurationError; + expect(configError.name).toBe("RouteConfigurationError"); + expect(configError.message).toContain("x402 Route Configuration Errors:"); + expect(configError.message).toContain("GET /api/test"); + } + }); + }); +}); diff --git a/typescript/packages/core/test/unit/http/x402HTTPResourceService.test.ts b/typescript/packages/core/test/unit/http/x402HTTPResourceService.test.ts new file mode 100644 index 0000000..b694130 --- /dev/null +++ b/typescript/packages/core/test/unit/http/x402HTTPResourceService.test.ts @@ -0,0 +1,816 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { + x402HTTPResourceServer, + HTTPRequestContext, + HTTPAdapter, +} from "../../../src/http/x402HTTPResourceServer"; +import { x402ResourceServer } from "../../../src/server/x402ResourceServer"; +import { + MockFacilitatorClient, + MockSchemeNetworkServer, + buildSupportedResponse, + buildVerifyResponse, + buildPaymentPayload, + buildPaymentRequirements, +} from "../../mocks"; +import { Network, Price } from "../../../src/types"; + +// Mock HTTP Adapter +/** + * + */ +class MockHTTPAdapter implements HTTPAdapter { + private headers: Record = {}; + + /** + * + * @param headers + */ + constructor(headers: Record = {}) { + this.headers = headers; + } + + /** + * + * @param name + */ + getHeader(name: string): string | undefined { + return this.headers[name.toLowerCase()]; + } + + /** + * + */ + getMethod(): string { + return "GET"; + } + + /** + * + */ + getPath(): string { + return "/api/test"; + } + + /** + * + */ + getUrl(): string { + return "https://example.com/api/test"; + } + + /** + * + */ + getAcceptHeader(): string { + return "application/json"; + } + + /** + * + */ + getUserAgent(): string { + return "TestClient/1.0"; + } + + /** + * + * @param name + * @param value + */ + setHeader(name: string, value: string): void { + this.headers[name.toLowerCase()] = value; + } +} + +describe("x402HTTPResourceServer", () => { + let ResourceServer: x402ResourceServer; + let mockFacilitator: MockFacilitatorClient; + let mockScheme: MockSchemeNetworkServer; + + beforeEach(async () => { + mockFacilitator = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + buildVerifyResponse({ isValid: true }), + ); + + ResourceServer = new x402ResourceServer(mockFacilitator); + + mockScheme = new MockSchemeNetworkServer("exact", { + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + extra: {}, + }); + + ResourceServer.register("eip155:8453" as Network, mockScheme); + await ResourceServer.initialize(); + }); + + describe("Construction", () => { + it("should accept ResourceServer and routes via composition", () => { + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + expect(httpServer).toBeDefined(); + }); + + it("should compile single route config", () => { + const singleRoute = { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: 1.0 as Price, + network: "eip155:8453" as Network, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, singleRoute); + + expect(httpServer).toBeDefined(); + }); + + it("should compile multiple route configs", () => { + const routes = { + "GET /api/route1": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: 1.0 as Price, + network: "eip155:8453" as Network, + }, + }, + "POST /api/route2": { + accepts: { + scheme: "exact", + payTo: "0xdef", + price: 2.0 as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + expect(httpServer).toBeDefined(); + }); + }); + + describe("Dynamic pricing", () => { + it("should resolve dynamic price function", async () => { + let contextReceived: HTTPRequestContext | null = null; + + const routes = { + "/api/dynamic": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: async (context: HTTPRequestContext) => { + contextReceived = context; + return "$5.00" as Price; + }, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/dynamic", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(contextReceived).toBeDefined(); + expect(contextReceived?.path).toBe("/api/dynamic"); + expect(result.type).toBe("payment-error"); // No payment provided + }); + + it("should use static price if not a function", async () => { + const routes = { + "/api/static": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/static", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("payment-error"); + }); + + it("should have access to request headers in dynamic price", async () => { + let headerValue: string | undefined; + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: async (context: HTTPRequestContext) => { + headerValue = context.adapter.getHeader("x-api-key"); + return "$1.00" as Price; + }, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter({ "x-api-key": "secret123" }); + const context: HTTPRequestContext = { + adapter, + path: "/api/test", + method: "GET", + }; + + await httpServer.processHTTPRequest(context); + + expect(headerValue).toBe("secret123"); + }); + }); + + describe("Dynamic payTo", () => { + it("should resolve dynamic payTo function", async () => { + let contextReceived: HTTPRequestContext | null = null; + + const routes = { + "/api/dynamic": { + accepts: { + scheme: "exact", + payTo: async (context: HTTPRequestContext) => { + contextReceived = context; + return "0xdynamic"; + }, + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/dynamic", + method: "GET", + }; + + await httpServer.processHTTPRequest(context); + + expect(contextReceived).toBeDefined(); + expect(contextReceived?.path).toBe("/api/dynamic"); + }); + + it("should use static payTo if not a function", async () => { + const routes = { + "/api/static": { + accepts: { + scheme: "exact", + payTo: "0xstatic", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/static", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("payment-error"); + }); + }); + + describe("Route matching", () => { + it("should match exact path", async () => { + const routes = { + "/api/exact": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/exact", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("payment-error"); // Route matched, no payment + }); + + it("should match wildcard paths", async () => { + const routes = { + "/api/*": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/anything", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("payment-error"); // Route matched + }); + + it("should return no-payment-required for unmatched routes", async () => { + const routes = { + "/api/protected": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/public", // Different path + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("no-payment-required"); + }); + + it("should match HTTP methods", async () => { + const routes = { + "POST /api/create": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + adapter.getMethod = () => "POST"; + + const context: HTTPRequestContext = { + adapter, + path: "/api/create", + method: "POST", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("payment-error"); // Route matched + }); + + it("should not match wrong HTTP method", async () => { + const routes = { + "POST /api/create": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/create", + method: "GET", // Wrong method + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("no-payment-required"); + }); + + describe("malformed percent-encoding", () => { + it("should require payment for path with trailing malformed %", async () => { + const routes = { + "/paywall/[param]": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/paywall/test%", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + expect(result.type).toBe("payment-error"); + }); + + it("should require payment for path with malformed %c0 sequence", async () => { + const routes = { + "/api/*": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/resource%c0", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + expect(result.type).toBe("payment-error"); + }); + + it("should require payment for path with multiple malformed sequences", async () => { + const routes = { + "/protected/*": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/protected/data%c0%c1%", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + expect(result.type).toBe("payment-error"); + }); + + it("should correctly identify requiresPayment for malformed paths", async () => { + const routes = { + "/paywall/[id]": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/paywall/test%", + method: "GET", + }; + + expect(httpServer.requiresPayment(context)).toBe(true); + }); + }); + }); + + describe("Payment processing", () => { + it("should return payment-error if no payment provided", async () => { + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/test", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("payment-error"); + if (result.type === "payment-error") { + expect(result.response.status).toBe(402); + expect(result.response.headers["PAYMENT-REQUIRED"]).toBeDefined(); + } + }); + + it("should return 412 Precondition Failed for permit2_allowance_required error", async () => { + // Override mock to simulate permit2 allowance required error + mockFacilitator.setVerifyResponse({ + isValid: false, + invalidReason: "permit2_allowance_required", + }); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + // Build requirements that match the route exactly (including amount/asset from mock scheme) + const matchingRequirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + payTo: "0xabc", + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + maxTimeoutSeconds: 300, + extra: {}, + }); + + // Create payment payload with matching requirements + const payload = buildPaymentPayload({ + accepted: matchingRequirements, + }); + + // Use proper encoding for payment header + const { encodePaymentSignatureHeader } = await import("../../../src/http"); + const paymentHeader = encodePaymentSignatureHeader(payload); + + const adapter = new MockHTTPAdapter({ + "payment-signature": paymentHeader, + }); + + const context: HTTPRequestContext = { + adapter, + path: "/api/test", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + // Verify that the mock was called + expect(mockFacilitator.verifyCalls.length).toBe(1); + + expect(result.type).toBe("payment-error"); + if (result.type === "payment-error") { + // Should return 412 for permit2_allowance_required + expect(result.response.status).toBe(412); + expect(result.response.headers["PAYMENT-REQUIRED"]).toBeDefined(); + } + }); + + it("should delegate verification to resource service", async () => { + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + // Create valid payment header + const adapter = new MockHTTPAdapter({ + "payment-signature": "valid_payment_signature", + }); + + const context: HTTPRequestContext = { + adapter, + path: "/api/test", + method: "GET", + }; + + // This would normally fail because we don't have a real payment, + // but it shows delegation happens + await httpServer.processHTTPRequest(context); + + // Verification was attempted (may fail on decoding, but that's ok for this test) + }); + }); + + describe("Settlement processing", () => { + it("should return success with headers on successful settlement", async () => { + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + const result = await httpServer.processSettlement(payload, requirements); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.headers["PAYMENT-RESPONSE"]).toBeDefined(); + } + expect(mockFacilitator.settleCalls.length).toBe(1); + }); + + it("should return failure when settlement fails", async () => { + // Override mock to simulate failure + mockFacilitator.settle = async () => ({ + success: false, + errorReason: "Insufficient funds", + transaction: "", + network: "eip155:8453" as Network, + }); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + const result = await httpServer.processSettlement(payload, requirements); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.errorReason).toBe("Insufficient funds"); + } + }); + }); + + describe("Browser detection", () => { + it("should detect web browser from accept header and user agent", async () => { + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + adapter.getAcceptHeader = () => "text/html,application/xhtml+xml"; + adapter.getUserAgent = () => "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"; + + const context: HTTPRequestContext = { + adapter, + path: "/api/test", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + // Should return HTML paywall for browsers + if (result.type === "payment-error") { + expect(result.response.isHtml).toBe(true); + } + }); + + it("should not treat API clients as browsers", async () => { + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + adapter.getAcceptHeader = () => "application/json"; + adapter.getUserAgent = () => "TestClient/1.0"; + + const context: HTTPRequestContext = { + adapter, + path: "/api/test", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + // Should return JSON response for API clients + if (result.type === "payment-error") { + expect(result.response.isHtml).toBeFalsy(); + } + }); + }); +}); diff --git a/typescript/packages/core/test/unit/schemas/schemas.test.ts b/typescript/packages/core/test/unit/schemas/schemas.test.ts new file mode 100644 index 0000000..84dfb28 --- /dev/null +++ b/typescript/packages/core/test/unit/schemas/schemas.test.ts @@ -0,0 +1,609 @@ +import { describe, it, expect } from "vitest"; +import { + // V1 schemas + PaymentRequirementsV1Schema, + PaymentRequiredV1Schema, + PaymentPayloadV1Schema, + isPaymentRequirementsV1, + isPaymentRequiredV1, + isPaymentPayloadV1, + // V2 schemas + PaymentRequirementsV2Schema, + PaymentRequiredV2Schema, + PaymentPayloadV2Schema, + isPaymentRequirementsV2, + isPaymentRequiredV2, + isPaymentPayloadV2, + // Union schemas + PaymentRequirementsSchema, + PaymentRequiredSchema, + PaymentPayloadSchema, + isPaymentRequirements, + isPaymentRequired, + isPaymentPayload, + // Validation functions + parsePaymentRequired, + parsePaymentRequirements, + parsePaymentPayload, + validatePaymentRequired, + validatePaymentRequirements, + validatePaymentPayload, +} from "../../../src/schemas"; + +describe("x402 Schemas", () => { + // ============================================================================ + // V1 Test Data + // ============================================================================ + const validPaymentRequirementsV1 = { + scheme: "exact", + network: "base-sepolia", + maxAmountRequired: "10000", + resource: "https://api.example.com/premium-data", + description: "Access to premium market data", + mimeType: "application/json", + outputSchema: null, + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + maxTimeoutSeconds: 60, + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + extra: { name: "USDC", version: "2" }, + }; + + const validPaymentRequiredV1 = { + x402Version: 1 as const, + error: "Payment required", + accepts: [validPaymentRequirementsV1], + }; + + const validPaymentPayloadV1 = { + x402Version: 1 as const, + scheme: "exact", + network: "base-sepolia", + payload: { + signature: "0x1234", + authorization: { from: "0xabc", to: "0xdef" }, + }, + }; + + // ============================================================================ + // V2 Test Data + // ============================================================================ + const validPaymentRequirementsV2 = { + scheme: "exact", + network: "eip155:84532", + amount: "10000", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + maxTimeoutSeconds: 60, + extra: { name: "USDC", version: "2" }, + }; + + const validPaymentRequiredV2 = { + x402Version: 2 as const, + error: "Payment required", + resource: { + url: "https://api.example.com/premium-data", + description: "Access to premium market data", + mimeType: "application/json", + }, + accepts: [validPaymentRequirementsV2], + extensions: {}, + }; + + const validPaymentPayloadV2 = { + x402Version: 2 as const, + resource: { + url: "https://api.example.com/premium-data", + description: "Access to premium market data", + }, + accepted: validPaymentRequirementsV2, + payload: { + signature: "0x1234", + authorization: { from: "0xabc", to: "0xdef" }, + }, + extensions: {}, + }; + + // ============================================================================ + // V1 Schema Tests + // ============================================================================ + describe("V1 Schemas", () => { + describe("PaymentRequirementsV1Schema", () => { + it("should validate valid PaymentRequirementsV1", () => { + const result = PaymentRequirementsV1Schema.safeParse(validPaymentRequirementsV1); + expect(result.success).toBe(true); + }); + + it("should reject missing required fields", () => { + const invalid = { ...validPaymentRequirementsV1 }; + delete (invalid as Record).scheme; + const result = PaymentRequirementsV1Schema.safeParse(invalid); + expect(result.success).toBe(false); + }); + + it("should reject empty scheme", () => { + const invalid = { ...validPaymentRequirementsV1, scheme: "" }; + const result = PaymentRequirementsV1Schema.safeParse(invalid); + expect(result.success).toBe(false); + }); + + it("should reject non-positive maxTimeoutSeconds", () => { + const invalid = { ...validPaymentRequirementsV1, maxTimeoutSeconds: 0 }; + const result = PaymentRequirementsV1Schema.safeParse(invalid); + expect(result.success).toBe(false); + }); + }); + + describe("PaymentRequiredV1Schema", () => { + it("should validate valid PaymentRequiredV1", () => { + const result = PaymentRequiredV1Schema.safeParse(validPaymentRequiredV1); + expect(result.success).toBe(true); + }); + + it("should reject wrong x402Version", () => { + const invalid = { ...validPaymentRequiredV1, x402Version: 2 }; + const result = PaymentRequiredV1Schema.safeParse(invalid); + expect(result.success).toBe(false); + }); + + it("should reject empty accepts array", () => { + const invalid = { ...validPaymentRequiredV1, accepts: [] }; + const result = PaymentRequiredV1Schema.safeParse(invalid); + expect(result.success).toBe(false); + }); + }); + + describe("PaymentPayloadV1Schema", () => { + it("should validate valid PaymentPayloadV1", () => { + const result = PaymentPayloadV1Schema.safeParse(validPaymentPayloadV1); + expect(result.success).toBe(true); + }); + + it("should reject wrong x402Version", () => { + const invalid = { ...validPaymentPayloadV1, x402Version: 2 }; + const result = PaymentPayloadV1Schema.safeParse(invalid); + expect(result.success).toBe(false); + }); + }); + + describe("V1 Type Guards", () => { + it("isPaymentRequirementsV1 should return true for valid V1 requirements", () => { + expect(isPaymentRequirementsV1(validPaymentRequirementsV1)).toBe(true); + }); + + it("isPaymentRequirementsV1 should return false for V2 requirements", () => { + expect(isPaymentRequirementsV1(validPaymentRequirementsV2)).toBe(false); + }); + + it("isPaymentRequiredV1 should return true for valid V1", () => { + expect(isPaymentRequiredV1(validPaymentRequiredV1)).toBe(true); + }); + + it("isPaymentRequiredV1 should return false for V2", () => { + expect(isPaymentRequiredV1(validPaymentRequiredV2)).toBe(false); + }); + + it("isPaymentPayloadV1 should return true for valid V1", () => { + expect(isPaymentPayloadV1(validPaymentPayloadV1)).toBe(true); + }); + + it("isPaymentPayloadV1 should return false for V2", () => { + expect(isPaymentPayloadV1(validPaymentPayloadV2)).toBe(false); + }); + }); + }); + + // ============================================================================ + // V2 Schema Tests + // ============================================================================ + describe("V2 Schemas", () => { + describe("PaymentRequirementsV2Schema", () => { + it("should validate valid PaymentRequirementsV2", () => { + const result = PaymentRequirementsV2Schema.safeParse(validPaymentRequirementsV2); + expect(result.success).toBe(true); + }); + + it("should reject missing required fields", () => { + const invalid = { ...validPaymentRequirementsV2 }; + delete (invalid as Record).amount; + const result = PaymentRequirementsV2Schema.safeParse(invalid); + expect(result.success).toBe(false); + }); + + it("should allow missing optional extra field", () => { + const withoutExtra = { ...validPaymentRequirementsV2 }; + delete (withoutExtra as Record).extra; + const result = PaymentRequirementsV2Schema.safeParse(withoutExtra); + expect(result.success).toBe(true); + }); + }); + + describe("PaymentRequiredV2Schema", () => { + it("should validate valid PaymentRequiredV2", () => { + const result = PaymentRequiredV2Schema.safeParse(validPaymentRequiredV2); + expect(result.success).toBe(true); + }); + + it("should reject wrong x402Version", () => { + const invalid = { ...validPaymentRequiredV2, x402Version: 1 }; + const result = PaymentRequiredV2Schema.safeParse(invalid); + expect(result.success).toBe(false); + }); + + it("should reject missing resource", () => { + const invalid = { ...validPaymentRequiredV2 }; + delete (invalid as Record).resource; + const result = PaymentRequiredV2Schema.safeParse(invalid); + expect(result.success).toBe(false); + }); + + it("should reject resource without url", () => { + const invalid = { + ...validPaymentRequiredV2, + resource: { description: "test" }, + }; + const result = PaymentRequiredV2Schema.safeParse(invalid); + expect(result.success).toBe(false); + }); + }); + + describe("PaymentPayloadV2Schema", () => { + it("should validate valid PaymentPayloadV2", () => { + const result = PaymentPayloadV2Schema.safeParse(validPaymentPayloadV2); + expect(result.success).toBe(true); + }); + + it("should reject wrong x402Version", () => { + const invalid = { ...validPaymentPayloadV2, x402Version: 1 }; + const result = PaymentPayloadV2Schema.safeParse(invalid); + expect(result.success).toBe(false); + }); + + it("should allow optional resource", () => { + const withoutResource = { ...validPaymentPayloadV2 }; + delete (withoutResource as Record).resource; + const result = PaymentPayloadV2Schema.safeParse(withoutResource); + expect(result.success).toBe(true); + }); + }); + + describe("V2 Type Guards", () => { + it("isPaymentRequirementsV2 should return true for valid V2 requirements", () => { + expect(isPaymentRequirementsV2(validPaymentRequirementsV2)).toBe(true); + }); + + it("isPaymentRequirementsV2 should return false for V1 requirements", () => { + expect(isPaymentRequirementsV2(validPaymentRequirementsV1)).toBe(false); + }); + + it("isPaymentRequiredV2 should return true for valid V2", () => { + expect(isPaymentRequiredV2(validPaymentRequiredV2)).toBe(true); + }); + + it("isPaymentRequiredV2 should return false for V1", () => { + expect(isPaymentRequiredV2(validPaymentRequiredV1)).toBe(false); + }); + + it("isPaymentPayloadV2 should return true for valid V2", () => { + expect(isPaymentPayloadV2(validPaymentPayloadV2)).toBe(true); + }); + + it("isPaymentPayloadV2 should return false for V1", () => { + expect(isPaymentPayloadV2(validPaymentPayloadV1)).toBe(false); + }); + }); + }); + + // ============================================================================ + // Union Schema Tests + // ============================================================================ + describe("Union Schemas", () => { + describe("PaymentRequirementsSchema", () => { + it("should accept V1 requirements", () => { + const result = PaymentRequirementsSchema.safeParse(validPaymentRequirementsV1); + expect(result.success).toBe(true); + }); + + it("should accept V2 requirements", () => { + const result = PaymentRequirementsSchema.safeParse(validPaymentRequirementsV2); + expect(result.success).toBe(true); + }); + + it("should reject invalid requirements", () => { + const result = PaymentRequirementsSchema.safeParse({ invalid: true }); + expect(result.success).toBe(false); + }); + }); + + describe("PaymentRequiredSchema (discriminated union)", () => { + it("should accept V1 PaymentRequired", () => { + const result = PaymentRequiredSchema.safeParse(validPaymentRequiredV1); + expect(result.success).toBe(true); + }); + + it("should accept V2 PaymentRequired", () => { + const result = PaymentRequiredSchema.safeParse(validPaymentRequiredV2); + expect(result.success).toBe(true); + }); + + it("should reject invalid x402Version", () => { + const invalid = { ...validPaymentRequiredV1, x402Version: 3 }; + const result = PaymentRequiredSchema.safeParse(invalid); + expect(result.success).toBe(false); + }); + }); + + describe("PaymentPayloadSchema (discriminated union)", () => { + it("should accept V1 PaymentPayload", () => { + const result = PaymentPayloadSchema.safeParse(validPaymentPayloadV1); + expect(result.success).toBe(true); + }); + + it("should accept V2 PaymentPayload", () => { + const result = PaymentPayloadSchema.safeParse(validPaymentPayloadV2); + expect(result.success).toBe(true); + }); + + it("should reject invalid x402Version", () => { + const invalid = { ...validPaymentPayloadV1, x402Version: 3 }; + const result = PaymentPayloadSchema.safeParse(invalid); + expect(result.success).toBe(false); + }); + }); + + describe("Union Type Guards", () => { + it("isPaymentRequired should return true for V1", () => { + expect(isPaymentRequired(validPaymentRequiredV1)).toBe(true); + }); + + it("isPaymentRequired should return true for V2", () => { + expect(isPaymentRequired(validPaymentRequiredV2)).toBe(true); + }); + + it("isPaymentRequired should return false for invalid", () => { + expect(isPaymentRequired({ invalid: true })).toBe(false); + }); + + it("isPaymentRequirements should return true for V1", () => { + expect(isPaymentRequirements(validPaymentRequirementsV1)).toBe(true); + }); + + it("isPaymentRequirements should return true for V2", () => { + expect(isPaymentRequirements(validPaymentRequirementsV2)).toBe(true); + }); + + it("isPaymentPayload should return true for V1", () => { + expect(isPaymentPayload(validPaymentPayloadV1)).toBe(true); + }); + + it("isPaymentPayload should return true for V2", () => { + expect(isPaymentPayload(validPaymentPayloadV2)).toBe(true); + }); + }); + }); + + // ============================================================================ + // Validation Function Tests + // ============================================================================ + describe("Validation Functions", () => { + describe("parsePaymentRequired", () => { + it("should return success for valid V1", () => { + const result = parsePaymentRequired(validPaymentRequiredV1); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.x402Version).toBe(1); + } + }); + + it("should return success for valid V2", () => { + const result = parsePaymentRequired(validPaymentRequiredV2); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.x402Version).toBe(2); + } + }); + + it("should return error for invalid data", () => { + const result = parsePaymentRequired({ invalid: true }); + expect(result.success).toBe(false); + }); + }); + + describe("validatePaymentRequired", () => { + it("should return validated data for valid input", () => { + const result = validatePaymentRequired(validPaymentRequiredV2); + expect(result.x402Version).toBe(2); + }); + + it("should throw for invalid input", () => { + expect(() => validatePaymentRequired({ invalid: true })).toThrow(); + }); + }); + + describe("parsePaymentRequirements", () => { + it("should return success for valid V1", () => { + const result = parsePaymentRequirements(validPaymentRequirementsV1); + expect(result.success).toBe(true); + }); + + it("should return success for valid V2", () => { + const result = parsePaymentRequirements(validPaymentRequirementsV2); + expect(result.success).toBe(true); + }); + }); + + describe("validatePaymentRequirements", () => { + it("should return validated data for valid input", () => { + const result = validatePaymentRequirements(validPaymentRequirementsV2); + expect(result.scheme).toBe("exact"); + }); + + it("should throw for invalid input", () => { + expect(() => validatePaymentRequirements({ invalid: true })).toThrow(); + }); + }); + + describe("parsePaymentPayload", () => { + it("should return success for valid V1", () => { + const result = parsePaymentPayload(validPaymentPayloadV1); + expect(result.success).toBe(true); + }); + + it("should return success for valid V2", () => { + const result = parsePaymentPayload(validPaymentPayloadV2); + expect(result.success).toBe(true); + }); + }); + + describe("validatePaymentPayload", () => { + it("should return validated data for valid input", () => { + const result = validatePaymentPayload(validPaymentPayloadV2); + expect(result.x402Version).toBe(2); + }); + + it("should throw for invalid input", () => { + expect(() => validatePaymentPayload({ invalid: true })).toThrow(); + }); + }); + }); + + // ============================================================================ + // Edge Cases + // ============================================================================ + describe("Edge Cases", () => { + it("should handle null values gracefully", () => { + expect(isPaymentRequired(null)).toBe(false); + expect(isPaymentRequirements(null)).toBe(false); + expect(isPaymentPayload(null)).toBe(false); + }); + + it("should handle undefined values gracefully", () => { + expect(isPaymentRequired(undefined)).toBe(false); + expect(isPaymentRequirements(undefined)).toBe(false); + expect(isPaymentPayload(undefined)).toBe(false); + }); + + it("should handle non-object values gracefully", () => { + expect(isPaymentRequired("string")).toBe(false); + expect(isPaymentRequired(123)).toBe(false); + expect(isPaymentRequired([])).toBe(false); + }); + + it("should allow extra unknown fields (loose validation)", () => { + const withExtra = { + ...validPaymentRequirementsV2, + unknownField: "should be ignored", + anotherUnknown: { nested: true }, + }; + const result = PaymentRequirementsV2Schema.safeParse(withExtra); + expect(result.success).toBe(true); + }); + + it("should validate V1 outputSchema can be null", () => { + const withNullSchema = { + ...validPaymentRequirementsV1, + outputSchema: null, + }; + const result = PaymentRequirementsV1Schema.safeParse(withNullSchema); + expect(result.success).toBe(true); + }); + + it("should validate V1 outputSchema can be an object", () => { + const withSchema = { + ...validPaymentRequirementsV1, + outputSchema: { type: "object", properties: {} }, + }; + const result = PaymentRequirementsV1Schema.safeParse(withSchema); + expect(result.success).toBe(true); + }); + }); + + // ============================================================================ + // Network Schema Tests + // ============================================================================ + describe("Network Schema Validation", () => { + describe("V1 Network (loose)", () => { + it("should accept any non-empty string for V1", () => { + const v1WithSimpleNetwork = { + ...validPaymentRequirementsV1, + network: "base-sepolia", + }; + const result = PaymentRequirementsV1Schema.safeParse(v1WithSimpleNetwork); + expect(result.success).toBe(true); + }); + + it("should accept CAIP-2 format for V1", () => { + const v1WithCaip2 = { + ...validPaymentRequirementsV1, + network: "eip155:84532", + }; + const result = PaymentRequirementsV1Schema.safeParse(v1WithCaip2); + expect(result.success).toBe(true); + }); + + it("should reject empty string for V1", () => { + const v1WithEmpty = { + ...validPaymentRequirementsV1, + network: "", + }; + const result = PaymentRequirementsV1Schema.safeParse(v1WithEmpty); + expect(result.success).toBe(false); + }); + }); + + describe("V2 Network (CAIP-2 required)", () => { + it("should accept valid CAIP-2 format for V2", () => { + const v2Valid = { + ...validPaymentRequirementsV2, + network: "eip155:84532", + }; + const result = PaymentRequirementsV2Schema.safeParse(v2Valid); + expect(result.success).toBe(true); + }); + + it("should accept Solana CAIP-2 format for V2", () => { + const v2Solana = { + ...validPaymentRequirementsV2, + network: "solana:devnet", + }; + const result = PaymentRequirementsV2Schema.safeParse(v2Solana); + expect(result.success).toBe(true); + }); + + it("should reject V1-style network names for V2", () => { + const v2WithV1Style = { + ...validPaymentRequirementsV2, + network: "base-sepolia", + }; + const result = PaymentRequirementsV2Schema.safeParse(v2WithV1Style); + expect(result.success).toBe(false); + }); + + it("should reject network without colon for V2", () => { + const v2NoColon = { + ...validPaymentRequirementsV2, + network: "ethereum", + }; + const result = PaymentRequirementsV2Schema.safeParse(v2NoColon); + expect(result.success).toBe(false); + }); + + it("should reject network too short for V2", () => { + const v2TooShort = { + ...validPaymentRequirementsV2, + network: "a:", + }; + const result = PaymentRequirementsV2Schema.safeParse(v2TooShort); + expect(result.success).toBe(false); + }); + + it("should accept minimum valid CAIP-2 for V2", () => { + const v2Minimal = { + ...validPaymentRequirementsV2, + network: "a:b", + }; + const result = PaymentRequirementsV2Schema.safeParse(v2Minimal); + expect(result.success).toBe(true); + }); + }); + }); +}); diff --git a/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts b/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts new file mode 100644 index 0000000..07380ef --- /dev/null +++ b/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts @@ -0,0 +1,1001 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { x402ResourceServer } from "../../../src/server/x402ResourceServer"; +import { + MockFacilitatorClient, + MockSchemeNetworkServer, + buildPaymentPayload, + buildPaymentRequirements, + buildSupportedResponse, + buildVerifyResponse, + buildSettleResponse, +} from "../../mocks"; +import { Network } from "../../../src/types"; + +describe("x402ResourceServer", () => { + describe("Construction", () => { + it("should create default HTTP facilitator client if none provided", () => { + const server = new x402ResourceServer(); + + expect(server).toBeDefined(); + }); + + it("should use provided facilitator client", () => { + const mockClient = new MockFacilitatorClient(buildSupportedResponse()); + const server = new x402ResourceServer(mockClient); + + expect(server).toBeDefined(); + }); + + it("should normalize single client to array", async () => { + const mockClient = new MockFacilitatorClient(buildSupportedResponse()); + const server = new x402ResourceServer(mockClient); + + await server.initialize(); + + expect(mockClient.getSupportedCalls).toBe(1); + }); + + it("should use array of facilitator clients", async () => { + const mockClient1 = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "scheme1", network: "network1" as Network }], + }), + ); + const mockClient2 = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "scheme2", network: "network2" as Network }], + }), + ); + + const server = new x402ResourceServer([mockClient1, mockClient2]); + await server.initialize(); + + expect(mockClient1.getSupportedCalls).toBe(1); + expect(mockClient2.getSupportedCalls).toBe(1); + }); + + it("should create default client if empty array provided", async () => { + const server = new x402ResourceServer([]); + + // Should not throw - uses default client + await expect(server.initialize()).resolves.not.toThrow(); + }); + }); + + describe("register", () => { + it("should register scheme for network", () => { + const server = new x402ResourceServer(); + const mockScheme = new MockSchemeNetworkServer("test-scheme"); + + const result = server.register("test:network" as Network, mockScheme); + + expect(result).toBe(server); // Chaining + }); + + it("should support multiple schemes per network", () => { + const server = new x402ResourceServer(); + const scheme1 = new MockSchemeNetworkServer("scheme1"); + const scheme2 = new MockSchemeNetworkServer("scheme2"); + + const result = server + .register("test:network" as Network, scheme1) + .register("test:network" as Network, scheme2); + + expect(result).toBe(server); + }); + + it("should not override existing scheme registration", () => { + const server = new x402ResourceServer(); + const firstScheme = new MockSchemeNetworkServer("test-scheme"); + const secondScheme = new MockSchemeNetworkServer("test-scheme"); + + server + .register("test:network" as Network, firstScheme) + .register("test:network" as Network, secondScheme); + + // This is verified implicitly - both registrations succeed without error + expect(server).toBeDefined(); + }); + }); + + describe("initialize", () => { + it("should fetch supported kinds from all facilitators", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + ); + + const server = new x402ResourceServer(mockClient); + + await server.initialize(); + + expect(mockClient.getSupportedCalls).toBe(1); + }); + + it("should build version/network/scheme mappings", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + ); + + const server = new x402ResourceServer(mockClient); + const mockScheme = new MockSchemeNetworkServer("exact"); + server.register("eip155:8453" as Network, mockScheme); + + await server.initialize(); + + // Should be able to get supported kind + const supportedKind = server.getSupportedKind(2, "eip155:8453" as Network, "exact"); + expect(supportedKind).toBeDefined(); + expect(supportedKind?.scheme).toBe("exact"); + }); + + it("should give precedence to earlier facilitators", async () => { + const mockClient1 = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [ + { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as Network, + extra: { facilitator: "first" }, + }, + ], + }), + ); + + const mockClient2 = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [ + { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as Network, + extra: { facilitator: "second" }, + }, + ], + }), + ); + + const server = new x402ResourceServer([mockClient1, mockClient2]); + + await server.initialize(); + + const supportedKind = server.getSupportedKind(2, "eip155:8453" as Network, "exact"); + expect(supportedKind?.extra?.facilitator).toBe("first"); + }); + + it("should continue if one facilitator fails", async () => { + const failingClient = new MockFacilitatorClient(buildSupportedResponse()); + failingClient.setVerifyResponse(new Error("Network error")); + + const workingClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + ); + + // Mock getSupported to throw for first client + failingClient.getSupported = async () => { + throw new Error("Network error"); + }; + + const server = new x402ResourceServer([failingClient, workingClient]); + + // Should not throw - continues with working client + await server.initialize(); + + expect(workingClient.getSupportedCalls).toBe(1); + }); + + it("should clear existing mappings on re-initialization", async () => { + const mockClient1 = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [ + { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as Network, + extra: { version: 1 }, + }, + ], + }), + ); + + const server = new x402ResourceServer(mockClient1); + + await server.initialize(); + + // Re-initialize - this tests the clear logic + await server.initialize(); + + // Mappings should be re-built + expect(mockClient1.getSupportedCalls).toBe(2); + }); + }); + + describe("buildPaymentRequirements", () => { + it("should build requirements from ResourceConfig", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "test-scheme", network: "test:network" as Network }], + }), + ); + + const server = new x402ResourceServer(mockClient); + const mockScheme = new MockSchemeNetworkServer("test-scheme", { + amount: "1000000", + asset: "USDC", + extra: {}, + }); + + server.register("test:network" as Network, mockScheme); + await server.initialize(); + + const requirements = await server.buildPaymentRequirements({ + scheme: "test-scheme", + payTo: "recipient_address", + price: "$1.00", + network: "test:network" as Network, + }); + + expect(requirements).toHaveLength(1); + expect(requirements[0].scheme).toBe("test-scheme"); + expect(requirements[0].payTo).toBe("recipient_address"); + expect(requirements[0].amount).toBe("1000000"); + expect(requirements[0].asset).toBe("USDC"); + }); + + it("should call scheme's parsePrice method", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "test-scheme", network: "test:network" as Network }], + }), + ); + + const server = new x402ResourceServer(mockClient); + const mockScheme = new MockSchemeNetworkServer("test-scheme"); + + server.register("test:network" as Network, mockScheme); + await server.initialize(); + + await server.buildPaymentRequirements({ + scheme: "test-scheme", + payTo: "recipient", + price: "$5.00", + network: "test:network" as Network, + }); + + expect(mockScheme.parsePriceCalls.length).toBe(1); + expect(mockScheme.parsePriceCalls[0].price).toBe("$5.00"); + expect(mockScheme.parsePriceCalls[0].network).toBe("test:network"); + }); + + it("should call enhancePaymentRequirements", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "test-scheme", network: "test:network" as Network }], + extensions: ["test-extension"], + }), + ); + + const server = new x402ResourceServer(mockClient); + const mockScheme = new MockSchemeNetworkServer("test-scheme"); + + server.register("test:network" as Network, mockScheme); + await server.initialize(); + + await server.buildPaymentRequirements({ + scheme: "test-scheme", + payTo: "recipient", + price: 1.0, + network: "test:network" as Network, + }); + + expect(mockScheme.enhanceCalls.length).toBe(1); + }); + + it("should use default maxTimeoutSeconds of 300", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "test-scheme", network: "test:network" as Network }], + }), + ); + + const server = new x402ResourceServer(mockClient); + const mockScheme = new MockSchemeNetworkServer("test-scheme"); + + server.register("test:network" as Network, mockScheme); + await server.initialize(); + + const requirements = await server.buildPaymentRequirements({ + scheme: "test-scheme", + payTo: "recipient", + price: 1.0, + network: "test:network" as Network, + }); + + expect(requirements[0].maxTimeoutSeconds).toBe(300); + }); + + it("should respect custom maxTimeoutSeconds", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "test-scheme", network: "test:network" as Network }], + }), + ); + + const server = new x402ResourceServer(mockClient); + const mockScheme = new MockSchemeNetworkServer("test-scheme"); + + server.register("test:network" as Network, mockScheme); + await server.initialize(); + + const requirements = await server.buildPaymentRequirements({ + scheme: "test-scheme", + payTo: "recipient", + price: 1.0, + network: "test:network" as Network, + maxTimeoutSeconds: 600, + }); + + expect(requirements[0].maxTimeoutSeconds).toBe(600); + }); + + it("should return empty array if no scheme registered for network", async () => { + const server = new x402ResourceServer(); + + const requirements = await server.buildPaymentRequirements({ + scheme: "test-scheme", + payTo: "recipient", + price: 1.0, + network: "test:network" as Network, + }); + + // Current implementation returns empty array and logs warning + expect(requirements).toEqual([]); + }); + + it("should throw if facilitator doesn't support scheme/network", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "other-scheme", network: "test:network" as Network }], + }), + ); + + const server = new x402ResourceServer(mockClient); + const mockScheme = new MockSchemeNetworkServer("test-scheme"); + + server.register("test:network" as Network, mockScheme); + await server.initialize(); + + await expect( + async () => + await server.buildPaymentRequirements({ + scheme: "test-scheme", + payTo: "recipient", + price: 1.0, + network: "test:network" as Network, + }), + ).rejects.toThrow("Facilitator does not support test-scheme on test:network"); + }); + }); + + describe("Lifecycle hooks", () => { + let server: x402ResourceServer; + let mockClient: MockFacilitatorClient; + + beforeEach(() => { + mockClient = new MockFacilitatorClient( + buildSupportedResponse(), + buildVerifyResponse({ isValid: true }), + buildSettleResponse({ success: true }), + ); + server = new x402ResourceServer(mockClient); + }); + + describe("onBeforeVerify", () => { + it("should execute hook before verification", async () => { + let hookExecuted = false; + + server.onBeforeVerify(async context => { + hookExecuted = true; + expect(context.paymentPayload).toBeDefined(); + expect(context.requirements).toBeDefined(); + }); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements(); + + await server.verifyPayment(payload, requirements); + + expect(hookExecuted).toBe(true); + }); + + it("should abort verification if hook returns abort", async () => { + server.onBeforeVerify(async () => { + return { abort: true, reason: "Rate limited" }; + }); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements(); + + const result = await server.verifyPayment(payload, requirements); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("Rate limited"); + expect(mockClient.verifyCalls.length).toBe(0); // Facilitator not called + }); + + it("should execute multiple hooks in order", async () => { + const executionOrder: number[] = []; + + server + .onBeforeVerify(async () => { + executionOrder.push(1); + }) + .onBeforeVerify(async () => { + executionOrder.push(2); + }) + .onBeforeVerify(async () => { + executionOrder.push(3); + }); + + await server.verifyPayment(buildPaymentPayload(), buildPaymentRequirements()); + + expect(executionOrder).toEqual([1, 2, 3]); + }); + + it("should stop on first abort", async () => { + const executionOrder: number[] = []; + + server + .onBeforeVerify(async () => { + executionOrder.push(1); + }) + .onBeforeVerify(async () => { + executionOrder.push(2); + return { abort: true, reason: "Aborted" }; + }) + .onBeforeVerify(async () => { + executionOrder.push(3); // Should not execute + }); + + await server.verifyPayment(buildPaymentPayload(), buildPaymentRequirements()); + + expect(executionOrder).toEqual([1, 2]); // Third hook not executed + }); + }); + + describe("onAfterVerify", () => { + it("should execute hook after successful verification", async () => { + let hookExecuted = false; + let hookResult: any; + + server.onAfterVerify(async context => { + hookExecuted = true; + hookResult = context.result; + }); + + const result = await server.verifyPayment( + buildPaymentPayload(), + buildPaymentRequirements(), + ); + + expect(hookExecuted).toBe(true); + expect(hookResult).toBe(result); + }); + + it("should execute multiple afterVerify hooks in order", async () => { + const executionOrder: number[] = []; + + server + .onAfterVerify(async () => { + executionOrder.push(1); + }) + .onAfterVerify(async () => { + executionOrder.push(2); + }) + .onAfterVerify(async () => { + executionOrder.push(3); + }); + + await server.verifyPayment(buildPaymentPayload(), buildPaymentRequirements()); + + expect(executionOrder).toEqual([1, 2, 3]); + }); + + it("should not execute afterVerify if verification aborted", async () => { + let afterVerifyCalled = false; + + server.onBeforeVerify(async () => { + return { abort: true, reason: "Aborted" }; + }); + + server.onAfterVerify(async () => { + afterVerifyCalled = true; + }); + + await server.verifyPayment(buildPaymentPayload(), buildPaymentRequirements()); + + expect(afterVerifyCalled).toBe(false); + }); + }); + + describe("onVerifyFailure", () => { + it("should execute when verification fails", async () => { + let hookExecuted = false; + let hookError: Error | undefined; + + mockClient.setVerifyResponse(new Error("Verification failed")); + + server.onVerifyFailure(async context => { + hookExecuted = true; + hookError = context.error; + }); + + await expect( + async () => await server.verifyPayment(buildPaymentPayload(), buildPaymentRequirements()), + ).rejects.toThrow("Verification failed"); + + expect(hookExecuted).toBe(true); + expect(hookError?.message).toBe("Verification failed"); + }); + + it("should allow recovery from failure", async () => { + mockClient.setVerifyResponse(new Error("Temporary failure")); + + server.onVerifyFailure(async _context => { + // Recover with successful result + return { + recovered: true, + result: { isValid: true, payer: "0xRecovered" }, + }; + }); + + const result = await server.verifyPayment( + buildPaymentPayload(), + buildPaymentRequirements(), + ); + + expect(result.isValid).toBe(true); + expect(result.payer).toBe("0xRecovered"); + }); + + it("should try all hooks until one recovers", async () => { + const executionOrder: number[] = []; + + mockClient.setVerifyResponse(new Error("Failure")); + + server + .onVerifyFailure(async () => { + executionOrder.push(1); + // No recovery + }) + .onVerifyFailure(async () => { + executionOrder.push(2); + return { recovered: true, result: { isValid: true } }; + }) + .onVerifyFailure(async () => { + executionOrder.push(3); // Should not execute + }); + + await server.verifyPayment(buildPaymentPayload(), buildPaymentRequirements()); + + expect(executionOrder).toEqual([1, 2]); // Stops after recovery + }); + + it("should re-throw if no recovery", async () => { + mockClient.setVerifyResponse(new Error("Fatal error")); + + server.onVerifyFailure(async () => { + // No recovery + }); + + await expect( + async () => await server.verifyPayment(buildPaymentPayload(), buildPaymentRequirements()), + ).rejects.toThrow("Fatal error"); + }); + }); + + describe("onBeforeSettle", () => { + it("should execute hook before settlement", async () => { + let hookExecuted = false; + + server.onBeforeSettle(async context => { + hookExecuted = true; + expect(context.paymentPayload).toBeDefined(); + expect(context.requirements).toBeDefined(); + }); + + await server.settlePayment(buildPaymentPayload(), buildPaymentRequirements()); + + expect(hookExecuted).toBe(true); + }); + + it("should abort settlement if hook returns abort", async () => { + server.onBeforeSettle(async () => { + return { abort: true, reason: "Insufficient balance" }; + }); + + await expect( + async () => await server.settlePayment(buildPaymentPayload(), buildPaymentRequirements()), + ).rejects.toThrow("before_settle_hook_error: Insufficient balance"); + + expect(mockClient.settleCalls.length).toBe(0); // Facilitator not called + }); + + it("should execute multiple hooks in order", async () => { + const executionOrder: number[] = []; + + server + .onBeforeSettle(async () => { + executionOrder.push(1); + }) + .onBeforeSettle(async () => { + executionOrder.push(2); + }); + + await server.settlePayment(buildPaymentPayload(), buildPaymentRequirements()); + + expect(executionOrder).toEqual([1, 2]); + }); + }); + + describe("onAfterSettle", () => { + it("should execute hook after successful settlement", async () => { + let hookExecuted = false; + let hookResult: any; + + server.onAfterSettle(async context => { + hookExecuted = true; + hookResult = context.result; + }); + + const result = await server.settlePayment( + buildPaymentPayload(), + buildPaymentRequirements(), + ); + + expect(hookExecuted).toBe(true); + expect(hookResult).toBe(result); + }); + }); + + describe("onSettleFailure", () => { + it("should execute when settlement fails", async () => { + let hookExecuted = false; + + mockClient.setSettleResponse(new Error("Settlement failed")); + + server.onSettleFailure(async context => { + hookExecuted = true; + expect(context.error.message).toBe("Settlement failed"); + }); + + await expect( + async () => await server.settlePayment(buildPaymentPayload(), buildPaymentRequirements()), + ).rejects.toThrow(); + + expect(hookExecuted).toBe(true); + }); + + it("should allow recovery from failure", async () => { + mockClient.setSettleResponse(new Error("Temporary failure")); + + server.onSettleFailure(async () => { + return { + recovered: true, + result: { + success: true, + transaction: "0xRecoveredTx", + network: "eip155:8453", + }, + }; + }); + + const result = await server.settlePayment( + buildPaymentPayload(), + buildPaymentRequirements(), + ); + + expect(result.success).toBe(true); + expect(result.transaction).toBe("0xRecoveredTx"); + }); + }); + }); + + describe("verifyPayment", () => { + it("should verify payment through facilitator client", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + buildVerifyResponse({ isValid: true }), + ); + + const server = new x402ResourceServer(mockClient); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + const result = await server.verifyPayment(payload, requirements); + + expect(result.isValid).toBe(true); + expect(mockClient.verifyCalls.length).toBe(1); + }); + + it("should throw if no facilitator found", async () => { + // Create server with mock that throws an error + const mockClient = new MockFacilitatorClient( + buildSupportedResponse(), + new Error("No facilitator supports this payment"), + ); + + const server = new x402ResourceServer(mockClient); + + await expect( + async () => + await server.verifyPayment( + buildPaymentPayload(), + buildPaymentRequirements({ scheme: "exact", network: "eip155:8453" as Network }), + ), + ).rejects.toThrow("No facilitator supports"); + }); + }); + + describe("settlePayment", () => { + it("should settle payment through facilitator client", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + }); + + const result = await server.settlePayment(payload, requirements); + + expect(result.success).toBe(true); + expect(mockClient.settleCalls.length).toBe(1); + }); + }); + + describe("findMatchingRequirements", () => { + it("should match v2 requirements by deep equality", () => { + const server = new x402ResourceServer(); + + const req1 = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + asset: "USDC", + }); + + const req2 = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "2000000", + asset: "USDC", + }); + + const payload = buildPaymentPayload({ + x402Version: 2, + accepted: req1, + }); + + const result = server.findMatchingRequirements([req1, req2], payload); + + expect(result).toEqual(req1); + }); + + it("should match v1 requirements by scheme and network", () => { + const server = new x402ResourceServer(); + + const req1 = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + const payload = buildPaymentPayload({ + x402Version: 1, + accepted: buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "9999999", // Different amount - should still match for v1 + }), + }); + + const result = server.findMatchingRequirements([req1], payload); + + expect(result).toEqual(req1); + }); + + it("should return undefined if no match found", () => { + const server = new x402ResourceServer(); + + const req1 = buildPaymentRequirements({ scheme: "exact", network: "eip155:8453" as Network }); + const payload = buildPaymentPayload({ + accepted: buildPaymentRequirements({ scheme: "intent", network: "eip155:8453" as Network }), + }); + + const result = server.findMatchingRequirements([req1], payload); + + expect(result).toBeUndefined(); + }); + + it("should handle objects with different property order (v2)", () => { + const server = new x402ResourceServer(); + + const req = { + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + asset: "USDC", + payTo: "0xabc", + maxTimeoutSeconds: 300, + extra: {}, + }; + + // Same data, different order + const accepted = { + extra: {}, + maxTimeoutSeconds: 300, + payTo: "0xabc", + asset: "USDC", + amount: "1000000", + network: "eip155:8453" as Network, + scheme: "exact", + }; + + const payload = buildPaymentPayload({ x402Version: 2, accepted }); + + const result = server.findMatchingRequirements([req], payload); + + expect(result).toBeDefined(); + }); + }); + + describe("createPaymentRequiredResponse", () => { + it("should create v2 response", async () => { + const server = new x402ResourceServer(); + + const requirements = [buildPaymentRequirements()]; + const resourceInfo = { + url: "https://example.com", + description: "Test resource", + mimeType: "application/json", + }; + + const result = await server.createPaymentRequiredResponse(requirements, resourceInfo); + + expect(result.x402Version).toBe(2); + expect(result.resource).toEqual(resourceInfo); + expect(result.accepts).toEqual(requirements); + }); + + it("should include error message if provided", async () => { + const server = new x402ResourceServer(); + + const result = await server.createPaymentRequiredResponse( + [buildPaymentRequirements()], + { url: "https://example.com", description: "", mimeType: "" }, + "Payment required", + ); + + expect(result.error).toBe("Payment required"); + }); + + it("should include extensions if provided", async () => { + const server = new x402ResourceServer(); + + const result = await server.createPaymentRequiredResponse( + [buildPaymentRequirements()], + { url: "https://example.com", description: "", mimeType: "" }, + undefined, + { bazaar: true, customExt: "value" }, + ); + + expect(result.extensions).toEqual({ bazaar: true, customExt: "value" }); + }); + + it("should omit extensions if empty", async () => { + const server = new x402ResourceServer(); + + const result = await server.createPaymentRequiredResponse( + [buildPaymentRequirements()], + { url: "https://example.com", description: "", mimeType: "" }, + undefined, + {}, + ); + + expect(result.extensions).toBeUndefined(); + }); + }); + + describe("getSupportedKind and getFacilitatorExtensions", () => { + it("should return supported kind after initialization", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [ + { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as Network, + extra: { test: true }, + }, + ], + }), + ); + + const server = new x402ResourceServer(mockClient); + await server.initialize(); + + const supportedKind = server.getSupportedKind(2, "eip155:8453" as Network, "exact"); + + expect(supportedKind).toBeDefined(); + expect(supportedKind?.scheme).toBe("exact"); + expect(supportedKind?.extra?.test).toBe(true); + }); + + it("should return undefined if not found", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + ); + + const server = new x402ResourceServer(mockClient); + await server.initialize(); + + const supportedKind = server.getSupportedKind(2, "solana:mainnet" as Network, "exact"); + + expect(supportedKind).toBeUndefined(); + }); + + it("should return facilitator extensions", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + extensions: ["bazaar", "sign_in_with_x"], + }), + ); + + const server = new x402ResourceServer(mockClient); + await server.initialize(); + + const extensions = server.getFacilitatorExtensions(2, "eip155:8453" as Network, "exact"); + + expect(extensions).toEqual(["bazaar", "sign_in_with_x"]); + }); + + it("should return empty array if no extensions", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + ); + + const server = new x402ResourceServer(mockClient); + await server.initialize(); + + const extensions = server.getFacilitatorExtensions(2, "eip155:8453" as Network, "exact"); + + expect(extensions).toEqual([]); + }); + }); +}); diff --git a/typescript/packages/core/test/unit/utils/utils.test.ts b/typescript/packages/core/test/unit/utils/utils.test.ts new file mode 100644 index 0000000..4ac1dca --- /dev/null +++ b/typescript/packages/core/test/unit/utils/utils.test.ts @@ -0,0 +1,355 @@ +import { describe, it, expect } from "vitest"; +import { + findByNetworkAndScheme, + findSchemesByNetwork, + deepEqual, + safeBase64Encode, + safeBase64Decode, +} from "../../../src/utils"; +import { Network } from "../../../src/types"; + +describe("Utils", () => { + describe("findSchemesByNetwork", () => { + it("should find schemes by exact network match", () => { + const map = new Map>(); + const schemes = new Map(); + schemes.set("exact", "exactImpl"); + schemes.set("intent", "intentImpl"); + map.set("eip155:8453", schemes); + + const result = findSchemesByNetwork(map, "eip155:8453" as Network); + + expect(result).toBeDefined(); + expect(result?.get("exact")).toBe("exactImpl"); + expect(result?.get("intent")).toBe("intentImpl"); + }); + + it("should return undefined for network not found", () => { + const map = new Map>(); + const schemes = new Map(); + schemes.set("exact", "exactImpl"); + map.set("eip155:8453", schemes); + + const result = findSchemesByNetwork(map, "solana:mainnet" as Network); + + expect(result).toBeUndefined(); + }); + + it("should match wildcard patterns - eip155:*", () => { + const map = new Map>(); + const schemes = new Map(); + schemes.set("exact", "evmImpl"); + map.set("eip155:*", schemes); + + const result = findSchemesByNetwork(map, "eip155:8453" as Network); + + expect(result).toBeDefined(); + expect(result?.get("exact")).toBe("evmImpl"); + }); + + it("should match wildcard patterns - solana:*", () => { + const map = new Map>(); + const schemes = new Map(); + schemes.set("exact", "svmImpl"); + map.set("solana:*", schemes); + + const result = findSchemesByNetwork( + map, + "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" as Network, + ); + + expect(result).toBeDefined(); + expect(result?.get("exact")).toBe("svmImpl"); + }); + + it("should match universal wildcard *", () => { + const map = new Map>(); + const schemes = new Map(); + schemes.set("cash", "cashImpl"); + map.set("*", schemes); + + const result1 = findSchemesByNetwork(map, "eip155:8453" as Network); + const result2 = findSchemesByNetwork(map, "solana:mainnet" as Network); + const result3 = findSchemesByNetwork(map, "custom:anything" as Network); + + expect(result1?.get("cash")).toBe("cashImpl"); + expect(result2?.get("cash")).toBe("cashImpl"); + expect(result3?.get("cash")).toBe("cashImpl"); + }); + + it("should prefer exact match over pattern match", () => { + const map = new Map>(); + + const exactSchemes = new Map(); + exactSchemes.set("exact", "exactNetworkImpl"); + map.set("eip155:8453", exactSchemes); + + const patternSchemes = new Map(); + patternSchemes.set("exact", "patternImpl"); + map.set("eip155:*", patternSchemes); + + const result = findSchemesByNetwork(map, "eip155:8453" as Network); + + expect(result?.get("exact")).toBe("exactNetworkImpl"); + }); + }); + + describe("findByNetworkAndScheme", () => { + it("should find implementation by network and scheme", () => { + const map = new Map>(); + const schemes = new Map(); + schemes.set("exact", "exactImpl"); + schemes.set("intent", "intentImpl"); + map.set("eip155:8453", schemes); + + const result = findByNetworkAndScheme(map, "exact", "eip155:8453" as Network); + + expect(result).toBe("exactImpl"); + }); + + it("should return undefined if network not found", () => { + const map = new Map>(); + + const result = findByNetworkAndScheme(map, "exact", "eip155:8453" as Network); + + expect(result).toBeUndefined(); + }); + + it("should return undefined if scheme not found in network", () => { + const map = new Map>(); + const schemes = new Map(); + schemes.set("intent", "intentImpl"); + map.set("eip155:8453", schemes); + + const result = findByNetworkAndScheme(map, "exact", "eip155:8453" as Network); + + expect(result).toBeUndefined(); + }); + + it("should use pattern matching for network", () => { + const map = new Map>(); + const schemes = new Map(); + schemes.set("exact", "evmImpl"); + map.set("eip155:*", schemes); + + const result = findByNetworkAndScheme(map, "exact", "eip155:8453" as Network); + + expect(result).toBe("evmImpl"); + }); + }); + + describe("deepEqual", () => { + describe("primitives", () => { + it("should match identical numbers", () => { + expect(deepEqual(42, 42)).toBe(true); + expect(deepEqual(42, 43)).toBe(false); + }); + + it("should match identical strings", () => { + expect(deepEqual("hello", "hello")).toBe(true); + expect(deepEqual("hello", "world")).toBe(false); + }); + + it("should match identical booleans", () => { + expect(deepEqual(true, true)).toBe(true); + expect(deepEqual(true, false)).toBe(false); + }); + + it("should match null and undefined", () => { + expect(deepEqual(null, null)).toBe(true); + expect(deepEqual(undefined, undefined)).toBe(true); + expect(deepEqual(null, undefined)).toBe(false); + }); + }); + + describe("objects", () => { + it("should match identical objects", () => { + const obj1 = { a: 1, b: 2 }; + const obj2 = { a: 1, b: 2 }; + + expect(deepEqual(obj1, obj2)).toBe(true); + }); + + it("should match objects with different key order", () => { + const obj1 = { a: 1, b: 2, c: 3 }; + const obj2 = { c: 3, a: 1, b: 2 }; + + expect(deepEqual(obj1, obj2)).toBe(true); + }); + + it("should not match objects with different values", () => { + const obj1 = { a: 1, b: 2 }; + const obj2 = { a: 1, b: 3 }; + + expect(deepEqual(obj1, obj2)).toBe(false); + }); + + it("should handle nested objects", () => { + const obj1 = { a: { b: { c: 1 } } }; + const obj2 = { a: { b: { c: 1 } } }; + + expect(deepEqual(obj1, obj2)).toBe(true); + }); + + it("should handle nested objects with different key order", () => { + const obj1 = { outer: { a: 1, b: 2 }, other: "val" }; + const obj2 = { other: "val", outer: { b: 2, a: 1 } }; + + expect(deepEqual(obj1, obj2)).toBe(true); + }); + + it("should not match if nested values differ", () => { + const obj1 = { a: { b: { c: 1 } } }; + const obj2 = { a: { b: { c: 2 } } }; + + expect(deepEqual(obj1, obj2)).toBe(false); + }); + + it("should handle objects with null/undefined values", () => { + const obj1 = { a: null, b: undefined }; + const obj2 = { a: null, b: undefined }; + + expect(deepEqual(obj1, obj2)).toBe(true); + }); + + it("should distinguish null from undefined", () => { + const obj1 = { a: null }; + const obj2 = { a: undefined }; + + expect(deepEqual(obj1, obj2)).toBe(false); + }); + }); + + describe("arrays", () => { + it("should match identical arrays", () => { + const arr1 = [1, 2, 3]; + const arr2 = [1, 2, 3]; + + expect(deepEqual(arr1, arr2)).toBe(true); + }); + + it("should respect array order", () => { + const arr1 = [1, 2, 3]; + const arr2 = [3, 2, 1]; + + expect(deepEqual(arr1, arr2)).toBe(false); + }); + + it("should handle arrays of objects", () => { + const arr1 = [{ a: 1 }, { b: 2 }]; + const arr2 = [{ a: 1 }, { b: 2 }]; + + expect(deepEqual(arr1, arr2)).toBe(true); + }); + + it("should handle nested arrays", () => { + const arr1 = [ + [1, 2], + [3, 4], + ]; + const arr2 = [ + [1, 2], + [3, 4], + ]; + + expect(deepEqual(arr1, arr2)).toBe(true); + }); + + it("should handle empty arrays", () => { + expect(deepEqual([], [])).toBe(true); + expect(deepEqual([], [1])).toBe(false); + }); + }); + + describe("complex structures", () => { + it("should match payment requirements with different key orders", () => { + const req1 = { + scheme: "exact", + network: "eip155:8453", + amount: "1000000", + asset: "0x833...", + payTo: "0xabc...", + extra: { foo: "bar" }, + }; + + const req2 = { + extra: { foo: "bar" }, + payTo: "0xabc...", + asset: "0x833...", + amount: "1000000", + network: "eip155:8453", + scheme: "exact", + }; + + expect(deepEqual(req1, req2)).toBe(true); + }); + + it("should handle empty objects", () => { + expect(deepEqual({}, {})).toBe(true); + expect(deepEqual({}, { a: 1 })).toBe(false); + }); + }); + }); + + describe("Base64 encoding", () => { + const unicodeOriginal = + "USD₮0 🤖 中文 ありがとう नमस्ते Привет مرحبا بالعالم שלום Γειά σου สวัสดี"; + const unicodeEncoded = + "VVNE4oKuMCDwn6SWIOS4reaWhyDjgYLjgorjgYzjgajjgYYg4KSo4KSu4KS44KWN4KSk4KWHINCf0YDQuNCy0LXRgiDZhdix2K3YqNinINio2KfZhNi52KfZhNmFINep15zXldedIM6TzrXOuc6sIM+Dzr/PhSDguKrguKfguLHguKrguJTguLU="; + + describe("safeBase64Encode", () => { + it("should encode simple strings", () => { + const encoded = safeBase64Encode("hello"); + expect(encoded).toBe("aGVsbG8="); + }); + + it("should encode strings with special characters", () => { + const encoded = safeBase64Encode("test data 123!@#"); + expect(encoded).toBe("dGVzdCBkYXRhIDEyMyFAIw=="); + }); + + it("should encode empty string", () => { + const encoded = safeBase64Encode(""); + expect(encoded).toBe(""); + }); + + it("should encode unicode characters", () => { + // Note: btoa doesn't handle unicode directly, need to encode first + // This test verifies the function exists and works with ASCII + const encoded = safeBase64Encode(unicodeOriginal); + expect(encoded).toBe(unicodeEncoded); + }); + }); + + describe("safeBase64Decode", () => { + it("should decode simple base64 strings", () => { + const decoded = safeBase64Decode("aGVsbG8="); + expect(decoded).toBe("hello"); + }); + + it("should roundtrip encode/decode", () => { + const original = "test data 123!@#"; + const encoded = safeBase64Encode(original); + const decoded = safeBase64Decode(encoded); + + expect(decoded).toBe(original); + }); + + it("should decode unicode characters", () => { + const decoded = safeBase64Decode(unicodeEncoded); + expect(decoded).toBe(unicodeOriginal); + }); + + it("should decode empty string", () => { + const decoded = safeBase64Decode(""); + expect(decoded).toBe(""); + }); + + it("should handle base64 with different padding", () => { + expect(safeBase64Decode("YQ==")).toBe("a"); + expect(safeBase64Decode("YWI=")).toBe("ab"); + expect(safeBase64Decode("YWJj")).toBe("abc"); + }); + }); + }); +}); diff --git a/typescript/packages/x402/tsconfig.json b/typescript/packages/core/tsconfig.json similarity index 100% rename from typescript/packages/x402/tsconfig.json rename to typescript/packages/core/tsconfig.json diff --git a/typescript/packages/x402/tsup.config.ts b/typescript/packages/core/tsup.config.ts similarity index 69% rename from typescript/packages/x402/tsup.config.ts rename to typescript/packages/core/tsup.config.ts index 681efc0..f4efd26 100644 --- a/typescript/packages/x402/tsup.config.ts +++ b/typescript/packages/core/tsup.config.ts @@ -3,14 +3,14 @@ import { defineConfig } from "tsup"; const baseConfig = { entry: { index: "src/index.ts", - "shared/index": "src/shared/index.ts", - "shared/evm/index": "src/shared/evm/index.ts", - "schemes/index": "src/schemes/index.ts", "client/index": "src/client/index.ts", - "verify/index": "src/verify/index.ts", "facilitator/index": "src/facilitator/index.ts", - "paywall/index": "src/paywall/index.ts", + "http/index": "src/http/index.ts", + "server/index": "src/server/index.ts", "types/index": "src/types/index.ts", + "types/v1/index": "src/types/v1/index.ts", + "utils/index": "src/utils/index.ts", + "schemas/index": "src/schemas/index.ts", }, dts: { resolve: true, diff --git a/typescript/packages/x402/vitest.config.ts b/typescript/packages/core/vitest.config.ts similarity index 100% rename from typescript/packages/x402/vitest.config.ts rename to typescript/packages/core/vitest.config.ts diff --git a/typescript/packages/extensions/.prettierignore b/typescript/packages/extensions/.prettierignore new file mode 100644 index 0000000..9fd1bad --- /dev/null +++ b/typescript/packages/extensions/.prettierignore @@ -0,0 +1,7 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +**/**/*.json +*.md diff --git a/typescript/packages/extensions/.prettierrc b/typescript/packages/extensions/.prettierrc new file mode 100644 index 0000000..ffb416b --- /dev/null +++ b/typescript/packages/extensions/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/packages/extensions/CHANGELOG.md b/typescript/packages/extensions/CHANGELOG.md new file mode 100644 index 0000000..91c383c --- /dev/null +++ b/typescript/packages/extensions/CHANGELOG.md @@ -0,0 +1,22 @@ +# @x402/extensions Changelog + +## 2.3.0 + +### Minor Changes + +- fe42994: Added Sign-In-With-X (SIWX) extension for wallet-based authentication. Clients can prove previous payment by signing a message, avoiding re-payment. Supports EVM and Solana signature schemes with multi-chain support, lifecycle hooks for servers and clients, and optional nonce tracking for replay protection. +- 51b8445: Added payment-identifier extension for tracking and validating payment identifiers + +### Patch Changes + +- Updated dependencies [51b8445] +- Updated dependencies [51b8445] + - @x402/core@2.3.0 + +## 2.0.0 + +- Implements x402 2.0.0 for the TypeScript SDK. + +## 1.0.0 + +- Implements x402 1.0.0 for the TypeScript SDK. diff --git a/typescript/packages/extensions/README.md b/typescript/packages/extensions/README.md new file mode 100644 index 0000000..47357dc --- /dev/null +++ b/typescript/packages/extensions/README.md @@ -0,0 +1,712 @@ +# @x402/extensions + +x402 Payment Protocol Extensions. This package provides optional extensions that enhance the x402 payment protocol with additional functionality. + +## Installation + +```bash +pnpm install @x402/extensions +``` + +## Overview + +Extensions are optional features that can be added to x402 payment flows. They follow a standardized `{ info, schema }` structure and are included in `PaymentRequired.extensions` and `PaymentPayload.extensions`. + +This package includes: +- **Bazaar Discovery**: Automatic cataloging and indexing of x402-enabled resources +- **Sign-In-With-X (SIWx)**: CAIP-122 wallet authentication for accessing previously purchased resources + +## Bazaar Discovery Extension + +The Bazaar Discovery Extension enables facilitators to automatically catalog and index x402-enabled resources by following server-declared discovery instructions. This allows users to discover paid APIs and services through facilitator catalogs. + +### How It Works + +1. **Servers** declare discovery metadata when configuring their payment endpoints +2. The HTTP method is automatically inferred from the route definition (e.g., `"GET /weather"`) +3. **Facilitators** extract this metadata from payment requests +4. **Users** can browse and discover available paid resources through facilitator catalogs + +### For Resource Servers + +Declare endpoint discovery metadata in your payment middleware configuration. This helps facilitators understand how to call your endpoints and what they return. + +> **Note:** The HTTP method is automatically inferred from the route key (e.g., `"GET /weather"` → GET method). You don't need to specify it in `declareDiscoveryExtension`. + +#### Basic Example: GET Endpoint with Query Parameters + +```typescript +import { declareDiscoveryExtension } from "@x402/extensions/bazaar"; + +const resources = { + "GET /weather": { + accepts: { + scheme: "exact", + price: "$0.001", + network: "eip155:84532", + payTo: "0xYourAddress" + }, + extensions: { + ...declareDiscoveryExtension({ + input: { city: "San Francisco" }, + inputSchema: { + properties: { + city: { type: "string" }, + units: { type: "string", enum: ["celsius", "fahrenheit"] } + }, + required: ["city"] + }, + output: { + example: { + city: "San Francisco", + weather: "foggy", + temperature: 15, + humidity: 85 + } + }, + }), + }, + }, +}; +``` + +#### Example: POST Endpoint with JSON Body + +For POST, PUT, and PATCH endpoints, specify `bodyType` to indicate the request body format: + +```typescript +import { declareDiscoveryExtension } from "@x402/extensions/bazaar"; + +const resources = { + "POST /api/translate": { + accepts: { + scheme: "exact", + price: "$0.01", + network: "eip155:84532", + payTo: "0xYourAddress" + }, + extensions: { + ...declareDiscoveryExtension({ + input: { + text: "Hello, world!", + targetLanguage: "es" + }, + inputSchema: { + properties: { + text: { type: "string" }, + targetLanguage: { type: "string", pattern: "^[a-z]{2}$" } + }, + required: ["text", "targetLanguage"] + }, + bodyType: "json", + output: { + example: { + translatedText: "¡Hola, mundo!", + sourceLanguage: "en", + targetLanguage: "es" + } + }, + }), + }, + }, +}; +``` + +#### Example: PUT Endpoint with Form Data + +```typescript +const resources = { + "PUT /api/user/profile": { + accepts: { + scheme: "exact", + price: "$0.05", + network: "eip155:84532", + payTo: "0xYourAddress" + }, + extensions: { + ...declareDiscoveryExtension({ + input: { + name: "John Doe", + email: "john@example.com", + bio: "Software developer" + }, + inputSchema: { + properties: { + name: { type: "string", minLength: 1 }, + email: { type: "string", format: "email" }, + bio: { type: "string", maxLength: 500 } + }, + required: ["name", "email"] + }, + bodyType: "form-data", + output: { + example: { + success: true, + userId: "123", + updatedAt: "2024-01-01T00:00:00Z" + } + }, + }), + }, + }, +}; +``` + +#### Example: DELETE Endpoint + +```typescript +const resources = { + "DELETE /api/data/:id": { + accepts: { + scheme: "exact", + price: "$0.001", + network: "eip155:84532", + payTo: "0xYourAddress" + }, + extensions: { + ...declareDiscoveryExtension({ + input: { id: "123" }, + inputSchema: { + properties: { + id: { type: "string" } + }, + required: ["id"] + }, + output: { + example: { + success: true, + deletedId: "123" + } + }, + }), + }, + }, +}; +``` + +#### Using with Next.js Middleware + +```typescript +import { paymentProxy, x402ResourceServer } from "@x402/next"; +import { HTTPFacilitatorClient } from "@x402/core/http"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { declareDiscoveryExtension } from "@x402/extensions/bazaar"; + +const facilitatorClient = new HTTPFacilitatorClient({ url: "https://facilitator.x402.org" }); +const resourceServer = new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()); + +export const proxy = paymentProxy( + { + "/api/weather": { + accepts: { + scheme: "exact", + price: "$0.001", + network: "eip155:84532", + payTo: "0xYourAddress", + }, + extensions: { + ...declareDiscoveryExtension({ + input: { city: "San Francisco" }, + inputSchema: { + properties: { city: { type: "string" } }, + required: ["city"], + }, + output: { + example: { city: "San Francisco", weather: "foggy" } + }, + }), + }, + }, + }, + resourceServer, +); +``` + +### For Facilitators + +Extract discovery information from incoming payment requests to catalog resources in the Bazaar. + +#### Basic Usage + +```typescript +import { extractDiscoveryInfo } from "@x402/extensions/bazaar"; +import type { PaymentPayload, PaymentRequirements } from "@x402/core/types"; + +async function handlePayment( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements +) { + // Extract discovery info from the payment + const discovered = extractDiscoveryInfo(paymentPayload, paymentRequirements); + + if (discovered) { + // discovered contains: + // { + // resourceUrl: "https://api.example.com/weather", + // method: "GET", + // x402Version: 2, + // discoveryInfo: { + // input: { type: "http", method: "GET", queryParams: { city: "..." } }, + // output: { type: "json", example: { ... } } + // } + // } + + // Catalog the resource in your Bazaar + await catalogResource({ + url: discovered.resourceUrl, + method: discovered.method, + inputSchema: discovered.discoveryInfo.input, + outputExample: discovered.discoveryInfo.output?.example, + }); + } +} +``` + +#### Validating Discovery Extensions + +```typescript +import { validateDiscoveryExtension, extractDiscoveryInfo } from "@x402/extensions/bazaar"; + +function processPayment(paymentPayload: PaymentPayload, paymentRequirements: PaymentRequirements) { + const discovered = extractDiscoveryInfo(paymentPayload, paymentRequirements); + + if (discovered && paymentPayload.extensions?.bazaar) { + // Validate the extension schema + const validation = validateDiscoveryExtension(paymentPayload.extensions.bazaar); + + if (!validation.valid) { + console.warn("Invalid discovery extension:", validation.errors); + // Handle invalid extension (log, reject, etc.) + return; + } + + // Extension is valid, proceed with cataloging + catalogResource(discovered); + } +} +``` + +#### Using with Server Extension Helper + +The `bazaarResourceServerExtension` automatically enriches discovery extensions with HTTP method information from the request context: + +```typescript +import { bazaarResourceServerExtension } from "@x402/extensions/bazaar"; +import { x402ResourceServer } from "@x402/core/server"; + +// The extension helper automatically extracts discovery info +const resourceServer = new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()) + .registerExtension(bazaarResourceServerExtension); +``` + +### Bazaar API Reference + +#### `declareDiscoveryExtension(config)` + +Creates a discovery extension object for resource servers. + +**Parameters:** +- `config.input` (optional): Example input values (query params for GET/HEAD/DELETE, body for POST/PUT/PATCH) +- `config.inputSchema` (optional): JSON Schema for input validation +- `config.bodyType` (required for body methods): For POST/PUT/PATCH, specify `"json"`, `"form-data"`, or `"text"`. This is how TypeScript discriminates between query methods (GET/HEAD/DELETE) and body methods. +- `config.output` (optional): Output specification + - `output.example`: Example output data + - `output.schema`: JSON Schema for output validation + +> **Note:** The HTTP method is NOT passed to this function. It is automatically inferred from the route key (e.g., `"GET /weather"`) or enriched by `bazaarResourceServerExtension` at runtime. + +**Returns:** An object with a `bazaar` key containing the discovery extension. + +**Example:** +```typescript +const extension = declareDiscoveryExtension({ + input: { query: "search term" }, + inputSchema: { + properties: { query: { type: "string" } }, + required: ["query"] + }, + output: { + example: { results: [] } + } +}); +// Returns: { bazaar: { info: {...}, schema: {...} } } +``` + +#### `extractDiscoveryInfo(paymentPayload, paymentRequirements, validate?)` + +Extracts discovery information from a payment request (for facilitators). + +**Parameters:** +- `paymentPayload`: The payment payload from the client +- `paymentRequirements`: The payment requirements from the server +- `validate` (optional): Whether to validate the extension (default: `true`) + +**Returns:** `DiscoveredResource` object or `null` if not found. + +```typescript +interface DiscoveredResource { + resourceUrl: string; + method: string; + x402Version: number; + discoveryInfo: DiscoveryInfo; +} +``` + +#### `validateDiscoveryExtension(extension)` + +Validates a discovery extension's info against its schema. + +**Returns:** `{ valid: boolean, errors?: string[] }` + +#### `validateAndExtract(extension)` + +Validates and extracts discovery info in one step. + +**Returns:** `{ valid: boolean, info?: DiscoveryInfo, errors?: string[] }` + +#### `bazaarResourceServerExtension` + +A server extension that automatically enriches discovery extensions with HTTP method information from the request context. + +**Usage:** +```typescript +import { bazaarResourceServerExtension } from "@x402/extensions/bazaar"; + +const resourceServer = new x402ResourceServer(facilitatorClient) + .registerExtension(bazaarResourceServerExtension); +``` + +### `BAZAAR` + +The extension identifier constant (`"bazaar"`). + +## Sign-In-With-X Extension + +The Sign-In-With-X extension implements [CAIP-122](https://chainagnostic.org/CAIPs/caip-122) for chain-agnostic wallet authentication. It allows clients to prove control of a wallet that previously paid for a resource, enabling access without repurchase. + +### How It Works + +1. Server returns 402 with `sign-in-with-x` extension containing challenge parameters +2. Client signs the CAIP-122 message with their wallet +3. Client sends signed proof in `SIGN-IN-WITH-X` header +4. Server verifies signature and grants access if wallet has previous payment + +### Server Usage + +#### Recommended: Hooks (Automatic) + +```typescript +import { + declareSIWxExtension, + siwxResourceServerExtension, + createSIWxSettleHook, + createSIWxRequestHook, + InMemorySIWxStorage, +} from '@x402/extensions/sign-in-with-x'; + +// Storage for tracking paid addresses +const storage = new InMemorySIWxStorage(); + +// 1. Register extension for time-based field refreshment +const resourceServer = new x402ResourceServer(facilitatorClient) + .register(NETWORK, new ExactEvmScheme()) + .registerExtension(siwxResourceServerExtension) // Refreshes nonce/timestamps per request + .onAfterSettle(createSIWxSettleHook({ storage })); // Records payments + +// 2. Declare SIWX support in routes (network/domain/uri derived automatically) +const routes = { + "GET /data": { + accepts: [{scheme: "exact", price: "$0.01", network: "eip155:8453", payTo}], + extensions: declareSIWxExtension({ + statement: 'Sign in to access your purchased content', + }), + }, +}; + +// 3. Verify incoming SIWX proofs +const httpServer = new x402HTTPResourceServer(resourceServer, routes) + .onProtectedRequest(createSIWxRequestHook({ storage })); // Grants access if paid + +// Optional: Enable smart wallet support (EIP-1271/EIP-6492) +import { createPublicClient, http } from 'viem'; +import { base } from 'viem/chains'; + +const publicClient = createPublicClient({ chain: base, transport: http() }); +const httpServerWithSmartWallets = new x402HTTPResourceServer(resourceServer, routes) + .onProtectedRequest(createSIWxRequestHook({ + storage, + verifyOptions: { evmVerifier: publicClient.verifyMessage }, + })); +``` + +The hooks automatically: +- **siwxResourceServerExtension**: Derives `network` from `accepts`, `domain`/`uri` from request URL, refreshes `nonce`/`issuedAt`/`expirationTime` per request +- **createSIWxSettleHook**: Records payment when settlement succeeds +- **createSIWxRequestHook**: Validates and verifies SIWX proofs, grants access if wallet has paid + +#### Manual Usage (Advanced) + +```typescript +import { + declareSIWxExtension, + parseSIWxHeader, + validateSIWxMessage, + verifySIWxSignature, + SIGN_IN_WITH_X, +} from '@x402/extensions/sign-in-with-x'; + +// 1. Declare in PaymentRequired response +const extensions = { + [SIGN_IN_WITH_X]: declareSIWxExtension({ + domain: 'api.example.com', + resourceUri: 'https://api.example.com/data', + network: 'eip155:8453', + statement: 'Sign in to access your purchased content', + }), +}; + +// 2. Verify incoming proof +async function handleRequest(request: Request) { + const header = request.headers.get('SIGN-IN-WITH-X'); + if (!header) return; // No auth provided + + // Parse the header + const payload = parseSIWxHeader(header); + + // Validate message fields (expiry, nonce, domain, etc.) + const validation = await validateSIWxMessage( + payload, + 'https://api.example.com/data' + ); + if (!validation.valid) { + return { error: validation.error }; + } + + // Verify signature and recover address + const verification = await verifySIWxSignature(payload); + if (!verification.valid) { + return { error: verification.error }; + } + + // verification.address is the verified wallet + // Check if this wallet has paid before + const hasPaid = await checkPaymentHistory(verification.address); + if (hasPaid) { + // Grant access without payment + } +} +``` + +### Client Usage + +#### Recommended: Client Hook (Automatic) + +```typescript +import { createSIWxClientHook } from '@x402/extensions/sign-in-with-x'; +import { x402HTTPClient } from '@x402/fetch'; + +// Configure client with SIWX hook - automatically tries SIWX auth before payment +const httpClient = new x402HTTPClient(client) + .onPaymentRequired(createSIWxClientHook(signer)); + +// Requests automatically use SIWX auth when server supports it +const response = await httpClient.fetch(url); +``` + +The client hook automatically: +- Detects SIWX support in 402 responses +- Matches your wallet's chain with server's `supportedChains` +- Signs and sends the authentication proof +- Falls back to payment if SIWX auth fails + +#### Manual Usage (Advanced) + +```typescript +import { + createSIWxPayload, + encodeSIWxHeader, +} from '@x402/extensions/sign-in-with-x'; + +// 1. Get extension and network from 402 response +const paymentRequired = await response.json(); +const extension = paymentRequired.extensions['sign-in-with-x']; +const paymentNetwork = paymentRequired.accepts[0].network; // e.g., "eip155:8453" + +// 2. Find matching chain in supportedChains +const matchingChain = extension.supportedChains.find( + chain => chain.chainId === paymentNetwork +); + +if (!matchingChain) { + // Payment network not supported for SIWX + throw new Error('Chain not supported'); +} + +// 3. Build complete info with selected chain +const completeInfo = { + ...extension.info, + chainId: matchingChain.chainId, + type: matchingChain.type, +}; + +// 4. Create signed payload +const payload = await createSIWxPayload(completeInfo, signer); + +// 5. Encode and send +const header = encodeSIWxHeader(payload); +const response = await fetch(url, { + headers: { 'SIGN-IN-WITH-X': header } +}); +``` + +### SIWx API Reference + +#### `declareSIWxExtension(options?)` + +Creates the extension object for servers to include in PaymentRequired. Most fields are derived automatically from request context when using `siwxResourceServerExtension`. + +```typescript +declareSIWxExtension({ + // All fields optional - derived from context if omitted + domain?: string; // Server domain (derived from request URL) + resourceUri?: string; // Full resource URI (derived from request URL) + network?: string | string[]; // CAIP-2 network(s) (derived from accepts[].network) + statement?: string; // Human-readable purpose + version?: string; // CAIP-122 version (default: "1") + expirationSeconds?: number; // Challenge TTL in seconds +}) +``` + +**Automatic derivation:** When using `siwxResourceServerExtension`, omitted fields are derived: +- `network` → from `accepts[].network` in route config +- `resourceUri` → from request URL +- `domain` → parsed from resourceUri + +**Multi-chain support:** When `network` is an array (or multiple networks in `accepts`), `supportedChains` will contain one entry per network. + +#### `parseSIWxHeader(header)` + +Parses a base64-encoded SIGN-IN-WITH-X header into a payload object. + +#### `validateSIWxMessage(payload, resourceUri, options?)` + +Validates message fields (expiry, domain binding, nonce, etc.). + +```typescript +validateSIWxMessage(payload, resourceUri, { + maxAge?: number; // Max age for issuedAt (default: 5 min) + checkNonce?: (nonce) => boolean; // Custom nonce validation +}) +// Returns: { valid: boolean; error?: string } +``` + +#### `verifySIWxSignature(payload, options?)` + +Verifies the cryptographic signature and recovers the signer address. + +```typescript +verifySIWxSignature(payload, { + evmVerifier?: EVMMessageVerifier; // For smart wallet support +}) +// Returns: { valid: boolean; address?: string; error?: string } +``` + +**Smart Wallet Support (EIP-1271 / EIP-6492):** + +By default, only EOA (Externally Owned Account) signatures are verified. To support smart contract wallets (like Coinbase Smart Wallet, Safe, etc.), pass `publicClient.verifyMessage` from viem: + +```typescript +import { createPublicClient, http } from 'viem'; +import { base } from 'viem/chains'; + +const publicClient = createPublicClient({ + chain: base, + transport: http() +}); + +// In your request hook +const result = await verifySIWxSignature(payload, { + evmVerifier: publicClient.verifyMessage, +}); +``` + +This enables: +- **EIP-1271**: Verification of deployed smart contract wallets +- **EIP-6492**: Verification of counterfactual (not-yet-deployed) wallets + +Note: Smart wallet verification requires RPC calls, while EOA verification is purely local. + +#### `createSIWxPayload(serverInfo, signer)` + +Client helper that creates and signs a complete payload. + +#### `encodeSIWxHeader(payload)` + +Encodes a payload as base64 for the SIGN-IN-WITH-X header. + +#### `SIGN_IN_WITH_X` + +Extension identifier constant (`"sign-in-with-x"`). + +### Supported Signature Schemes + +| Scheme | Description | +|--------|-------------| +| `eip191` | personal_sign (default for EVM EOAs) | +| `eip1271` | Smart contract wallet verification | +| `eip6492` | Counterfactual smart wallet verification | +| `siws` | Sign-In-With-Solana | + +## Troubleshooting + +### Bazaar Extension Not Being Extracted + +**Problem:** `extractDiscoveryInfo` returns `null`. + +**Solutions:** +- Ensure the server has declared the extension using `declareDiscoveryExtension` +- Check that `paymentPayload.extensions.bazaar` exists +- Verify you're using x402 v2 (v1 uses a different format in `outputSchema`) + +### Bazaar Schema Validation Fails + +**Problem:** `validateDiscoveryExtension` returns `valid: false`. + +**Solutions:** +- Ensure `inputSchema` matches the structure of `input` +- Check that required fields are marked in `inputSchema.required` +- Verify JSON Schema syntax is correct + +### SIWx Signature Verification Fails + +**Problem:** `verifySIWxSignature` returns `valid: false`. + +**Solutions:** +- Ensure the message was signed with the correct wallet +- Check that the signature scheme matches the wallet type +- For smart wallets, enable `checkSmartWallet` option with a provider + +### SIWx Message Validation Fails + +**Problem:** `validateSIWxMessage` returns `valid: false`. + +**Solutions:** +- Check that `issuedAt` is recent (within `maxAge`, default 5 minutes) +- Verify `expirationTime` hasn't passed +- Ensure `domain` matches the server's domain +- Confirm `uri` matches the resource URI + +## Related Resources + +- [x402 Core Package](../core/README.md) - Core x402 protocol implementation +- [CAIP-122 Specification](https://chainagnostic.org/CAIPs/caip-122) - Sign-In-With-X standard + +## Version Support + +This package supports both x402 v1 and v2: +- **v2**: Extensions are in `PaymentPayload.extensions` and `PaymentRequired.extensions` +- **v1**: Discovery info is in `PaymentRequirements.outputSchema` (automatically converted) + +The `extractDiscoveryInfo` function automatically handles both versions. diff --git a/typescript/packages/x402/eslint.config.js b/typescript/packages/extensions/eslint.config.js similarity index 96% rename from typescript/packages/x402/eslint.config.js rename to typescript/packages/extensions/eslint.config.js index 687c7ff..a276444 100644 --- a/typescript/packages/x402/eslint.config.js +++ b/typescript/packages/extensions/eslint.config.js @@ -7,7 +7,7 @@ import importPlugin from "eslint-plugin-import"; export default [ { - ignores: ["dist/**", "node_modules/**", "src/paywall/dist/**", "src/paywall/gen/**"], + ignores: ["dist/**", "node_modules/**"], }, { files: ["**/*.ts", "**/*.tsx"], diff --git a/typescript/packages/extensions/package.json b/typescript/packages/extensions/package.json new file mode 100644 index 0000000..a92668f --- /dev/null +++ b/typescript/packages/extensions/package.json @@ -0,0 +1,99 @@ +{ + "name": "@x402/extensions", + "version": "2.3.0", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/cjs/index.d.ts", + "scripts": { + "start": "tsx --env-file=.env index.ts", + "build": "tsup", + "test": "vitest run", + "test:watch": "vitest", + "watch": "tsc --watch", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "keywords": [ + "x402", + "payment", + "protocol", + "extensions" + ], + "license": "Apache-2.0", + "author": "Coinbase Inc.", + "repository": "https://github.com/coinbase/x402", + "description": "x402 Payment Protocol Extensions", + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/node": "^22.13.4", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsup": "^8.4.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vite": "^6.2.6", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.5" + }, + "dependencies": { + "@scure/base": "^1.2.6", + "@x402/core": "workspace:~", + "ajv": "^8.17.1", + "siwe": "^2.3.2", + "tweetnacl": "^1.0.3", + "viem": "^2.43.5", + "zod": "^3.24.2" + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.mts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./bazaar": { + "import": { + "types": "./dist/esm/bazaar/index.d.mts", + "default": "./dist/esm/bazaar/index.mjs" + }, + "require": { + "types": "./dist/cjs/bazaar/index.d.ts", + "default": "./dist/cjs/bazaar/index.js" + } + }, + "./sign-in-with-x": { + "import": { + "types": "./dist/esm/sign-in-with-x/index.d.mts", + "default": "./dist/esm/sign-in-with-x/index.mjs" + }, + "require": { + "types": "./dist/cjs/sign-in-with-x/index.d.ts", + "default": "./dist/cjs/sign-in-with-x/index.js" + } + }, + "./payment-identifier": { + "import": { + "types": "./dist/esm/payment-identifier/index.d.mts", + "default": "./dist/esm/payment-identifier/index.mjs" + }, + "require": { + "types": "./dist/cjs/payment-identifier/index.d.ts", + "default": "./dist/cjs/payment-identifier/index.js" + } + } + }, + "files": [ + "dist" + ] +} diff --git a/typescript/packages/extensions/pnpm-lock.yaml b/typescript/packages/extensions/pnpm-lock.yaml new file mode 100644 index 0000000..638b94b --- /dev/null +++ b/typescript/packages/extensions/pnpm-lock.yaml @@ -0,0 +1,4089 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@x402/core': + specifier: workspace:* + version: link:../core + ajv: + specifier: ^8.17.1 + version: 8.17.1 + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.39.1 + '@types/node': + specifier: ^22.13.4 + version: 22.19.1 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.46.4(eslint@9.39.1)(typescript@5.9.3) + eslint: + specifier: ^9.24.0 + version: 9.39.1 + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.39.1) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.39.1)(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.1(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3) + tsx: + specifier: ^4.19.2 + version: 4.20.6 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vite: + specifier: ^6.2.6 + version: 6.4.1(@types/node@22.19.1)(tsx@4.20.6) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.1)(tsx@4.20.6)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/node@22.19.1)(tsx@4.20.6) + +packages: + + '@es-joy/jsdoccomment@0.50.2': + resolution: {integrity: sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==} + engines: {node: '>=18'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.0': + resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.0': + resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.0': + resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.0': + resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.0': + resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.0': + resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.0': + resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.0': + resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.0': + resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.0': + resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.0': + resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.0': + resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.0': + resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.0': + resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.0': + resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.0': + resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.0': + resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.0': + resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.0': + resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.0': + resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.0': + resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.0': + resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.0': + resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.0': + resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.0': + resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.0': + resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.1': + resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@rollup/rollup-android-arm-eabi@4.53.2': + resolution: {integrity: sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.53.2': + resolution: {integrity: sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.53.2': + resolution: {integrity: sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.53.2': + resolution: {integrity: sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.53.2': + resolution: {integrity: sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.53.2': + resolution: {integrity: sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.53.2': + resolution: {integrity: sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.53.2': + resolution: {integrity: sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.53.2': + resolution: {integrity: sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.53.2': + resolution: {integrity: sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.53.2': + resolution: {integrity: sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.53.2': + resolution: {integrity: sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.53.2': + resolution: {integrity: sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.53.2': + resolution: {integrity: sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.53.2': + resolution: {integrity: sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.53.2': + resolution: {integrity: sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.53.2': + resolution: {integrity: sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.53.2': + resolution: {integrity: sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.53.2': + resolution: {integrity: sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.53.2': + resolution: {integrity: sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.53.2': + resolution: {integrity: sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.53.2': + resolution: {integrity: sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@22.19.1': + resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} + + '@typescript-eslint/eslint-plugin@8.46.4': + resolution: {integrity: sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.46.4 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.46.4': + resolution: {integrity: sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.46.4': + resolution: {integrity: sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.46.4': + resolution: {integrity: sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.46.4': + resolution: {integrity: sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.46.4': + resolution: {integrity: sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.46.4': + resolution: {integrity: sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.46.4': + resolution: {integrity: sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.46.4': + resolution: {integrity: sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.46.4': + resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + + 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.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.0: + resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsdoc@50.8.0: + resolution: {integrity: sha512-UyGb5755LMFWPrZTEqqvTJ3urLz1iqj+bYOHFNag+sw3NvaMWP9K2z+uIn37XfNALmQLQyrBlJ5mkiVPL7ADEg==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-prettier@5.5.4: + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.1: + resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + 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==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + 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'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsdoc-type-pratt-parser@4.1.0: + resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} + engines: {node: '>=12.0.0'} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + 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'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-imports-exports@0.2.4: + resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} + + parse-statements@1.0.11: + resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.5.2: + resolution: {integrity: sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.53.2: + resolution: {integrity: sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + 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.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + + spdx-license-ids@3.0.22: + resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@es-joy/jsdoccomment@0.50.2': + dependencies: + '@types/estree': 1.0.8 + '@typescript-eslint/types': 8.46.4 + comment-parser: 1.4.1 + esquery: 1.6.0 + jsdoc-type-pratt-parser: 4.1.0 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.0': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.0': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.0': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.0': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.0': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.0': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.0': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.0': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.0': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.0': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.0': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.0': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.0': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.0': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.0': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.0': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.0': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.0': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.0': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.0': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.0': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.0': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.0': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.0': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.0': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.0': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1)': + dependencies: + eslint: 9.39.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.1': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.2.9': {} + + '@rollup/rollup-android-arm-eabi@4.53.2': + optional: true + + '@rollup/rollup-android-arm64@4.53.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.53.2': + optional: true + + '@rollup/rollup-darwin-x64@4.53.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.53.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.53.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.53.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.53.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.53.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.53.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.53.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.53.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.53.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.53.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.53.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.53.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.53.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.53.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.53.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.53.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.2': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@22.19.1': + dependencies: + undici-types: 6.21.0 + + '@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 + eslint: 9.39.1 + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 + debug: 4.4.3 + eslint: 9.39.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.46.4(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.46.4': + dependencies: + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 + + '@typescript-eslint/tsconfig-utils@8.46.4(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.46.4(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.1 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.46.4': {} + + '@typescript-eslint/typescript-estree@8.46.4(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.46.4(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.3 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.46.4(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + eslint: 9.39.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.46.4': + dependencies: + '@typescript-eslint/types': 8.46.4 + eslint-visitor-keys: 4.2.1 + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@22.19.1)(tsx@4.20.6))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.1(@types/node@22.19.1)(tsx@4.20.6) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + any-promise@1.3.0: {} + + are-docs-informative@0.0.2: {} + + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + assertion-error@2.0.1: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + balanced-match@1.0.2: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + bundle-require@5.1.0(esbuild@0.27.0): + dependencies: + esbuild: 0.27.0 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@2.1.1: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@4.1.1: {} + + comment-parser@1.4.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.0 + '@esbuild/android-arm': 0.27.0 + '@esbuild/android-arm64': 0.27.0 + '@esbuild/android-x64': 0.27.0 + '@esbuild/darwin-arm64': 0.27.0 + '@esbuild/darwin-x64': 0.27.0 + '@esbuild/freebsd-arm64': 0.27.0 + '@esbuild/freebsd-x64': 0.27.0 + '@esbuild/linux-arm': 0.27.0 + '@esbuild/linux-arm64': 0.27.0 + '@esbuild/linux-ia32': 0.27.0 + '@esbuild/linux-loong64': 0.27.0 + '@esbuild/linux-mips64el': 0.27.0 + '@esbuild/linux-ppc64': 0.27.0 + '@esbuild/linux-riscv64': 0.27.0 + '@esbuild/linux-s390x': 0.27.0 + '@esbuild/linux-x64': 0.27.0 + '@esbuild/netbsd-arm64': 0.27.0 + '@esbuild/netbsd-x64': 0.27.0 + '@esbuild/openbsd-arm64': 0.27.0 + '@esbuild/openbsd-x64': 0.27.0 + '@esbuild/openharmony-arm64': 0.27.0 + '@esbuild/sunos-x64': 0.27.0 + '@esbuild/win32-arm64': 0.27.0 + '@esbuild/win32-ia32': 0.27.0 + '@esbuild/win32-x64': 0.27.0 + + escape-string-regexp@4.0.0: {} + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + eslint: 9.39.1 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsdoc@50.8.0(eslint@9.39.1): + dependencies: + '@es-joy/jsdoccomment': 0.50.2 + are-docs-informative: 0.0.2 + comment-parser: 1.4.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint: 9.39.1 + espree: 10.4.0 + esquery: 1.6.0 + parse-imports-exports: 0.2.4 + semver: 7.7.3 + spdx-expression-parse: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-prettier@5.5.4(eslint@9.39.1)(prettier@3.5.2): + dependencies: + eslint: 9.39.1 + prettier: 3.5.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.39.1 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + expect-type@1.2.2: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.0 + rollup: 4.53.2 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + 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.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globrex@0.1.2: {} + + gopd@1.2.0: {} + + graphemer@1.4.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + joycon@3.1.1: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsdoc-type-pratt-parser@4.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-imports-exports@0.2.4: + dependencies: + parse-statements: 1.0.11 + + parse-statements@1.0.11: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + possible-typed-array-names@1.1.0: {} + + postcss-load-config@6.0.1(postcss@8.5.6)(tsx@4.20.6): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.6 + tsx: 4.20.6 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.5.2: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + readdirp@4.1.2: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.53.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.2 + '@rollup/rollup-android-arm64': 4.53.2 + '@rollup/rollup-darwin-arm64': 4.53.2 + '@rollup/rollup-darwin-x64': 4.53.2 + '@rollup/rollup-freebsd-arm64': 4.53.2 + '@rollup/rollup-freebsd-x64': 4.53.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.2 + '@rollup/rollup-linux-arm-musleabihf': 4.53.2 + '@rollup/rollup-linux-arm64-gnu': 4.53.2 + '@rollup/rollup-linux-arm64-musl': 4.53.2 + '@rollup/rollup-linux-loong64-gnu': 4.53.2 + '@rollup/rollup-linux-ppc64-gnu': 4.53.2 + '@rollup/rollup-linux-riscv64-gnu': 4.53.2 + '@rollup/rollup-linux-riscv64-musl': 4.53.2 + '@rollup/rollup-linux-s390x-gnu': 4.53.2 + '@rollup/rollup-linux-x64-gnu': 4.53.2 + '@rollup/rollup-linux-x64-musl': 4.53.2 + '@rollup/rollup-openharmony-arm64': 4.53.2 + '@rollup/rollup-win32-arm64-msvc': 4.53.2 + '@rollup/rollup-win32-ia32-msvc': 4.53.2 + '@rollup/rollup-win32-x64-gnu': 4.53.2 + '@rollup/rollup-win32-x64-msvc': 4.53.2 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + semver@6.3.1: {} + + semver@7.7.3: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + 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.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.22 + + spdx-license-ids@3.0.22: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tree-kill@1.2.2: {} + + ts-api-utils@2.1.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-interface-checker@0.1.13: {} + + tsconfck@3.1.6(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tsup@8.5.1(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.0) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.0 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.6)(tsx@4.20.6) + resolve-from: 5.0.0 + rollup: 4.53.2 + source-map: 0.7.6 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.6 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsx@4.20.6: + dependencies: + esbuild: 0.25.12 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.9.3: {} + + ufo@1.6.1: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite-node@3.2.4(@types/node@22.19.1)(tsx@4.20.6): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.4.1(@types/node@22.19.1)(tsx@4.20.6) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.1)(tsx@4.20.6)): + dependencies: + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.3) + optionalDependencies: + vite: 6.4.1(@types/node@22.19.1)(tsx@4.20.6) + transitivePeerDependencies: + - supports-color + - typescript + + vite@6.4.1(@types/node@22.19.1)(tsx@4.20.6): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.2 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.1 + fsevents: 2.3.3 + tsx: 4.20.6 + + vitest@3.2.4(@types/node@22.19.1)(tsx@4.20.6): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.1)(tsx@4.20.6)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.4.1(@types/node@22.19.1)(tsx@4.20.6) + vite-node: 3.2.4(@types/node@22.19.1)(tsx@4.20.6) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.1 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + yocto-queue@0.1.0: {} + + zod@3.25.76: {} diff --git a/typescript/packages/extensions/src/bazaar/facilitator.ts b/typescript/packages/extensions/src/bazaar/facilitator.ts new file mode 100644 index 0000000..bf97674 --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/facilitator.ts @@ -0,0 +1,260 @@ +/** + * Facilitator functions for validating and extracting Bazaar discovery extensions + * + * These functions help facilitators validate extension data against schemas + * and extract the discovery information for cataloging in the Bazaar. + * + * Supports both v2 (extensions in PaymentRequired) and v1 (outputSchema in PaymentRequirements). + */ + +import Ajv from "ajv/dist/2020.js"; +import type { PaymentPayload, PaymentRequirements, PaymentRequirementsV1 } from "@x402/core/types"; +import type { DiscoveryExtension, DiscoveryInfo } from "./types"; +import { BAZAAR } from "./types"; +import { extractDiscoveryInfoV1 } from "./v1/facilitator"; + +/** + * Validation result for discovery extensions + */ +export interface ValidationResult { + valid: boolean; + errors?: string[]; +} + +/** + * Validates a discovery extension's info against its schema + * + * @param extension - The discovery extension containing info and schema + * @returns Validation result indicating if the info matches the schema + * + * @example + * ```typescript + * const extension = declareDiscoveryExtension(...); + * const result = validateDiscoveryExtension(extension); + * + * if (result.valid) { + * console.log("Extension is valid"); + * } else { + * console.error("Validation errors:", result.errors); + * } + * ``` + */ +export function validateDiscoveryExtension(extension: DiscoveryExtension): ValidationResult { + try { + const ajv = new Ajv({ strict: false, allErrors: true }); + const validate = ajv.compile(extension.schema); + + // The schema describes the structure of info directly + // Schema has properties: { input: {...}, output: {...} } + // So we validate extension.info which has { input: {...}, output: {...} } + const valid = validate(extension.info); + + if (valid) { + return { valid: true }; + } + + const errors = validate.errors?.map(err => { + const path = err.instancePath || "(root)"; + return `${path}: ${err.message}`; + }) || ["Unknown validation error"]; + + return { valid: false, errors }; + } catch (error) { + return { + valid: false, + errors: [ + `Schema validation failed: ${error instanceof Error ? error.message : String(error)}`, + ], + }; + } +} + +/** + * Extracts the discovery info from payment payload and requirements + * + * This function handles both v2 (extensions) and v1 (outputSchema) formats. + * + * For v2: Discovery info is in PaymentPayload.extensions (client copied it from PaymentRequired) + * For v1: Discovery info is in PaymentRequirements.outputSchema + * + * V1 data is automatically transformed to v2 DiscoveryInfo format, making smart + * assumptions about field names (queryParams/query/params for GET, bodyFields/body/data for POST, etc.) + * + * @param paymentPayload - The payment payload containing extensions (v2) and version info + * @param paymentRequirements - The payment requirements (contains outputSchema for v1) + * @param validate - Whether to validate v2 extensions before extracting (default: true) + * @returns The discovery info in v2 format if present, or null if not discoverable + * + * @example + * ```typescript + * // V2 - extensions are in PaymentPayload + * const info = extractDiscoveryInfo(paymentPayload, paymentRequirements); + * + * // V1 - discovery info is in PaymentRequirements.outputSchema + * const info = extractDiscoveryInfo(paymentPayloadV1, paymentRequirementsV1); + * + * if (info) { + * // Both v1 and v2 return the same DiscoveryInfo structure + * console.log("Method:", info.input.method); + * } + * ``` + */ +export interface DiscoveredResource { + resourceUrl: string; + description?: string; + mimeType?: string; + method: string; + x402Version: number; + discoveryInfo: DiscoveryInfo; +} + +/** + * Extracts discovery information from payment payload and requirements. + * Combines resource URL, HTTP method, version, and discovery info into a single object. + * + * @param paymentPayload - The payment payload containing extensions and resource info + * @param paymentRequirements - The payment requirements to validate against + * @param validate - Whether to validate the discovery info against the schema (default: true) + * @returns Discovered resource info with URL, method, version and discovery data, or null if not found + */ +export function extractDiscoveryInfo( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements | PaymentRequirementsV1, + validate: boolean = true, +): DiscoveredResource | null { + let discoveryInfo: DiscoveryInfo | null = null; + let resourceUrl: string; + + if (paymentPayload.x402Version === 2) { + resourceUrl = paymentPayload.resource?.url ?? ""; + + if (paymentPayload.extensions) { + const bazaarExtension = paymentPayload.extensions[BAZAAR]; + + if (bazaarExtension && typeof bazaarExtension === "object") { + try { + const extension = bazaarExtension as DiscoveryExtension; + + if (validate) { + const result = validateDiscoveryExtension(extension); + if (!result.valid) { + console.warn( + `V2 discovery extension validation failed: ${result.errors?.join(", ")}`, + ); + } else { + discoveryInfo = extension.info; + } + } else { + discoveryInfo = extension.info; + } + } catch (error) { + console.warn(`V2 discovery extension extraction failed: ${error}`); + } + } + } + } else if (paymentPayload.x402Version === 1) { + const requirementsV1 = paymentRequirements as PaymentRequirementsV1; + resourceUrl = requirementsV1.resource; + discoveryInfo = extractDiscoveryInfoV1(requirementsV1); + } else { + return null; + } + + if (!discoveryInfo) { + return null; + } + + // Strip query params (?) and hash sections (#) for discovery cataloging + const url = new URL(resourceUrl); + const normalizedResourceUrl = `${url.origin}${url.pathname}`; + + // Extract description and mimeType from resource info (v2) or requirements (v1) + let description: string | undefined; + let mimeType: string | undefined; + + if (paymentPayload.x402Version === 2) { + description = paymentPayload.resource?.description; + mimeType = paymentPayload.resource?.mimeType; + } else if (paymentPayload.x402Version === 1) { + const requirementsV1 = paymentRequirements as PaymentRequirementsV1; + description = requirementsV1.description; + mimeType = requirementsV1.mimeType; + } + + return { + resourceUrl: normalizedResourceUrl, + description, + mimeType, + method: discoveryInfo.input.method, + x402Version: paymentPayload.x402Version, + discoveryInfo, + }; +} + +/** + * Extracts discovery info from a v2 extension directly + * + * This is a lower-level function for when you already have the extension object. + * For general use, prefer the main extractDiscoveryInfo function. + * + * @param extension - The discovery extension to extract info from + * @param validate - Whether to validate before extracting (default: true) + * @returns The discovery info if valid + * @throws Error if validation fails and validate is true + */ +export function extractDiscoveryInfoFromExtension( + extension: DiscoveryExtension, + validate: boolean = true, +): DiscoveryInfo { + if (validate) { + const result = validateDiscoveryExtension(extension); + if (!result.valid) { + throw new Error( + `Invalid discovery extension: ${result.errors?.join(", ") || "Unknown error"}`, + ); + } + } + + return extension.info; +} + +/** + * Validates and extracts discovery info in one step + * + * This is a convenience function that combines validation and extraction, + * returning both the validation result and the info if valid. + * + * @param extension - The discovery extension to validate and extract + * @returns Object containing validation result and info (if valid) + * + * @example + * ```typescript + * const extension = declareDiscoveryExtension(...); + * const { valid, info, errors } = validateAndExtract(extension); + * + * if (valid && info) { + * // Store info in Bazaar catalog + * } else { + * console.error("Validation errors:", errors); + * } + * ``` + */ +export function validateAndExtract(extension: DiscoveryExtension): { + valid: boolean; + info?: DiscoveryInfo; + errors?: string[]; +} { + const result = validateDiscoveryExtension(extension); + + if (result.valid) { + return { + valid: true, + info: extension.info, + }; + } + + return { + valid: false, + errors: result.errors, + }; +} diff --git a/typescript/packages/extensions/src/bazaar/facilitatorClient.ts b/typescript/packages/extensions/src/bazaar/facilitatorClient.ts new file mode 100644 index 0000000..16125b3 --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/facilitatorClient.ts @@ -0,0 +1,156 @@ +/** + * Client extensions for querying Bazaar discovery resources + */ + +import { HTTPFacilitatorClient } from "@x402/core/http"; +import type { PaymentRequirements } from "@x402/core/types"; +import { WithExtensions } from "../types"; + +/** + * Parameters for listing discovery resources. + * All parameters are optional and used for filtering/pagination. + */ +export interface ListDiscoveryResourcesParams { + /** + * Filter by protocol type (e.g., "http", "mcp"). + * Currently, the only supported protocol type is "http". + */ + type?: string; + + /** + * The number of discovered x402 resources to return per page. + */ + limit?: number; + + /** + * The offset of the first discovered x402 resource to return. + */ + offset?: number; +} + +/** + * A discovered x402 resource from the bazaar. + */ +export interface DiscoveryResource { + /** The URL or identifier of the discovered resource */ + resource: string; + /** The protocol type of the resource (e.g., "http") */ + type: string; + /** The x402 protocol version supported by this resource */ + x402Version: number; + /** Array of accepted payment methods for this resource */ + accepts: PaymentRequirements[]; + /** ISO 8601 timestamp of when the resource was last updated */ + lastUpdated: string; + /** Additional metadata about the resource */ + metadata?: Record; +} + +/** + * Response from listing discovery resources. + */ +export interface DiscoveryResourcesResponse { + /** The x402 protocol version of this response */ + x402Version: number; + /** The list of discovered resources */ + items: DiscoveryResource[]; + /** Pagination information for the response */ + pagination: { + /** Maximum number of results returned */ + limit: number; + /** Number of results skipped */ + offset: number; + /** Total count of resources matching the query */ + total: number; + }; +} + +/** + * Bazaar client extension interface providing discovery query functionality. + */ +export interface BazaarClientExtension { + discovery: { + /** + * List x402 discovery resources from the bazaar. + * + * @param params - Optional filtering and pagination parameters + * @returns A promise resolving to the discovery resources response + */ + listResources(params?: ListDiscoveryResourcesParams): Promise; + }; +} + +/** + * Extends a facilitator client with Bazaar discovery query functionality. + * Preserves and merges with any existing extensions from prior chaining. + * + * @param client - The facilitator client to extend + * @returns The client extended with bazaar discovery capabilities + * + * @example + * ```ts + * // Basic usage + * const client = withBazaar(new HTTPFacilitatorClient()); + * const resources = await client.extensions.discovery.listResources({ type: "http" }); + * + * // Chaining with other extensions + * const client = withBazaar(withOtherExtension(new HTTPFacilitatorClient())); + * await client.extensions.other.someMethod(); + * await client.extensions.discovery.listResources(); + * ``` + */ +export function withBazaar( + client: T, +): WithExtensions { + // Preserve any existing extensions from prior chaining + const existingExtensions = + (client as T & { extensions?: Record }).extensions ?? {}; + + const extended = client as WithExtensions; + + extended.extensions = { + ...existingExtensions, + discovery: { + async listResources( + params?: ListDiscoveryResourcesParams, + ): Promise { + let headers: Record = { + "Content-Type": "application/json", + }; + + const authHeaders = await client.createAuthHeaders("discovery"); + headers = { ...headers, ...authHeaders.headers }; + + const queryParams = new URLSearchParams(); + if (params?.type !== undefined) { + queryParams.set("type", params.type); + } + if (params?.limit !== undefined) { + queryParams.set("limit", params.limit.toString()); + } + if (params?.offset !== undefined) { + queryParams.set("offset", params.offset.toString()); + } + + const queryString = queryParams.toString(); + const endpoint = `${client.url}/discovery/resources${queryString ? `?${queryString}` : ""}`; + + const response = await fetch(endpoint, { + method: "GET", + headers, + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => response.statusText); + throw new Error( + `Facilitator listDiscoveryResources failed (${response.status}): ${errorText}`, + ); + } + + return (await response.json()) as DiscoveryResourcesResponse; + }, + }, + } as WithExtensions["extensions"]; + + return extended; +} diff --git a/typescript/packages/extensions/src/bazaar/index.ts b/typescript/packages/extensions/src/bazaar/index.ts new file mode 100644 index 0000000..f2d94d8 --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/index.ts @@ -0,0 +1,111 @@ +/** + * Bazaar Discovery Extension for x402 v2 and v1 + * + * Enables facilitators to automatically catalog and index x402-enabled resources + * by following the server's provided discovery instructions. + * + * ## V2 Usage + * + * The v2 extension follows a pattern where: + * - `info`: Contains the actual discovery data (the values) + * - `schema`: JSON Schema that validates the structure of `info` + * + * ### For Resource Servers (V2) + * + * ```typescript + * import { declareDiscoveryExtension, BAZAAR } from '@x402/extensions/bazaar'; + * + * // Declare a GET endpoint + * const extension = declareDiscoveryExtension( + * "GET", + * { query: "example" }, + * { + * properties: { + * query: { type: "string" } + * }, + * required: ["query"] + * } + * ); + * + * // Include in PaymentRequired response + * const paymentRequired = { + * x402Version: 2, + * resource: { ... }, + * accepts: [ ... ], + * extensions: { + * [BAZAAR]: extension + * } + * }; + * ``` + * + * ### For Facilitators (V2 and V1) + * + * ```typescript + * import { + * extractDiscoveryInfo, + * BAZAAR + * } from '@x402/extensions/bazaar'; + * + * // V2: Extensions are in PaymentPayload.extensions (client copied from PaymentRequired) + * // V1: Discovery info is in PaymentRequirements.outputSchema + * const info = extractDiscoveryInfo( + * paymentPayload, + * paymentRequirements + * ); + * + * if (info) { + * // Catalog info in Bazaar + * } + * ``` + * + * ## V1 Support + * + * V1 discovery information is stored in the `outputSchema` field of PaymentRequirements. + * The `extractDiscoveryInfo` function automatically handles v1 format as a fallback. + * + * ```typescript + * import { extractDiscoveryInfoV1 } from '@x402/extensions/bazaar/v1'; + * + * // Direct v1 extraction + * const infoV1 = extractDiscoveryInfoV1(paymentRequirementsV1); + * ``` + */ + +// Export types +export type { + DiscoveryInfo, + QueryDiscoveryInfo, + BodyDiscoveryInfo, + QueryDiscoveryExtension, + BodyDiscoveryExtension, + DiscoveryExtension, +} from "./types"; + +export { BAZAAR } from "./types"; + +// Export resource service functions (for servers) +export { declareDiscoveryExtension } from "./resourceService"; + +export { bazaarResourceServerExtension } from "./server"; + +// Export facilitator functions (for facilitators) +export { + validateDiscoveryExtension, + extractDiscoveryInfo, + extractDiscoveryInfoFromExtension, + validateAndExtract, + type ValidationResult, + type DiscoveredResource, +} from "./facilitator"; + +// Export v1 functions (v1 data is transformed to v2 DiscoveryInfo format) +export { extractDiscoveryInfoV1, isDiscoverableV1, extractResourceMetadataV1 } from "./v1"; + +// Export client extension (for facilitator clients querying discovery) +export { + withBazaar, + BazaarClientExtension, + ListDiscoveryResourcesParams, + DiscoveryResource, + DiscoveryResourcesResponse, +} from "./facilitatorClient"; diff --git a/typescript/packages/extensions/src/bazaar/resourceService.ts b/typescript/packages/extensions/src/bazaar/resourceService.ts new file mode 100644 index 0000000..fb121cf --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/resourceService.ts @@ -0,0 +1,241 @@ +/** + * Resource Service functions for creating Bazaar discovery extensions + * + * These functions help servers declare the shape of their endpoints + * for facilitator discovery and cataloging in the Bazaar. + */ + +import { + type DiscoveryExtension, + type QueryDiscoveryExtension, + type BodyDiscoveryExtension, + type DeclareDiscoveryExtensionInput, + type DeclareQueryDiscoveryExtensionConfig, + type DeclareBodyDiscoveryExtensionConfig, +} from "./types"; + +/** + * Internal helper to create a query discovery extension + * + * @param root0 - Configuration object for query discovery extension + * @param root0.method - HTTP method (GET, HEAD, DELETE) + * @param root0.input - Query parameters + * @param root0.inputSchema - JSON schema for query parameters + * @param root0.output - Output specification with example + * @returns QueryDiscoveryExtension with info and schema + */ +function createQueryDiscoveryExtension({ + method, + input = {}, + inputSchema = { properties: {} }, + output, +}: DeclareQueryDiscoveryExtensionConfig): QueryDiscoveryExtension { + return { + info: { + input: { + type: "http", + ...(method ? { method } : {}), + ...(input ? { queryParams: input } : {}), + } as QueryDiscoveryExtension["info"]["input"], + ...(output?.example + ? { + output: { + type: "json", + example: output.example, + }, + } + : {}), + }, + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + input: { + type: "object", + properties: { + type: { + type: "string", + const: "http", + }, + method: { + type: "string", + enum: ["GET", "HEAD", "DELETE"], + }, + ...(inputSchema + ? { + queryParams: { + type: "object" as const, + ...(typeof inputSchema === "object" ? inputSchema : {}), + }, + } + : {}), + }, + required: ["type"] as ("type" | "method")[], + additionalProperties: false, + }, + ...(output?.example + ? { + output: { + type: "object" as const, + properties: { + type: { + type: "string" as const, + }, + example: { + type: "object" as const, + ...(output.schema && typeof output.schema === "object" ? output.schema : {}), + }, + }, + required: ["type"] as const, + }, + } + : {}), + }, + required: ["input"], + }, + }; +} + +/** + * Internal helper to create a body discovery extension + * + * @param root0 - Configuration object for body discovery extension + * @param root0.method - HTTP method (POST, PUT, PATCH) + * @param root0.input - Request body specification + * @param root0.inputSchema - JSON schema for request body + * @param root0.bodyType - Content type of body (json, form-data, text) - required for body methods + * @param root0.output - Output specification with example + * @returns BodyDiscoveryExtension with info and schema + */ +function createBodyDiscoveryExtension({ + method, + input = {}, + inputSchema = { properties: {} }, + bodyType, + output, +}: DeclareBodyDiscoveryExtensionConfig): BodyDiscoveryExtension { + return { + info: { + input: { + type: "http", + ...(method ? { method } : {}), + bodyType, + body: input, + } as BodyDiscoveryExtension["info"]["input"], + ...(output?.example + ? { + output: { + type: "json", + example: output.example, + }, + } + : {}), + }, + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + input: { + type: "object", + properties: { + type: { + type: "string", + const: "http", + }, + method: { + type: "string", + enum: ["POST", "PUT", "PATCH"], + }, + bodyType: { + type: "string", + enum: ["json", "form-data", "text"], + }, + body: inputSchema, + }, + required: ["type", "bodyType", "body"] as ("type" | "method" | "bodyType" | "body")[], + additionalProperties: false, + }, + ...(output?.example + ? { + output: { + type: "object" as const, + properties: { + type: { + type: "string" as const, + }, + example: { + type: "object" as const, + ...(output.schema && typeof output.schema === "object" ? output.schema : {}), + }, + }, + required: ["type"] as const, + }, + } + : {}), + }, + required: ["input"], + }, + }; +} + +/** + * Create a discovery extension for any HTTP method + * + * This function helps servers declare how their endpoint should be called, + * including the expected input parameters/body and output format. + * + * @param config - Configuration object for the discovery extension + * @returns A discovery extension object with both info and schema + * + * @example + * ```typescript + * // For a GET endpoint with no input + * const getExtension = declareDiscoveryExtension({ + * method: "GET", + * output: { + * example: { message: "Success", timestamp: "2024-01-01T00:00:00Z" } + * } + * }); + * + * // For a GET endpoint with query params + * const getWithParams = declareDiscoveryExtension({ + * method: "GET", + * input: { query: "example" }, + * inputSchema: { + * properties: { + * query: { type: "string" } + * }, + * required: ["query"] + * } + * }); + * + * // For a POST endpoint with JSON body + * const postExtension = declareDiscoveryExtension({ + * method: "POST", + * input: { name: "John", age: 30 }, + * inputSchema: { + * properties: { + * name: { type: "string" }, + * age: { type: "number" } + * }, + * required: ["name"] + * }, + * bodyType: "json", + * output: { + * example: { success: true, id: "123" } + * } + * }); + * ``` + */ +export function declareDiscoveryExtension( + config: DeclareDiscoveryExtensionInput, +): Record { + const bodyType = (config as DeclareBodyDiscoveryExtensionConfig).bodyType; + const isBodyMethod = bodyType !== undefined; + + const extension = isBodyMethod + ? createBodyDiscoveryExtension(config as DeclareBodyDiscoveryExtensionConfig) + : createQueryDiscoveryExtension(config as DeclareQueryDiscoveryExtensionConfig); + + return { bazaar: extension as DiscoveryExtension }; +} diff --git a/typescript/packages/extensions/src/bazaar/server.ts b/typescript/packages/extensions/src/bazaar/server.ts new file mode 100644 index 0000000..66ff348 --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/server.ts @@ -0,0 +1,87 @@ +import type { ResourceServerExtension } from "@x402/core/types"; +import type { HTTPRequestContext } from "@x402/core/http"; +import { BAZAAR } from "./types"; + +/** + * Type guard to check if context is an HTTP request context. + * + * @param ctx - The context to check + * @returns True if context is an HTTPRequestContext + */ +function isHTTPRequestContext(ctx: unknown): ctx is HTTPRequestContext { + return ctx !== null && typeof ctx === "object" && "method" in ctx && "adapter" in ctx; +} + +interface ExtensionDeclaration { + [key: string]: unknown; + info?: { + [key: string]: unknown; + input?: Record; + }; + schema?: { + [key: string]: unknown; + properties?: { + [key: string]: unknown; + input?: { + [key: string]: unknown; + properties?: { + [key: string]: unknown; + method?: Record; + }; + required?: string[]; + }; + }; + }; +} + +export const bazaarResourceServerExtension: ResourceServerExtension = { + key: BAZAAR, + + enrichDeclaration: (declaration, transportContext) => { + if (!isHTTPRequestContext(transportContext)) { + return declaration; + } + + const extension = declaration as ExtensionDeclaration; + const method = transportContext.method; + + // At declaration time, the schema uses a broad enum (["GET", "HEAD", "DELETE"] or ["POST", "PUT", "PATCH"]) + // because the method isn't known until the HTTP context is available. + // Here we narrow it to the actual method for precise schema validation. + const existingInputProps = extension.schema?.properties?.input?.properties || {}; + const updatedInputProps = { + ...existingInputProps, + method: { + type: "string", + enum: [method], + }, + }; + + return { + ...extension, + info: { + ...(extension.info || {}), + input: { + ...(extension.info?.input || {}), + method, + }, + }, + schema: { + ...(extension.schema || {}), + properties: { + ...(extension.schema?.properties || {}), + input: { + ...(extension.schema?.properties?.input || {}), + properties: updatedInputProps, + required: [ + ...(extension.schema?.properties?.input?.required || []), + ...(!(extension.schema?.properties?.input?.required || []).includes("method") + ? ["method"] + : []), + ], + }, + }, + }, + }; + }, +}; diff --git a/typescript/packages/extensions/src/bazaar/types.ts b/typescript/packages/extensions/src/bazaar/types.ts new file mode 100644 index 0000000..3bec6c2 --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/types.ts @@ -0,0 +1,214 @@ +/** + * Type definitions for the Bazaar Discovery Extension + */ + +import type { BodyMethods, QueryParamMethods } from "@x402/core/http"; + +/** + * Extension identifier constant for the Bazaar discovery extension + */ +export const BAZAAR = "bazaar"; + +/** + * Discovery info for query parameter methods (GET, HEAD, DELETE) + */ +export interface QueryDiscoveryInfo { + input: { + type: "http"; + method: QueryParamMethods; + queryParams?: Record; + headers?: Record; + }; + output?: { + type?: string; + format?: string; + example?: unknown; + }; +} + +/** + * Discovery info for body methods (POST, PUT, PATCH) + */ +export interface BodyDiscoveryInfo { + input: { + type: "http"; + method: BodyMethods; + bodyType: "json" | "form-data" | "text"; + body: Record; + queryParams?: Record; + headers?: Record; + }; + output?: { + type?: string; + format?: string; + example?: unknown; + }; +} + +/** + * Combined discovery info type + */ +export type DiscoveryInfo = QueryDiscoveryInfo | BodyDiscoveryInfo; + +/** + * Discovery extension for query parameter methods (GET, HEAD, DELETE) + */ +export interface QueryDiscoveryExtension { + info: QueryDiscoveryInfo; + + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema"; + type: "object"; + properties: { + input: { + type: "object"; + properties: { + type: { + type: "string"; + const: "http"; + }; + method: { + type: "string"; + enum: QueryParamMethods[]; + }; + queryParams?: { + type: "object"; + properties?: Record; + required?: string[]; + additionalProperties?: boolean; + }; + headers?: { + type: "object"; + additionalProperties: { + type: "string"; + }; + }; + }; + required: ("type" | "method")[]; + additionalProperties?: boolean; + }; + output?: { + type: "object"; + properties?: Record; + required?: readonly string[]; + additionalProperties?: boolean; + }; + }; + required: ["input"]; + }; +} + +/** + * Discovery extension for body methods (POST, PUT, PATCH) + */ +export interface BodyDiscoveryExtension { + info: BodyDiscoveryInfo; + + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema"; + type: "object"; + properties: { + input: { + type: "object"; + properties: { + type: { + type: "string"; + const: "http"; + }; + method: { + type: "string"; + enum: BodyMethods[]; + }; + bodyType: { + type: "string"; + enum: ["json", "form-data", "text"]; + }; + body: Record; + queryParams?: { + type: "object"; + properties?: Record; + required?: string[]; + additionalProperties?: boolean; + }; + headers?: { + type: "object"; + additionalProperties: { + type: "string"; + }; + }; + }; + required: ("type" | "method" | "bodyType" | "body")[]; + additionalProperties?: boolean; + }; + output?: { + type: "object"; + properties?: Record; + required?: readonly string[]; + additionalProperties?: boolean; + }; + }; + required: ["input"]; + }; +} + +/** + * Combined discovery extension type + */ +export type DiscoveryExtension = QueryDiscoveryExtension | BodyDiscoveryExtension; + +export interface DeclareQueryDiscoveryExtensionConfig { + method?: QueryParamMethods; + input?: Record; + inputSchema?: Record; + output?: { + example?: unknown; + schema?: Record; + }; +} + +export interface DeclareBodyDiscoveryExtensionConfig { + method?: BodyMethods; + input?: Record; + inputSchema?: Record; + bodyType: "json" | "form-data" | "text"; + output?: { + example?: unknown; + schema?: Record; + }; +} + +export type DeclareDiscoveryExtensionConfig = + | DeclareQueryDiscoveryExtensionConfig + | DeclareBodyDiscoveryExtensionConfig; + +/** + * Distributive Omit - properly distributes Omit over union types. + * + * Standard `Omit` collapses to common properties only, + * losing discriminant properties like `bodyType`. + * + * This type uses conditional type distribution to preserve the union: + * `DistributiveOmit` = `Omit | Omit` + */ +export type DistributiveOmit = T extends T ? Omit : never; + +/** + * Config type for declareDiscoveryExtension function. + * Uses DistributiveOmit to preserve bodyType discriminant in the union. + */ +export type DeclareDiscoveryExtensionInput = DistributiveOmit< + DeclareDiscoveryExtensionConfig, + "method" +>; + +export const isQueryExtensionConfig = ( + config: DeclareDiscoveryExtensionConfig, +): config is DeclareQueryDiscoveryExtensionConfig => { + return !("bodyType" in config); +}; + +export const isBodyExtensionConfig = ( + config: DeclareDiscoveryExtensionConfig, +): config is DeclareBodyDiscoveryExtensionConfig => { + return "bodyType" in config; +}; diff --git a/typescript/packages/extensions/src/bazaar/v1/facilitator.ts b/typescript/packages/extensions/src/bazaar/v1/facilitator.ts new file mode 100644 index 0000000..041635d --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/v1/facilitator.ts @@ -0,0 +1,297 @@ +/** + * V1 Facilitator functions for extracting Bazaar discovery information + * + * In v1, discovery information is stored in the `outputSchema` field + * of PaymentRequirements, which has a different structure than v2. + * + * This module transforms v1 data into v2 DiscoveryInfo format. + */ + +import type { PaymentRequirementsV1 } from "@x402/core/types"; +import type { BodyMethods, QueryParamMethods } from "@x402/core/http"; +import type { DiscoveryInfo, QueryDiscoveryInfo, BodyDiscoveryInfo } from "../types"; + +/** + * Type guard to check if an object has the v1 outputSchema structure + * + * @param obj - The object to check + * @returns True if object has v1 outputSchema structure + */ +function hasV1OutputSchema( + obj: unknown, +): obj is { input: Record; output?: Record } { + return ( + obj !== null && + typeof obj === "object" && + "input" in obj && + obj.input !== null && + typeof obj.input === "object" && + "type" in obj.input && + obj.input.type === "http" && + "method" in obj.input + ); +} + +/** + * Checks if a method is a query parameter method + * + * @param method - HTTP method string to check + * @returns True if method is GET, HEAD, or DELETE + */ +function isQueryMethod(method: string): method is QueryParamMethods { + const upperMethod = method.toUpperCase(); + return upperMethod === "GET" || upperMethod === "HEAD" || upperMethod === "DELETE"; +} + +/** + * Checks if a method is a body method + * + * @param method - HTTP method string to check + * @returns True if method is POST, PUT, or PATCH + */ +function isBodyMethod(method: string): method is BodyMethods { + const upperMethod = method.toUpperCase(); + return upperMethod === "POST" || upperMethod === "PUT" || upperMethod === "PATCH"; +} + +/** + * Extracts query parameters from v1 input, making smart assumptions + * about common field names used in v1 + * + * @param v1Input - V1 input object from payment requirements + * @returns Extracted query parameters or undefined + */ +function extractQueryParams(v1Input: Record): Record | undefined { + // Check various common field names used in v1 (both camelCase and snake_case) + if (v1Input.queryParams && typeof v1Input.queryParams === "object") { + return v1Input.queryParams as Record; + } + if (v1Input.query_params && typeof v1Input.query_params === "object") { + return v1Input.query_params as Record; + } + if (v1Input.query && typeof v1Input.query === "object") { + return v1Input.query as Record; + } + if (v1Input.params && typeof v1Input.params === "object") { + return v1Input.params as Record; + } + return undefined; +} + +/** + * Extracts body information from v1 input, making smart assumptions + * + * @param v1Input - V1 input object from payment requirements + * @returns Object containing body content and bodyType + */ +function extractBodyInfo(v1Input: Record): { + body: Record; + bodyType: "json" | "form-data" | "text"; +} { + // Determine body type (check both camelCase and snake_case) + let bodyType: "json" | "form-data" | "text" = "json"; + const bodyTypeField = v1Input.bodyType || v1Input.body_type; + + if (bodyTypeField && typeof bodyTypeField === "string") { + const type = bodyTypeField.toLowerCase(); + if (type.includes("form") || type.includes("multipart")) { + bodyType = "form-data"; + } else if (type.includes("text") || type.includes("plain")) { + bodyType = "text"; + } else { + bodyType = "json"; + } + } + + // Extract body content from various possible fields + // Priority order based on observed patterns in real data + let body: Record = {}; + + if (v1Input.bodyFields && typeof v1Input.bodyFields === "object") { + body = v1Input.bodyFields as Record; + } else if ( + v1Input.body_fields && + v1Input.body_fields !== null && + typeof v1Input.body_fields === "object" + ) { + body = v1Input.body_fields as Record; + } else if (v1Input.bodyParams && typeof v1Input.bodyParams === "object") { + body = v1Input.bodyParams as Record; + } else if (v1Input.body && typeof v1Input.body === "object") { + body = v1Input.body as Record; + } else if (v1Input.data && typeof v1Input.data === "object") { + body = v1Input.data as Record; + } else if (v1Input.properties && typeof v1Input.properties === "object") { + // Some endpoints have properties directly at the input level + body = v1Input.properties as Record; + } + + return { body, bodyType }; +} + +/** + * Extracts discovery info from v1 PaymentRequirements and transforms to v2 format + * + * In v1, the discovery information is stored in the `outputSchema` field, + * which contains both input (endpoint shape) and output (response schema) information. + * + * This function makes smart assumptions to normalize v1 data into v2 DiscoveryInfo format: + * - For GET/HEAD/DELETE: Looks for queryParams, query, or params fields + * - For POST/PUT/PATCH: Looks for bodyFields, body, or data fields and normalizes bodyType + * - Extracts optional headers if present + * + * @param paymentRequirements - V1 payment requirements + * @returns Discovery info in v2 format if present and valid, or null if not discoverable + * + * @example + * ```typescript + * const requirements: PaymentRequirementsV1 = { + * scheme: "exact", + * network: "eip155:8453", + * maxAmountRequired: "100000", + * resource: "https://api.example.com/data", + * description: "Get data", + * mimeType: "application/json", + * outputSchema: { + * input: { + * type: "http", + * method: "GET", + * discoverable: true, + * queryParams: { query: "string" } + * }, + * output: { type: "object" } + * }, + * payTo: "0x...", + * maxTimeoutSeconds: 300, + * asset: "0x...", + * extra: {} + * }; + * + * const info = extractDiscoveryInfoV1(requirements); + * if (info) { + * console.log("Endpoint method:", info.input.method); + * } + * ``` + */ +export function extractDiscoveryInfoV1( + paymentRequirements: PaymentRequirementsV1, +): DiscoveryInfo | null { + const { outputSchema } = paymentRequirements; + + // Check if outputSchema exists and has the expected structure + if (!outputSchema || !hasV1OutputSchema(outputSchema)) { + return null; + } + + const v1Input = outputSchema.input; + + // Check if the endpoint is marked as discoverable + // Default to true if not specified (for backwards compatibility) + const isDiscoverable = v1Input.discoverable ?? true; + + if (!isDiscoverable) { + return null; + } + + const method = typeof v1Input.method === "string" ? v1Input.method.toUpperCase() : ""; + + // Extract headers if present (check both camelCase and snake_case) + const headersRaw = v1Input.headerFields || v1Input.header_fields || v1Input.headers; + const headers = + headersRaw && typeof headersRaw === "object" + ? (headersRaw as Record) + : undefined; + + // Extract output example/schema if present + const output = outputSchema.output + ? { + type: "json" as const, + example: outputSchema.output, + } + : undefined; + + // Transform based on method type + if (isQueryMethod(method)) { + // Query parameter method (GET, HEAD, DELETE) + const queryParams = extractQueryParams(v1Input); + + const discoveryInfo: QueryDiscoveryInfo = { + input: { + type: "http", + method: method as QueryParamMethods, + ...(queryParams ? { queryParams } : {}), + ...(headers ? { headers } : {}), + }, + ...(output ? { output } : {}), + }; + + return discoveryInfo; + } else if (isBodyMethod(method)) { + // Body method (POST, PUT, PATCH) + const { body, bodyType } = extractBodyInfo(v1Input); + const queryParams = extractQueryParams(v1Input); // Some POST requests also have query params + + const discoveryInfo: BodyDiscoveryInfo = { + input: { + type: "http", + method: method as BodyMethods, + bodyType, + body, + ...(queryParams ? { queryParams } : {}), + ...(headers ? { headers } : {}), + }, + ...(output ? { output } : {}), + }; + + return discoveryInfo; + } + + // Unsupported method, return null + return null; +} + +/** + * Checks if v1 PaymentRequirements contains discoverable information + * + * @param paymentRequirements - V1 payment requirements + * @returns True if the requirements contain valid discovery info + * + * @example + * ```typescript + * if (isDiscoverableV1(requirements)) { + * const info = extractDiscoveryInfoV1(requirements); + * // Catalog info in Bazaar + * } + * ``` + */ +export function isDiscoverableV1(paymentRequirements: PaymentRequirementsV1): boolean { + return extractDiscoveryInfoV1(paymentRequirements) !== null; +} + +/** + * Extracts resource metadata from v1 PaymentRequirements + * + * In v1, resource information is embedded directly in the payment requirements + * rather than in a separate resource object. + * + * @param paymentRequirements - V1 payment requirements + * @returns Resource metadata + * + * @example + * ```typescript + * const metadata = extractResourceMetadataV1(requirements); + * console.log("Resource URL:", metadata.url); + * console.log("Description:", metadata.description); + * ``` + */ +export function extractResourceMetadataV1(paymentRequirements: PaymentRequirementsV1): { + url: string; + description: string; + mimeType: string; +} { + return { + url: paymentRequirements.resource, + description: paymentRequirements.description, + mimeType: paymentRequirements.mimeType, + }; +} diff --git a/typescript/packages/extensions/src/bazaar/v1/index.ts b/typescript/packages/extensions/src/bazaar/v1/index.ts new file mode 100644 index 0000000..2d4a80f --- /dev/null +++ b/typescript/packages/extensions/src/bazaar/v1/index.ts @@ -0,0 +1,8 @@ +/** + * V1 Bazaar Discovery Extension + * + * Provides functions for extracting discovery information from v1 PaymentRequirements + * and transforming it to v2 DiscoveryInfo format. + */ + +export { extractDiscoveryInfoV1, isDiscoverableV1, extractResourceMetadataV1 } from "./facilitator"; diff --git a/typescript/packages/extensions/src/index.ts b/typescript/packages/extensions/src/index.ts new file mode 100644 index 0000000..f51b8a8 --- /dev/null +++ b/typescript/packages/extensions/src/index.ts @@ -0,0 +1,13 @@ +// Shared extension utilities +export { WithExtensions } from "./types"; + +// Bazaar extension +export * from "./bazaar"; +export { bazaarResourceServerExtension } from "./bazaar/server"; + +// Sign-in-with-x extension +export * from "./sign-in-with-x"; + +// Payment-identifier extension +export * from "./payment-identifier"; +export { paymentIdentifierResourceServerExtension } from "./payment-identifier/resourceServer"; diff --git a/typescript/packages/extensions/src/payment-identifier/client.ts b/typescript/packages/extensions/src/payment-identifier/client.ts new file mode 100644 index 0000000..5cf747a --- /dev/null +++ b/typescript/packages/extensions/src/payment-identifier/client.ts @@ -0,0 +1,68 @@ +/** + * Client-side utilities for the Payment-Identifier Extension + */ + +import { PAYMENT_IDENTIFIER } from "./types"; +import { generatePaymentId, isValidPaymentId } from "./utils"; +import { isPaymentIdentifierExtension } from "./validation"; + +/** + * Appends a payment identifier to the extensions object if the server declared support. + * + * This function reads the server's `payment-identifier` declaration from the extensions, + * and appends the client's ID to it. If the extension is not present (server didn't declare it), + * the extensions are returned unchanged. + * + * @param extensions - The extensions object from PaymentRequired (will be modified in place) + * @param id - Optional custom payment ID. If not provided, a new ID will be generated. + * @returns The modified extensions object (same reference as input) + * @throws Error if the provided ID is invalid + * + * @example + * ```typescript + * import { appendPaymentIdentifierToExtensions } from '@x402/extensions/payment-identifier'; + * + * // Get extensions from server's PaymentRequired response + * const extensions = paymentRequired.extensions ?? {}; + * + * // Append a generated ID (only if server declared payment-identifier) + * appendPaymentIdentifierToExtensions(extensions); + * + * // Or use a custom ID + * appendPaymentIdentifierToExtensions(extensions, "pay_my_custom_id_12345"); + * + * // Include in PaymentPayload + * const paymentPayload = { + * x402Version: 2, + * resource: paymentRequired.resource, + * accepted: selectedPaymentOption, + * payload: { ... }, + * extensions + * }; + * ``` + */ +export function appendPaymentIdentifierToExtensions( + extensions: Record, + id?: string, +): Record { + const extension = extensions[PAYMENT_IDENTIFIER]; + + // Only append if the server declared this extension with valid structure + if (!isPaymentIdentifierExtension(extension)) { + return extensions; + } + + const paymentId = id ?? generatePaymentId(); + + if (!isValidPaymentId(paymentId)) { + throw new Error( + `Invalid payment ID: "${paymentId}". ` + + `ID must be 16-128 characters and contain only alphanumeric characters, hyphens, and underscores.`, + ); + } + + // Append the ID to the existing extension info + extension.info.id = paymentId; + + return extensions; +} diff --git a/typescript/packages/extensions/src/payment-identifier/index.ts b/typescript/packages/extensions/src/payment-identifier/index.ts new file mode 100644 index 0000000..83eb8f3 --- /dev/null +++ b/typescript/packages/extensions/src/payment-identifier/index.ts @@ -0,0 +1,114 @@ +/** + * Payment-Identifier Extension for x402 v2 + * + * Enables clients to provide an idempotency key (`id`) that resource servers + * can use for deduplication of payment requests. + * + * ## Usage + * + * ### For Resource Servers + * + * ```typescript + * import { + * declarePaymentIdentifierExtension, + * PAYMENT_IDENTIFIER + * } from '@x402/extensions/payment-identifier'; + * + * // Advertise support in PaymentRequired response (optional identifier) + * const paymentRequired = { + * x402Version: 2, + * resource: { ... }, + * accepts: [ ... ], + * extensions: { + * [PAYMENT_IDENTIFIER]: declarePaymentIdentifierExtension() + * } + * }; + * + * // Require payment identifier + * const paymentRequiredStrict = { + * x402Version: 2, + * resource: { ... }, + * accepts: [ ... ], + * extensions: { + * [PAYMENT_IDENTIFIER]: declarePaymentIdentifierExtension(true) + * } + * }; + * ``` + * + * ### For Clients + * + * ```typescript + * import { appendPaymentIdentifierToExtensions } from '@x402/extensions/payment-identifier'; + * + * // Get extensions from server's PaymentRequired response + * const extensions = { ...paymentRequired.extensions }; + * + * // Append payment ID (only if server declared the extension) + * appendPaymentIdentifierToExtensions(extensions); + * + * // Include in PaymentPayload + * const paymentPayload = { + * x402Version: 2, + * resource: paymentRequired.resource, + * accepted: selectedPaymentOption, + * payload: { ... }, + * extensions + * }; + * ``` + * + * ### For Idempotency Implementation + * + * ```typescript + * import { extractPaymentIdentifier } from '@x402/extensions/payment-identifier'; + * + * // In your settle handler + * const id = extractPaymentIdentifier(paymentPayload); + * if (id) { + * const cached = await idempotencyStore.get(id); + * if (cached) { + * return cached; // Return cached response + * } + * } + * ``` + */ + +// Export types +export type { + PaymentIdentifierInfo, + PaymentIdentifierExtension, + PaymentIdentifierSchema, +} from "./types"; + +export { + PAYMENT_IDENTIFIER, + PAYMENT_ID_MIN_LENGTH, + PAYMENT_ID_MAX_LENGTH, + PAYMENT_ID_PATTERN, +} from "./types"; + +// Export schema +export { paymentIdentifierSchema } from "./schema"; + +// Export utilities +export { generatePaymentId, isValidPaymentId } from "./utils"; + +// Export client functions +export { appendPaymentIdentifierToExtensions } from "./client"; + +// Export resource server functions +export { + declarePaymentIdentifierExtension, + paymentIdentifierResourceServerExtension, +} from "./resourceServer"; + +// Export validation and extraction functions +export { + isPaymentIdentifierExtension, + validatePaymentIdentifier, + extractPaymentIdentifier, + extractAndValidatePaymentIdentifier, + hasPaymentIdentifier, + isPaymentIdentifierRequired, + validatePaymentIdentifierRequirement, + type PaymentIdentifierValidationResult, +} from "./validation"; diff --git a/typescript/packages/extensions/src/payment-identifier/resourceServer.ts b/typescript/packages/extensions/src/payment-identifier/resourceServer.ts new file mode 100644 index 0000000..0e6c365 --- /dev/null +++ b/typescript/packages/extensions/src/payment-identifier/resourceServer.ts @@ -0,0 +1,73 @@ +/** + * Resource Server utilities for the Payment-Identifier Extension + */ + +import type { ResourceServerExtension } from "@x402/core/types"; +import type { PaymentIdentifierExtension } from "./types"; +import { PAYMENT_IDENTIFIER } from "./types"; +import { paymentIdentifierSchema } from "./schema"; + +/** + * Declares the payment-identifier extension for inclusion in PaymentRequired.extensions. + * + * Resource servers call this function to advertise support for payment identifiers. + * The declaration indicates whether a payment identifier is required and includes + * the schema that clients must follow. + * + * @param required - Whether clients must provide a payment identifier. Defaults to false. + * @returns A PaymentIdentifierExtension object ready for PaymentRequired.extensions + * + * @example + * ```typescript + * import { declarePaymentIdentifierExtension, PAYMENT_IDENTIFIER } from '@x402/extensions/payment-identifier'; + * + * // Include in PaymentRequired response (optional identifier) + * const paymentRequired = { + * x402Version: 2, + * resource: { ... }, + * accepts: [ ... ], + * extensions: { + * [PAYMENT_IDENTIFIER]: declarePaymentIdentifierExtension() + * } + * }; + * + * // Require payment identifier + * const paymentRequiredStrict = { + * x402Version: 2, + * resource: { ... }, + * accepts: [ ... ], + * extensions: { + * [PAYMENT_IDENTIFIER]: declarePaymentIdentifierExtension(true) + * } + * }; + * ``` + */ +export function declarePaymentIdentifierExtension( + required: boolean = false, +): PaymentIdentifierExtension { + return { + info: { required }, + schema: paymentIdentifierSchema, + }; +} + +/** + * ResourceServerExtension implementation for payment-identifier. + * + * This extension doesn't require any enrichment hooks since the declaration + * is static. It's provided for consistency with other extensions and for + * potential future use with the extension registration system. + * + * @example + * ```typescript + * import { paymentIdentifierResourceServerExtension } from '@x402/extensions/payment-identifier'; + * + * resourceServer.registerExtension(paymentIdentifierResourceServerExtension); + * ``` + */ +export const paymentIdentifierResourceServerExtension: ResourceServerExtension = { + key: PAYMENT_IDENTIFIER, + + // No enrichment needed - the declaration is static + // Future hooks for idempotency could be added here if needed +}; diff --git a/typescript/packages/extensions/src/payment-identifier/schema.ts b/typescript/packages/extensions/src/payment-identifier/schema.ts new file mode 100644 index 0000000..30fa6d1 --- /dev/null +++ b/typescript/packages/extensions/src/payment-identifier/schema.ts @@ -0,0 +1,27 @@ +/** + * JSON Schema definitions for the Payment-Identifier Extension + */ + +import type { PaymentIdentifierSchema } from "./types"; +import { PAYMENT_ID_MIN_LENGTH, PAYMENT_ID_MAX_LENGTH } from "./types"; + +/** + * JSON Schema for validating payment identifier info. + * Compliant with JSON Schema Draft 2020-12. + */ +export const paymentIdentifierSchema: PaymentIdentifierSchema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + required: { + type: "boolean", + }, + id: { + type: "string", + minLength: PAYMENT_ID_MIN_LENGTH, + maxLength: PAYMENT_ID_MAX_LENGTH, + pattern: "^[a-zA-Z0-9_-]+$", + }, + }, + required: ["required"], +}; diff --git a/typescript/packages/extensions/src/payment-identifier/types.ts b/typescript/packages/extensions/src/payment-identifier/types.ts new file mode 100644 index 0000000..ffaefbe --- /dev/null +++ b/typescript/packages/extensions/src/payment-identifier/types.ts @@ -0,0 +1,82 @@ +/** + * Type definitions for the Payment-Identifier Extension + * + * Enables clients to provide an idempotency key that resource servers + * can use for deduplication of payment requests. + */ + +/** + * Extension identifier constant for the payment-identifier extension + */ +export const PAYMENT_IDENTIFIER = "payment-identifier"; + +/** + * Minimum length for payment identifier + */ +export const PAYMENT_ID_MIN_LENGTH = 16; + +/** + * Maximum length for payment identifier + */ +export const PAYMENT_ID_MAX_LENGTH = 128; + +/** + * Pattern for valid payment identifier characters (alphanumeric, hyphens, underscores) + */ +export const PAYMENT_ID_PATTERN = /^[a-zA-Z0-9_-]+$/; + +/** + * Payment identifier info containing the required flag and client-provided ID + */ +export interface PaymentIdentifierInfo { + /** + * Whether the server requires clients to include a payment identifier. + * When true, clients must provide an `id` or receive a 400 Bad Request. + */ + required: boolean; + + /** + * Client-provided unique identifier for idempotency. + * Must be 16-128 characters, alphanumeric with hyphens and underscores allowed. + */ + id?: string; +} + +/** + * Payment identifier extension with info and schema. + * + * Used both for server-side declarations (info without id) and + * client-side payloads (info with id). + */ +export interface PaymentIdentifierExtension { + /** + * The payment identifier info. + * Server declarations have required only, clients add the id. + */ + info: PaymentIdentifierInfo; + + /** + * JSON Schema validating the info structure + */ + schema: PaymentIdentifierSchema; +} + +/** + * JSON Schema type for the payment-identifier extension + */ +export interface PaymentIdentifierSchema { + $schema: "https://json-schema.org/draft/2020-12/schema"; + type: "object"; + properties: { + required: { + type: "boolean"; + }; + id: { + type: "string"; + minLength: number; + maxLength: number; + pattern: string; + }; + }; + required: ["required"]; +} diff --git a/typescript/packages/extensions/src/payment-identifier/utils.ts b/typescript/packages/extensions/src/payment-identifier/utils.ts new file mode 100644 index 0000000..0c2a474 --- /dev/null +++ b/typescript/packages/extensions/src/payment-identifier/utils.ts @@ -0,0 +1,54 @@ +/** + * Utility functions for the Payment-Identifier Extension + */ + +import { PAYMENT_ID_MIN_LENGTH, PAYMENT_ID_MAX_LENGTH, PAYMENT_ID_PATTERN } from "./types"; + +/** + * Generates a unique payment identifier. + * + * @param prefix - Optional prefix for the ID (e.g., "pay_"). Defaults to "pay_". + * @returns A unique payment identifier string + * + * @example + * ```typescript + * // With default prefix + * const id = generatePaymentId(); // "pay_7d5d747be160e280504c099d984bcfe0" + * + * // With custom prefix + * const id = generatePaymentId("txn_"); // "txn_7d5d747be160e280504c099d984bcfe0" + * + * // Without prefix + * const id = generatePaymentId(""); // "7d5d747be160e280504c099d984bcfe0" + * ``` + */ +export function generatePaymentId(prefix: string = "pay_"): string { + // Generate UUID v4 without hyphens (32 hex chars) + const uuid = crypto.randomUUID().replace(/-/g, ""); + return `${prefix}${uuid}`; +} + +/** + * Validates that a payment ID meets the format requirements. + * + * @param id - The payment ID to validate + * @returns True if the ID is valid, false otherwise + * + * @example + * ```typescript + * isValidPaymentId("pay_7d5d747be160e280"); // true (exactly 16 chars after prefix removal check) + * isValidPaymentId("abc"); // false (too short) + * isValidPaymentId("pay_abc!@#"); // false (invalid characters) + * ``` + */ +export function isValidPaymentId(id: string): boolean { + if (typeof id !== "string") { + return false; + } + + if (id.length < PAYMENT_ID_MIN_LENGTH || id.length > PAYMENT_ID_MAX_LENGTH) { + return false; + } + + return PAYMENT_ID_PATTERN.test(id); +} diff --git a/typescript/packages/extensions/src/payment-identifier/validation.ts b/typescript/packages/extensions/src/payment-identifier/validation.ts new file mode 100644 index 0000000..97511b9 --- /dev/null +++ b/typescript/packages/extensions/src/payment-identifier/validation.ts @@ -0,0 +1,326 @@ +/** + * Validation and extraction utilities for the Payment-Identifier Extension + */ + +import Ajv from "ajv/dist/2020.js"; +import type { PaymentPayload } from "@x402/core/types"; +import type { PaymentIdentifierExtension, PaymentIdentifierInfo } from "./types"; +import { PAYMENT_IDENTIFIER } from "./types"; +import { paymentIdentifierSchema } from "./schema"; +import { isValidPaymentId } from "./utils"; + +/** + * Type guard to check if an object is a valid payment-identifier extension structure. + * + * This checks for the basic structure (info object with required boolean), + * but does not validate the id format if present. + * + * @param extension - The object to check + * @returns True if the object has the expected payment-identifier extension structure + * + * @example + * ```typescript + * if (isPaymentIdentifierExtension(extensions["payment-identifier"])) { + * // TypeScript knows this is PaymentIdentifierExtension + * console.log(extension.info.required); + * } + * ``` + */ +export function isPaymentIdentifierExtension( + extension: unknown, +): extension is PaymentIdentifierExtension { + if (!extension || typeof extension !== "object") { + return false; + } + + const ext = extension as Partial; + + if (!ext.info || typeof ext.info !== "object") { + return false; + } + + const info = ext.info as Partial; + + // Must have required boolean + if (typeof info.required !== "boolean") { + return false; + } + + return true; +} + +/** + * Result of payment identifier validation + */ +export interface PaymentIdentifierValidationResult { + /** + * Whether the payment identifier is valid + */ + valid: boolean; + + /** + * Error messages if validation failed + */ + errors?: string[]; +} + +/** + * Validates a payment-identifier extension object. + * + * Checks both the structure (using JSON Schema) and the ID format. + * + * @param extension - The extension object to validate + * @returns Validation result with errors if invalid + * + * @example + * ```typescript + * const result = validatePaymentIdentifier(paymentPayload.extensions?.["payment-identifier"]); + * if (!result.valid) { + * console.error("Invalid payment identifier:", result.errors); + * } + * ``` + */ +export function validatePaymentIdentifier(extension: unknown): PaymentIdentifierValidationResult { + if (!extension || typeof extension !== "object") { + return { + valid: false, + errors: ["Extension must be an object"], + }; + } + + const ext = extension as Partial; + + // Check info exists + if (!ext.info || typeof ext.info !== "object") { + return { + valid: false, + errors: ["Extension must have an 'info' property"], + }; + } + + const info = ext.info as Partial; + + // Check required field exists and is a boolean + if (typeof info.required !== "boolean") { + return { + valid: false, + errors: ["Extension info must have a 'required' boolean property"], + }; + } + + // Check id exists and is a string (if provided) + if (info.id !== undefined && typeof info.id !== "string") { + return { + valid: false, + errors: ["Extension info 'id' must be a string if provided"], + }; + } + + // Validate ID format if provided + if (info.id !== undefined && !isValidPaymentId(info.id)) { + return { + valid: false, + errors: [ + `Invalid payment ID format. ID must be 16-128 characters and contain only alphanumeric characters, hyphens, and underscores.`, + ], + }; + } + + // If schema is provided, validate against it + if (ext.schema) { + try { + const ajv = new Ajv({ strict: false, allErrors: true }); + const validate = ajv.compile(ext.schema); + const valid = validate(ext.info); + + if (!valid && validate.errors) { + const errors = validate.errors?.map(err => { + const path = err.instancePath || "(root)"; + return `${path}: ${err.message}`; + }) || ["Unknown validation error"]; + + return { valid: false, errors }; + } + } catch (error) { + return { + valid: false, + errors: [ + `Schema validation failed: ${error instanceof Error ? error.message : String(error)}`, + ], + }; + } + } + + return { valid: true }; +} + +/** + * Extracts the payment identifier from a PaymentPayload. + * + * @param paymentPayload - The payment payload to extract from + * @param validate - Whether to validate the ID before returning (default: true) + * @returns The payment ID string, or null if not present or invalid + * + * @example + * ```typescript + * const id = extractPaymentIdentifier(paymentPayload); + * if (id) { + * // Use for idempotency lookup + * const cached = await idempotencyStore.get(id); + * } + * ``` + */ +export function extractPaymentIdentifier( + paymentPayload: PaymentPayload, + validate: boolean = true, +): string | null { + if (!paymentPayload.extensions) { + return null; + } + + const extension = paymentPayload.extensions[PAYMENT_IDENTIFIER]; + + if (!extension || typeof extension !== "object") { + return null; + } + + const ext = extension as Partial; + + if (!ext.info || typeof ext.info !== "object") { + return null; + } + + const info = ext.info as Partial; + + if (typeof info.id !== "string") { + return null; + } + + if (validate && !isValidPaymentId(info.id)) { + return null; + } + + return info.id; +} + +/** + * Extracts and validates the payment identifier from a PaymentPayload. + * + * @param paymentPayload - The payment payload to extract from + * @returns Object with the ID and validation result + * + * @example + * ```typescript + * const { id, validation } = extractAndValidatePaymentIdentifier(paymentPayload); + * if (!validation.valid) { + * return res.status(400).json({ error: validation.errors }); + * } + * if (id) { + * // Use for idempotency + * } + * ``` + */ +export function extractAndValidatePaymentIdentifier(paymentPayload: PaymentPayload): { + id: string | null; + validation: PaymentIdentifierValidationResult; +} { + if (!paymentPayload.extensions) { + return { id: null, validation: { valid: true } }; + } + + const extension = paymentPayload.extensions[PAYMENT_IDENTIFIER]; + + if (!extension) { + return { id: null, validation: { valid: true } }; + } + + const validation = validatePaymentIdentifier(extension); + + if (!validation.valid) { + return { id: null, validation }; + } + + const ext = extension as PaymentIdentifierExtension; + return { id: ext.info.id ?? null, validation: { valid: true } }; +} + +/** + * Checks if a PaymentPayload contains a payment-identifier extension. + * + * @param paymentPayload - The payment payload to check + * @returns True if the extension is present + */ +export function hasPaymentIdentifier(paymentPayload: PaymentPayload): boolean { + return !!(paymentPayload.extensions && paymentPayload.extensions[PAYMENT_IDENTIFIER]); +} + +/** + * Checks if the server requires a payment identifier based on the extension info. + * + * @param extension - The payment-identifier extension from PaymentRequired or PaymentPayload + * @returns True if the server requires a payment identifier + */ +export function isPaymentIdentifierRequired(extension: unknown): boolean { + if (!extension || typeof extension !== "object") { + return false; + } + + const ext = extension as Partial; + + if (!ext.info || typeof ext.info !== "object") { + return false; + } + + return (ext.info as Partial).required === true; +} + +/** + * Validates that a payment identifier is provided when required. + * + * Use this to check if a client's PaymentPayload satisfies the server's requirement. + * + * @param paymentPayload - The client's payment payload + * @param serverRequired - Whether the server requires a payment identifier (from PaymentRequired) + * @returns Validation result - invalid if required but not provided + * + * @example + * ```typescript + * const serverExtension = paymentRequired.extensions?.["payment-identifier"]; + * const serverRequired = isPaymentIdentifierRequired(serverExtension); + * const result = validatePaymentIdentifierRequirement(paymentPayload, serverRequired); + * if (!result.valid) { + * return res.status(400).json({ error: result.errors }); + * } + * ``` + */ +export function validatePaymentIdentifierRequirement( + paymentPayload: PaymentPayload, + serverRequired: boolean, +): PaymentIdentifierValidationResult { + if (!serverRequired) { + return { valid: true }; + } + + const id = extractPaymentIdentifier(paymentPayload, false); + + if (!id) { + return { + valid: false, + errors: ["Server requires a payment identifier but none was provided"], + }; + } + + // Validate the ID format + if (!isValidPaymentId(id)) { + return { + valid: false, + errors: [ + `Invalid payment ID format. ID must be 16-128 characters and contain only alphanumeric characters, hyphens, and underscores.`, + ], + }; + } + + return { valid: true }; +} + +export { paymentIdentifierSchema }; diff --git a/typescript/packages/extensions/src/sign-in-with-x/client.ts b/typescript/packages/extensions/src/sign-in-with-x/client.ts new file mode 100644 index 0000000..f381710 --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/client.ts @@ -0,0 +1,75 @@ +/** + * Complete client flow for SIWX extension + * + * Combines message construction, signing, and payload creation. + * Supports both EVM and Solana wallets. + */ + +import type { SIWxExtensionInfo, SIWxPayload, SignatureType, SignatureScheme } from "./types"; +import type { SIWxSigner, EVMSigner, SolanaSigner } from "./sign"; +import { getEVMAddress, getSolanaAddress, signEVMMessage, signSolanaMessage } from "./sign"; +import { createSIWxMessage } from "./message"; + +/** + * Complete SIWX info with chain-specific fields. + * Used by utility functions that need the selected chain information. + */ +export type CompleteSIWxInfo = SIWxExtensionInfo & { + chainId: string; + type: SignatureType; + signatureScheme?: SignatureScheme; +}; + +/** + * Create a complete SIWX payload from server extension info with selected chain. + * + * Routes to EVM or Solana signing based on the chainId prefix: + * - `eip155:*` → EVM signing + * - `solana:*` → Solana signing + * + * @param serverExtension - Server extension info with chain selected (includes chainId, type) + * @param signer - Wallet that can sign messages (EVMSigner or SolanaSigner) + * @returns Complete SIWX payload with signature + * + * @example + * ```typescript + * // EVM wallet + * const completeInfo = { ...extension.info, chainId: "eip155:8453", type: "eip191" }; + * const payload = await createSIWxPayload(completeInfo, evmWallet); + * ``` + */ +export async function createSIWxPayload( + serverExtension: CompleteSIWxInfo, + signer: SIWxSigner, +): Promise { + const isSolana = serverExtension.chainId.startsWith("solana:"); + + // Get address and sign based on chain type + const address = isSolana + ? getSolanaAddress(signer as SolanaSigner) + : getEVMAddress(signer as EVMSigner); + + const message = createSIWxMessage(serverExtension, address); + + const signature = isSolana + ? await signSolanaMessage(message, signer as SolanaSigner) + : await signEVMMessage(message, signer as EVMSigner); + + return { + domain: serverExtension.domain, + address, + statement: serverExtension.statement, + uri: serverExtension.uri, + version: serverExtension.version, + chainId: serverExtension.chainId, + type: serverExtension.type, + nonce: serverExtension.nonce, + issuedAt: serverExtension.issuedAt, + expirationTime: serverExtension.expirationTime, + notBefore: serverExtension.notBefore, + requestId: serverExtension.requestId, + resources: serverExtension.resources, + signatureScheme: serverExtension.signatureScheme, + signature, + }; +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/declare.ts b/typescript/packages/extensions/src/sign-in-with-x/declare.ts new file mode 100644 index 0000000..2328433 --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/declare.ts @@ -0,0 +1,115 @@ +/** + * Server-side declaration helper for SIWX extension + * + * Helps servers declare SIWX authentication requirements in PaymentRequired responses. + */ + +import type { + SIWxExtension, + SIWxExtensionInfo, + DeclareSIWxOptions, + SignatureType, + SupportedChain, +} from "./types"; +import { SIGN_IN_WITH_X } from "./types"; +import { buildSIWxSchema } from "./schema"; + +/** + * Derive signature type from network. + * + * @param network - CAIP-2 network identifier + * @returns Signature algorithm type + */ +export function getSignatureType(network: string): SignatureType { + return network.startsWith("solana:") ? "ed25519" : "eip191"; +} + +/** + * Internal type for SIWX declaration with stored options. + * The _options field is used by enrichPaymentRequiredResponse to derive + * values from request context. + */ +export interface SIWxDeclaration extends SIWxExtension { + _options: DeclareSIWxOptions; +} + +/** + * Create SIWX extension declaration for PaymentRequired.extensions + * + * Most fields are derived automatically from request context when using + * siwxResourceServerExtension: + * - `network`: From payment requirements (accepts[].network) + * - `resourceUri`: From request URL + * - `domain`: Parsed from resourceUri + * + * Explicit values in options override automatic derivation. + * + * @param options - Configuration options (most are optional) + * @returns Extension object ready for PaymentRequired.extensions + * + * @example + * ```typescript + * // Minimal - derives network, domain, resourceUri from context + * const extensions = declareSIWxExtension({ + * statement: 'Sign in to access your purchased content', + * }); + * + * // With explicit network (overrides accepts) + * const extensions = declareSIWxExtension({ + * network: 'eip155:8453', + * statement: 'Sign in to access', + * }); + * + * // Full explicit config (no derivation) + * const extensions = declareSIWxExtension({ + * domain: 'api.example.com', + * resourceUri: 'https://api.example.com/data', + * network: ['eip155:8453', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], + * statement: 'Sign in to access', + * expirationSeconds: 300, + * }); + * ``` + */ +export function declareSIWxExtension( + options: DeclareSIWxOptions = {}, +): Record { + // Build partial info with static fields only + // Time-based fields (nonce, issuedAt, expirationTime) are generated + // per-request by enrichPaymentRequiredResponse in siwxResourceServerExtension + const info: Partial & { version: string } = { + version: options.version ?? "1", + }; + + // Add fields that are provided + if (options.domain) { + info.domain = options.domain; + } + if (options.resourceUri) { + info.uri = options.resourceUri; + info.resources = [options.resourceUri]; + } + if (options.statement) { + info.statement = options.statement; + } + // Note: expirationSeconds is stored in _options and used by + // enrichPaymentRequiredResponse to calculate expirationTime per-request + + // Build supportedChains if network is provided + let supportedChains: SupportedChain[] = []; + if (options.network) { + const networks = Array.isArray(options.network) ? options.network : [options.network]; + supportedChains = networks.map(network => ({ + chainId: network, + type: getSignatureType(network), + })); + } + + const declaration: SIWxDeclaration = { + info: info as SIWxExtensionInfo, + supportedChains, + schema: buildSIWxSchema(), + _options: options, + }; + + return { [SIGN_IN_WITH_X]: declaration }; +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/encode.ts b/typescript/packages/extensions/src/sign-in-with-x/encode.ts new file mode 100644 index 0000000..dd59142 --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/encode.ts @@ -0,0 +1,31 @@ +/** + * Header encoding for SIWX extension + * + * Encodes SIWX payload for the SIGN-IN-WITH-X HTTP header. + * Per CHANGELOG-v2.md line 335: header should be base64-encoded. + */ + +import { safeBase64Encode } from "@x402/core/utils"; +import type { SIWxPayload } from "./types"; + +/** + * Encode SIWX payload for SIGN-IN-WITH-X header. + * + * Uses base64 encoding per x402 v2 spec (CHANGELOG-v2.md line 335). + * + * @param payload - Complete SIWX payload with signature + * @returns Base64-encoded JSON string + * + * @example + * ```typescript + * const payload = await createSIWxPayload(serverInfo, signer); + * const header = encodeSIWxHeader(payload); + * + * fetch(url, { + * headers: { 'SIGN-IN-WITH-X': header } + * }); + * ``` + */ +export function encodeSIWxHeader(payload: SIWxPayload): string { + return safeBase64Encode(JSON.stringify(payload)); +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/evm.ts b/typescript/packages/extensions/src/sign-in-with-x/evm.ts new file mode 100644 index 0000000..8dfae8b --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/evm.ts @@ -0,0 +1,175 @@ +/** + * EVM Sign-In-With-Ethereum (SIWE) support + * + * Implements EIP-4361 compliant message format and signature verification + * for EVM chains (Ethereum, Base, Polygon, etc.) + */ + +import { verifyMessage } from "viem"; +import { SiweMessage } from "siwe"; +import type { EVMMessageVerifier } from "./types"; +import type { CompleteSIWxInfo } from "./client"; +import type { SIWxSigner } from "./sign"; + +/** + * Extract numeric chain ID from CAIP-2 EVM chainId. + * + * @param chainId - CAIP-2 format chain ID (e.g., "eip155:8453") + * @returns Numeric chain ID (e.g., 8453) + * @throws Error if chainId format is invalid + * + * @example + * ```typescript + * extractEVMChainId("eip155:1") // 1 (Ethereum mainnet) + * extractEVMChainId("eip155:8453") // 8453 (Base) + * extractEVMChainId("eip155:137") // 137 (Polygon) + * ``` + */ +export function extractEVMChainId(chainId: string): number { + const match = /^eip155:(\d+)$/.exec(chainId); + if (!match) { + throw new Error(`Invalid EVM chainId format: ${chainId}. Expected eip155:`); + } + return parseInt(match[1], 10); +} + +/** + * Format SIWE message following EIP-4361 specification. + * + * Uses the siwe library to ensure message format matches verification. + * + * @param info - Server-provided extension info + * @param address - Client's EVM wallet address (0x-prefixed) + * @returns Message string ready for signing + * + * @example + * ```typescript + * const message = formatSIWEMessage(serverInfo, "0x1234...abcd"); + * // Returns EIP-4361 formatted message: + * // "api.example.com wants you to sign in with your Ethereum account: + * // 0x1234...abcd + * // + * // Sign in to access your content + * // + * // URI: https://api.example.com/data + * // Version: 1 + * // Chain ID: 8453 + * // Nonce: abc123 + * // Issued At: 2024-01-01T00:00:00.000Z" + * ``` + */ +export function formatSIWEMessage(info: CompleteSIWxInfo, address: string): string { + const numericChainId = extractEVMChainId(info.chainId); + + const siweMessage = new SiweMessage({ + domain: info.domain, + address, + statement: info.statement, + uri: info.uri, + version: info.version, + chainId: numericChainId, + nonce: info.nonce, + issuedAt: info.issuedAt, + expirationTime: info.expirationTime, + notBefore: info.notBefore, + requestId: info.requestId, + resources: info.resources, + }); + + return siweMessage.prepareMessage(); +} + +/** + * Verify EVM signature. + * + * Supports: + * - EOA signatures (standard ECDSA via EIP-191) - always available + * - EIP-1271 (deployed smart contract wallets) - requires verifier + * - EIP-6492 (counterfactual/pre-deploy smart wallets) - requires verifier + * + * @param message - The SIWE message that was signed + * @param address - The claimed signer address + * @param signature - The signature to verify + * @param verifier - Optional message verifier for smart wallet support. + * Pass publicClient.verifyMessage for EIP-1271/EIP-6492 support. + * Without this, only EOA signatures are verified. + * @returns true if signature is valid + * + * @example + * ```typescript + * // EOA-only verification (default, no RPC required) + * const valid = await verifyEVMSignature(message, address, signature); + * + * // Smart wallet verification with viem PublicClient + * import { createPublicClient, http } from 'viem'; + * import { base } from 'viem/chains'; + * + * const publicClient = createPublicClient({ chain: base, transport: http() }); + * const valid = await verifyEVMSignature( + * message, + * address, + * signature, + * publicClient.verifyMessage + * ); + * ``` + */ +export async function verifyEVMSignature( + message: string, + address: string, + signature: string, + verifier?: EVMMessageVerifier, +): Promise { + const args = { + address: address as `0x${string}`, + message, + signature: signature as `0x${string}`, + }; + + if (verifier) { + // Use provided verifier (supports EIP-1271/EIP-6492 via RPC) + return verifier(args); + } + + // Fallback to standalone verifyMessage (EOA only, no RPC) + return verifyMessage(args); +} + +/** + * Detect if a signer is EVM-compatible. + * Checks for EVM-specific properties. + * + * @param signer - The signer to check + * @returns true if the signer is an EVM signer + */ +export function isEVMSigner(signer: SIWxSigner): boolean { + // Check for Solana-specific properties first (to exclude them) + // signMessages (plural) is only on @solana/kit signers + if ("signMessages" in signer && typeof signer.signMessages === "function") { + return false; + } + // Check for Solana wallet adapter publicKey (has toBase58 or is non-hex string) + if ("publicKey" in signer && signer.publicKey) { + const pk = signer.publicKey; + if (typeof pk === "object" && pk !== null && "toBase58" in pk) { + return false; + } + if (typeof pk === "string" && !pk.startsWith("0x")) { + return false; + } + } + // EVM signers have account.address or direct address property (hex format) + if ("account" in signer && signer.account && typeof signer.account === "object") { + const account = signer.account as { address?: string }; + if (account.address && account.address.startsWith("0x")) { + return true; + } + } + if ( + "address" in signer && + typeof signer.address === "string" && + signer.address.startsWith("0x") + ) { + return true; + } + return false; +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/fetch.ts b/typescript/packages/extensions/src/sign-in-with-x/fetch.ts new file mode 100644 index 0000000..718942c --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/fetch.ts @@ -0,0 +1,101 @@ +/** + * Fetch wrapper for SIWX authentication. + * + * Provides a convenient wrapper around fetch that automatically handles + * SIWX authentication when a 402 response includes SIWX extension info. + */ + +import { decodePaymentRequiredHeader } from "@x402/core/http"; +import type { SIWxSigner } from "./sign"; +import type { SIWxExtension } from "./types"; +import { SIGN_IN_WITH_X } from "./types"; +import { createSIWxPayload } from "./client"; +import { encodeSIWxHeader } from "./encode"; + +/** + * Wraps fetch to automatically handle SIWX authentication. + * + * When a 402 response is received with a SIWX extension: + * 1. Extracts SIWX info from PAYMENT-REQUIRED header + * 2. Creates signed SIWX proof using the provided signer + * 3. Retries the request with the SIWX header + * + * If the 402 response doesn't include SIWX extension info, the original + * response is returned unchanged (allowing payment handling to proceed). + * + * @param fetch - The fetch function to wrap (typically globalThis.fetch) + * @param signer - Wallet signer (EVMSigner or SolanaSigner) + * @returns A wrapped fetch function that handles SIWX authentication + * + * @example + * ```typescript + * import { wrapFetchWithSIWx } from '@x402/extensions/sign-in-with-x'; + * import { privateKeyToAccount } from 'viem/accounts'; + * + * const signer = privateKeyToAccount(privateKey); + * const fetchWithSIWx = wrapFetchWithSIWx(fetch, signer); + * + * // Request that may require SIWX auth (for returning paid users) + * const response = await fetchWithSIWx('https://api.example.com/data'); + * ``` + */ +export function wrapFetchWithSIWx(fetch: typeof globalThis.fetch, signer: SIWxSigner) { + return async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const request = new Request(input, init); + const clonedRequest = request.clone(); + + const response = await fetch(request); + + if (response.status !== 402) { + return response; + } + + // Extract SIWX info from 402 response + const paymentRequiredHeader = response.headers.get("PAYMENT-REQUIRED"); + if (!paymentRequiredHeader) { + return response; // No PAYMENT-REQUIRED header, return original response + } + + const paymentRequired = decodePaymentRequiredHeader(paymentRequiredHeader); + const siwxExtension = paymentRequired.extensions?.[SIGN_IN_WITH_X] as SIWxExtension | undefined; + + if (!siwxExtension?.supportedChains) { + return response; // Server doesn't support SIWX, return original 402 + } + + // Prevent infinite loops + if (clonedRequest.headers.has(SIGN_IN_WITH_X)) { + throw new Error("SIWX authentication already attempted"); + } + + // Get network from payment requirements + const paymentNetwork = paymentRequired.accepts?.[0]?.network; + if (!paymentNetwork) { + return response; // No network in payment requirements + } + + // Find matching chain in supportedChains + const matchingChain = siwxExtension.supportedChains.find( + chain => chain.chainId === paymentNetwork, + ); + + if (!matchingChain) { + return response; // Payment network not in SIWX supportedChains + } + + // Build complete info with selected chain + const completeInfo = { + ...siwxExtension.info, + chainId: matchingChain.chainId, + type: matchingChain.type, + }; + + // Create and send SIWX proof + const payload = await createSIWxPayload(completeInfo, signer); + const siwxHeader = encodeSIWxHeader(payload); + + clonedRequest.headers.set(SIGN_IN_WITH_X, siwxHeader); + + return fetch(clonedRequest); + }; +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/hooks.ts b/typescript/packages/extensions/src/sign-in-with-x/hooks.ts new file mode 100644 index 0000000..8713e8b --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/hooks.ts @@ -0,0 +1,211 @@ +/** + * SIWX Lifecycle Hooks + * + * Pre-built hooks for integrating SIWX authentication with x402 servers and clients. + */ + +import type { SIWxStorage } from "./storage"; +import type { SIWxExtension, SIWxVerifyOptions, SignatureType } from "./types"; +import type { SIWxSigner } from "./sign"; +import { SIGN_IN_WITH_X } from "./types"; +import { parseSIWxHeader } from "./parse"; +import { validateSIWxMessage } from "./validate"; +import { verifySIWxSignature } from "./verify"; +import { createSIWxPayload } from "./client"; +import { encodeSIWxHeader } from "./encode"; +import { isSolanaSigner } from "./solana"; + +/** + * Options for creating server-side SIWX hooks. + */ +export interface CreateSIWxHookOptions { + /** Storage for tracking paid addresses */ + storage: SIWxStorage; + /** Options for signature verification (e.g., EVM smart wallet support) */ + verifyOptions?: SIWxVerifyOptions; + /** Optional callback for logging/debugging */ + onEvent?: (event: SIWxHookEvent) => void; +} + +/** + * Events emitted by SIWX hooks for logging/debugging. + */ +export type SIWxHookEvent = + | { type: "payment_recorded"; resource: string; address: string } + | { type: "access_granted"; resource: string; address: string } + | { type: "validation_failed"; resource: string; error?: string } + | { type: "nonce_reused"; resource: string; nonce: string } + | { type: "siwx_header_sent"; resource: string }; + +/** + * Creates an onAfterSettle hook that records payments for SIWX. + * + * @param options - Hook configuration + * @returns Hook function for x402ResourceServer.onAfterSettle() + * + * @example + * ```typescript + * const storage = new InMemorySIWxStorage(); + * const resourceServer = new x402ResourceServer(facilitator) + * .onAfterSettle(createSIWxSettleHook({ storage })); + * ``` + */ +export function createSIWxSettleHook(options: CreateSIWxHookOptions) { + const { storage, onEvent } = options; + + return async (ctx: { + paymentPayload: { payload: unknown; resource: { url: string } }; + result: { success: boolean; payer?: string }; + }): Promise => { + // Only record payment if settlement succeeded + if (!ctx.result.success) return; + + // Get payer from facilitator's settle result (works for all payment schemes) + const address = ctx.result.payer; + if (!address) return; + + const resource = new URL(ctx.paymentPayload.resource.url).pathname; + await storage.recordPayment(resource, address); + onEvent?.({ type: "payment_recorded", resource, address }); + }; +} + +/** + * Creates an onProtectedRequest hook that validates SIWX auth before payment. + * + * @param options - Hook configuration + * @returns Hook function for x402HTTPResourceServer.onProtectedRequest() + * + * @example + * ```typescript + * const storage = new InMemorySIWxStorage(); + * const httpServer = new x402HTTPResourceServer(resourceServer, routes) + * .onProtectedRequest(createSIWxRequestHook({ storage })); + * ``` + */ +export function createSIWxRequestHook(options: CreateSIWxHookOptions) { + const { storage, verifyOptions, onEvent } = options; + + // Validate nonce tracking is fully implemented or not at all + const hasUsedNonce = typeof storage.hasUsedNonce === "function"; + const hasRecordNonce = typeof storage.recordNonce === "function"; + if (hasUsedNonce !== hasRecordNonce) { + throw new Error( + "SIWxStorage nonce tracking requires both hasUsedNonce and recordNonce to be implemented", + ); + } + + return async (context: { + adapter: { getHeader(name: string): string | undefined; getUrl(): string }; + path: string; + }): Promise => { + // Try both cases for header (HTTP headers are case-insensitive) + const header = + context.adapter.getHeader(SIGN_IN_WITH_X) || + context.adapter.getHeader(SIGN_IN_WITH_X.toLowerCase()); + if (!header) return; + + try { + const payload = parseSIWxHeader(header); + const resourceUri = context.adapter.getUrl(); + + const validation = await validateSIWxMessage(payload, resourceUri); + if (!validation.valid) { + onEvent?.({ type: "validation_failed", resource: context.path, error: validation.error }); + return; + } + + const verification = await verifySIWxSignature(payload, verifyOptions); + if (!verification.valid || !verification.address) { + onEvent?.({ type: "validation_failed", resource: context.path, error: verification.error }); + return; + } + + // Check if nonce was already used (prevents signature replay attacks) + if (storage.hasUsedNonce) { + const nonceUsed = await storage.hasUsedNonce(payload.nonce); + if (nonceUsed) { + onEvent?.({ type: "nonce_reused", resource: context.path, nonce: payload.nonce }); + return; + } + } + + const hasPaid = await storage.hasPaid(context.path, verification.address); + if (hasPaid) { + // Record nonce as used before granting access + if (storage.recordNonce) { + await storage.recordNonce(payload.nonce); + } + + onEvent?.({ + type: "access_granted", + resource: context.path, + address: verification.address, + }); + return { grantAccess: true }; + } + } catch (err) { + onEvent?.({ + type: "validation_failed", + resource: context.path, + error: err instanceof Error ? err.message : "Unknown error", + }); + } + }; +} + +/** + * Creates an onPaymentRequired hook for client-side SIWX authentication. + * + * Matches the signer type to a compatible chain in supportedChains. + * For EVM signers: matches any eip191 chain + * For Solana signers: matches any ed25519 chain + * + * @param signer - Wallet signer for creating SIWX proofs + * @returns Hook function for x402HTTPClient.onPaymentRequired() + * + * @example + * ```typescript + * const httpClient = new x402HTTPClient(client) + * .onPaymentRequired(createSIWxClientHook(signer)); + * ``` + */ +export function createSIWxClientHook(signer: SIWxSigner) { + // Determine signer type once at hook creation + const signerIsSolana = isSolanaSigner(signer); + const expectedSignatureType: SignatureType = signerIsSolana ? "ed25519" : "eip191"; + + return async (context: { + paymentRequired: { accepts?: Array<{ network: string }>; extensions?: Record }; + }): Promise<{ headers: Record } | void> => { + const extensions = context.paymentRequired.extensions ?? {}; + const siwxExtension = extensions[SIGN_IN_WITH_X] as SIWxExtension | undefined; + + if (!siwxExtension?.supportedChains) return; + + try { + // Find a chain that matches the signer's signature type + const matchingChain = siwxExtension.supportedChains.find( + chain => chain.type === expectedSignatureType, + ); + + if (!matchingChain) { + // No chain compatible with this signer type + return; + } + + // Build complete info with selected chain + const completeInfo = { + ...siwxExtension.info, + chainId: matchingChain.chainId, + type: matchingChain.type, + }; + + const payload = await createSIWxPayload(completeInfo, signer); + const header = encodeSIWxHeader(payload); + return { headers: { [SIGN_IN_WITH_X]: header } }; + } catch { + // Failed to create SIWX payload, continue to payment + } + }; +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/index.ts b/typescript/packages/extensions/src/sign-in-with-x/index.ts new file mode 100644 index 0000000..75dde2f --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/index.ts @@ -0,0 +1,139 @@ +/** + * Sign-In-With-X Extension for x402 v2 + * + * CAIP-122 compliant wallet authentication for payment-protected resources. + * Allows clients to prove control of a wallet that may have previously paid + * for a resource, enabling servers to grant access without requiring repurchase. + * + * ## Server Usage + * + * ```typescript + * import { + * declareSIWxExtension, + * parseSIWxHeader, + * validateSIWxMessage, + * verifySIWxSignature, + * SIGN_IN_WITH_X, + * } from '@x402/extensions/sign-in-with-x'; + * + * // 1. Declare auth requirement in PaymentRequired response + * const extensions = declareSIWxExtension({ + * domain: 'api.example.com', + * resourceUri: 'https://api.example.com/data', + * network: 'eip155:8453', + * statement: 'Sign in to access your purchased content', + * }); + * + * // 2. Verify incoming proof + * const header = request.headers.get('SIGN-IN-WITH-X'); + * if (header) { + * const payload = parseSIWxHeader(header); + * + * const validation = await validateSIWxMessage( + * payload, + * 'https://api.example.com/data' + * ); + * + * if (validation.valid) { + * const verification = await verifySIWxSignature(payload); + * if (verification.valid) { + * // Authentication successful! + * // verification.address is the verified wallet + * } + * } + * } + * ``` + * + * ## Client Usage + * + * ```typescript + * import { + * createSIWxPayload, + * encodeSIWxHeader, + * } from '@x402/extensions/sign-in-with-x'; + * + * // 1. Get extension info from 402 response + * const serverInfo = paymentRequired.extensions['sign-in-with-x'].info; + * + * // 2. Create signed payload + * const payload = await createSIWxPayload(serverInfo, wallet); + * + * // 3. Encode for header + * const header = encodeSIWxHeader(payload); + * + * // 4. Send authenticated request + * fetch(url, { headers: { 'SIGN-IN-WITH-X': header } }); + * ``` + * + * @module sign-in-with-x + */ + +// Constants +export { SIGN_IN_WITH_X, SIWxPayloadSchema } from "./types"; +export { SOLANA_MAINNET, SOLANA_DEVNET, SOLANA_TESTNET } from "./solana"; + +// Types +export type { + SIWxExtension, + SIWxExtensionInfo, + SIWxExtensionSchema, + SIWxPayload, + DeclareSIWxOptions, + SignatureScheme, + SignatureType, + SIWxValidationResult, + SIWxValidationOptions, + SIWxVerifyResult, + EVMMessageVerifier, + SIWxVerifyOptions, + SupportedChain, +} from "./types"; +export type { CompleteSIWxInfo } from "./client"; + +// Server +export { declareSIWxExtension } from "./declare"; +export { siwxResourceServerExtension } from "./server"; +export { parseSIWxHeader } from "./parse"; +export { validateSIWxMessage } from "./validate"; +export { verifySIWxSignature } from "./verify"; +export { buildSIWxSchema } from "./schema"; + +// Client +export { createSIWxMessage } from "./message"; +export { createSIWxPayload } from "./client"; +export { encodeSIWxHeader } from "./encode"; +export { wrapFetchWithSIWx } from "./fetch"; +export { + getEVMAddress, + getSolanaAddress, + signEVMMessage, + signSolanaMessage, + type SIWxSigner, + type EVMSigner, + type SolanaSigner, +} from "./sign"; + +// Chain utilities - EVM +export { formatSIWEMessage, verifyEVMSignature, extractEVMChainId, isEVMSigner } from "./evm"; + +// Chain utilities - Solana +export { + formatSIWSMessage, + verifySolanaSignature, + decodeBase58, + encodeBase58, + extractSolanaChainReference, + isSolanaSigner, +} from "./solana"; + +// Storage +export { type SIWxStorage, InMemorySIWxStorage } from "./storage"; + +// Hooks +export { + createSIWxSettleHook, + createSIWxRequestHook, + createSIWxClientHook, + type CreateSIWxHookOptions, + type SIWxHookEvent, +} from "./hooks"; diff --git a/typescript/packages/extensions/src/sign-in-with-x/message.ts b/typescript/packages/extensions/src/sign-in-with-x/message.ts new file mode 100644 index 0000000..1368d7b --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/message.ts @@ -0,0 +1,46 @@ +/** + * CAIP-122 message construction for SIWX extension + * + * Constructs the canonical message string for signing. + * Routes to chain-specific formatters based on chainId namespace. + */ + +import { formatSIWEMessage } from "./evm"; +import { formatSIWSMessage } from "./solana"; +import type { CompleteSIWxInfo } from "./client"; + +/** + * Construct CAIP-122 compliant message string for signing. + * + * Routes to the appropriate chain-specific message formatter based on the + * chainId namespace prefix: + * - `eip155:*` → SIWE (EIP-4361) format via siwe library + * - `solana:*` → SIWS format + * + * @param serverInfo - Server extension info with chain selected (includes chainId) + * @param address - Client wallet address + * @returns Message string ready for signing + * @throws Error if chainId namespace is not supported + * + * @example + * ```typescript + * // EVM (Ethereum, Base, etc.) + * const completeInfo = { ...extension.info, chainId: "eip155:8453", type: "eip191" }; + * const evmMessage = createSIWxMessage(completeInfo, "0x1234..."); + * ``` + */ +export function createSIWxMessage(serverInfo: CompleteSIWxInfo, address: string): string { + // Route by chain namespace + if (serverInfo.chainId.startsWith("eip155:")) { + return formatSIWEMessage(serverInfo, address); + } + + if (serverInfo.chainId.startsWith("solana:")) { + return formatSIWSMessage(serverInfo, address); + } + + throw new Error( + `Unsupported chain namespace: ${serverInfo.chainId}. ` + + `Supported: eip155:* (EVM), solana:* (Solana)`, + ); +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/parse.ts b/typescript/packages/extensions/src/sign-in-with-x/parse.ts new file mode 100644 index 0000000..dcdf86d --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/parse.ts @@ -0,0 +1,54 @@ +/** + * Header parsing for SIWX extension + * + * Parses the SIGN-IN-WITH-X header from client requests. + * Requires base64-encoded JSON per x402 v2 spec. + */ + +import { Base64EncodedRegex, safeBase64Decode } from "@x402/core/utils"; +import { SIWxPayloadSchema, type SIWxPayload } from "./types"; + +/** + * Parse SIGN-IN-WITH-X header into structured payload. + * + * Expects base64-encoded JSON per x402 v2 spec (CHANGELOG-v2.md line 335). + * + * @param header - The SIGN-IN-WITH-X header value (base64-encoded JSON) + * @returns Parsed SIWX payload + * @throws Error if header is invalid or missing required fields + * + * @example + * ```typescript + * const header = request.headers.get('SIGN-IN-WITH-X'); + * if (header) { + * const payload = parseSIWxHeader(header); + * // payload.address, payload.signature, etc. + * } + * ``` + */ +export function parseSIWxHeader(header: string): SIWxPayload { + if (!Base64EncodedRegex.test(header)) { + throw new Error("Invalid SIWX header: not valid base64"); + } + + const jsonStr = safeBase64Decode(header); + + let rawPayload: unknown; + try { + rawPayload = JSON.parse(jsonStr); + } catch (error) { + if (error instanceof SyntaxError) { + throw new Error("Invalid SIWX header: not valid JSON"); + } + throw error; + } + + const parsed = SIWxPayloadSchema.safeParse(rawPayload); + + if (!parsed.success) { + const issues = parsed.error.issues.map(i => `${i.path.join(".")}: ${i.message}`).join(", "); + throw new Error(`Invalid SIWX header: ${issues}`); + } + + return parsed.data; +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/schema.ts b/typescript/packages/extensions/src/sign-in-with-x/schema.ts new file mode 100644 index 0000000..02ea569 --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/schema.ts @@ -0,0 +1,47 @@ +/** + * JSON Schema builder for SIWX extension + * + * Per CHANGELOG-v2.md lines 276-292 + */ + +import type { SIWxExtensionSchema } from "./types"; + +/** + * Build JSON Schema for SIWX extension validation. + * This schema validates the client proof payload structure. + * + * @returns JSON Schema for validating SIWX client payloads + */ +export function buildSIWxSchema(): SIWxExtensionSchema { + return { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + domain: { type: "string" }, + address: { type: "string" }, + statement: { type: "string" }, + uri: { type: "string", format: "uri" }, + version: { type: "string" }, + chainId: { type: "string" }, + type: { type: "string" }, + nonce: { type: "string" }, + issuedAt: { type: "string", format: "date-time" }, + expirationTime: { type: "string", format: "date-time" }, + notBefore: { type: "string", format: "date-time" }, + requestId: { type: "string" }, + resources: { type: "array", items: { type: "string", format: "uri" } }, + signature: { type: "string" }, + }, + required: [ + "domain", + "address", + "uri", + "version", + "chainId", + "type", + "nonce", + "issuedAt", + "signature", + ], + }; +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/server.ts b/typescript/packages/extensions/src/sign-in-with-x/server.ts new file mode 100644 index 0000000..429f12d --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/server.ts @@ -0,0 +1,104 @@ +/** + * Server-side ResourceServerExtension for SIWX + * + * Provides enrichPaymentRequiredResponse hook to: + * - Derive missing fields from request context (network, resourceUri, domain) + * - Refresh time-based fields per request (nonce, issuedAt, expirationTime) + */ + +import { randomBytes } from "crypto"; +import type { ResourceServerExtension, PaymentRequiredContext } from "@x402/core/types"; +import type { SIWxExtension, SIWxExtensionInfo, SupportedChain, DeclareSIWxOptions } from "./types"; +import { SIGN_IN_WITH_X } from "./types"; +import { getSignatureType, type SIWxDeclaration } from "./declare"; +import { buildSIWxSchema } from "./schema"; + +/** + * SIWX Resource Server Extension. + * + * Implements enrichPaymentRequiredResponse hook to: + * 1. Derive missing fields from context (network from requirements, URL from resourceInfo) + * 2. Refresh time-based fields (nonce, issuedAt, expirationTime) per request + * + * @example + * ```typescript + * import { siwxResourceServerExtension } from "@x402/extensions/sign-in-with-x"; + * + * const resourceServer = new x402ResourceServer(facilitator) + * .registerExtension(siwxResourceServerExtension) + * .onAfterSettle(createSIWxSettleHook({ storage })); + * ``` + */ +export const siwxResourceServerExtension: ResourceServerExtension = { + key: SIGN_IN_WITH_X, + + enrichPaymentRequiredResponse: async ( + declaration: unknown, + context: PaymentRequiredContext, + ): Promise => { + const decl = declaration as SIWxDeclaration; + const opts: DeclareSIWxOptions = decl._options ?? {}; + + // Derive resourceUri from context if not provided + const resourceUri = opts.resourceUri ?? context.resourceInfo.url; + + // Derive domain from resourceUri + let domain = opts.domain; + if (!domain && resourceUri) { + try { + domain = new URL(resourceUri).hostname; + } catch { + // If URL parsing fails, leave domain undefined (will cause validation error) + } + } + + // Derive networks from payment requirements if not provided + let networks: string[]; + if (opts.network) { + networks = Array.isArray(opts.network) ? opts.network : [opts.network]; + } else { + // Get unique networks from payment requirements + networks = [...new Set(context.requirements.map(r => r.network))]; + } + + // Generate fresh time-based fields + const nonce = randomBytes(16).toString("hex"); + const issuedAt = new Date().toISOString(); + + // Calculate expirationTime based on configured duration + const expirationSeconds = opts.expirationSeconds; + const expirationTime = + expirationSeconds !== undefined + ? new Date(Date.now() + expirationSeconds * 1000).toISOString() + : undefined; + + // Build complete info + const info: SIWxExtensionInfo = { + domain: domain ?? "", + uri: resourceUri, + version: opts.version ?? "1", + nonce, + issuedAt, + resources: [resourceUri], + }; + + if (expirationTime) { + info.expirationTime = expirationTime; + } + if (opts.statement) { + info.statement = opts.statement; + } + + // Build supportedChains from networks + const supportedChains: SupportedChain[] = networks.map(network => ({ + chainId: network, + type: getSignatureType(network), + })); + + return { + info, + supportedChains, + schema: buildSIWxSchema(), + }; + }, +}; diff --git a/typescript/packages/extensions/src/sign-in-with-x/sign.ts b/typescript/packages/extensions/src/sign-in-with-x/sign.ts new file mode 100644 index 0000000..82af112 --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/sign.ts @@ -0,0 +1,138 @@ +/** + * Message signing for SIWX extension + * + * Client-side helpers for signing SIWX messages. + * Supports both EVM (viem) and Solana wallet adapters. + */ + +import { encodeBase58 } from "./solana"; + +/** + * Signer interface for EVM SIWX message signing. + * Compatible with viem WalletClient and PrivateKeyAccount. + */ +export interface EVMSigner { + /** Sign a message and return hex-encoded signature */ + signMessage: (args: { message: string; account?: unknown }) => Promise; + /** Account object (for WalletClient) */ + account?: { address: string }; + /** Direct address (for PrivateKeyAccount) */ + address?: string; +} + +/** + * Wallet adapter style Solana signer. + * Compatible with @solana/wallet-adapter, Phantom/Solflare wallet APIs. + */ +export interface WalletAdapterSigner { + /** Sign a message and return raw signature bytes */ + signMessage: (message: Uint8Array) => Promise; + /** Solana public key (Base58 encoded string or PublicKey-like object) */ + publicKey: string | { toBase58: () => string }; +} + +/** + * Solana Kit KeyPairSigner style. + * Compatible with createKeyPairSignerFromBytes and generateKeyPairSigner from @solana/kit. + */ +export type SolanaKitSigner = { + /** Solana address (Base58 encoded string) */ + address: string; + /** Sign messages - accepts messages with content and signatures */ + signMessages: ( + messages: Array<{ content: Uint8Array; signatures: Record }>, + ) => Promise>>; +}; + +/** + * Union type for Solana signers - supports both wallet adapter and @solana/kit. + */ +export type SolanaSigner = WalletAdapterSigner | SolanaKitSigner; + +/** + * Union type for SIWX signers - supports both EVM and Solana wallets. + */ +export type SIWxSigner = EVMSigner | SolanaSigner; + +/** + * Get address from an EVM signer. + * + * @param signer - EVM wallet signer instance + * @returns The wallet address as a hex string + */ +export function getEVMAddress(signer: EVMSigner): string { + if (signer.account?.address) { + return signer.account.address; + } + if (signer.address) { + return signer.address; + } + throw new Error("EVM signer missing address"); +} + +/** + * Get address from a Solana signer. + * Supports both wallet adapter (publicKey) and @solana/kit (address) interfaces. + * + * @param signer - Solana wallet signer instance + * @returns The wallet address as a Base58 string + */ +export function getSolanaAddress(signer: SolanaSigner): string { + // Check for @solana/kit KeyPairSigner interface (address property) + if ("address" in signer && signer.address) { + return signer.address; + } + // Fall back to wallet adapter interface (publicKey property) + if ("publicKey" in signer) { + const pk = signer.publicKey; + return typeof pk === "string" ? pk : pk.toBase58(); + } + throw new Error("Solana signer missing address or publicKey"); +} + +/** + * Sign a message with an EVM wallet. + * Returns hex-encoded signature. + * + * @param message - The message to sign + * @param signer - EVM wallet signer instance + * @returns Hex-encoded signature + */ +export async function signEVMMessage(message: string, signer: EVMSigner): Promise { + if (signer.account) { + return signer.signMessage({ message, account: signer.account }); + } + return signer.signMessage({ message }); +} + +/** + * Sign a message with a Solana wallet. + * Returns Base58-encoded signature. + * Supports both wallet adapter (signMessage) and @solana/kit (signMessages) interfaces. + * + * @param message - The message to sign + * @param signer - Solana wallet signer instance + * @returns Base58-encoded signature + */ +export async function signSolanaMessage(message: string, signer: SolanaSigner): Promise { + const messageBytes = new TextEncoder().encode(message); + + // Check for @solana/kit signMessages interface + if ("signMessages" in signer) { + const results = await signer.signMessages([{ content: messageBytes, signatures: {} }]); + // signMessages returns an array of signature dictionaries + // The signature is keyed by the signer's address + const sigDict = results[0] as { [key: string]: Uint8Array }; + // Get the first (and only) signature value from the dictionary + const signatureBytes = Object.values(sigDict)[0]; + return encodeBase58(signatureBytes); + } + + // Fall back to wallet adapter signMessage interface + if ("signMessage" in signer) { + const signatureBytes = await signer.signMessage(messageBytes); + return encodeBase58(signatureBytes); + } + + throw new Error("Solana signer missing signMessage or signMessages method"); +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/solana.ts b/typescript/packages/extensions/src/sign-in-with-x/solana.ts new file mode 100644 index 0000000..3ca37cd --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/solana.ts @@ -0,0 +1,186 @@ +/** + * Solana Sign-In-With-X (SIWS) support + * + * Implements CAIP-122 compliant message format and Ed25519 signature verification + * for Solana wallets. + */ + +import { base58 } from "@scure/base"; +import nacl from "tweetnacl"; +import type { CompleteSIWxInfo } from "./client"; +import type { SIWxSigner } from "./sign"; + +/** + * Common Solana network CAIP-2 identifiers. + * Uses genesis hash as the chain reference per CAIP-30. + */ +export const SOLANA_MAINNET = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"; +export const SOLANA_DEVNET = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"; +export const SOLANA_TESTNET = "solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z"; + +/** + * Extract chain reference from CAIP-2 Solana chainId. + * + * @param chainId - CAIP-2 format chain ID (e.g., "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp") + * @returns Chain reference (genesis hash) + * + * @example + * ```typescript + * extractSolanaChainReference("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp") // "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + * ``` + */ +export function extractSolanaChainReference(chainId: string): string { + const [, reference] = chainId.split(":"); + return reference; +} + +/** + * Format SIWS message following CAIP-122 ABNF specification. + * + * The message format is identical to SIWE (EIP-4361) but uses "Solana account" + * instead of "Ethereum account" in the header line. + * + * @param info - Server-provided extension info + * @param address - Client's Solana wallet address (Base58 encoded public key) + * @returns Message string ready for signing + * + * @example + * ```typescript + * const message = formatSIWSMessage(serverInfo, "BSmWDgE9ex6dZYbiTsJGcwMEgFp8q4aWh92hdErQPeVW"); + * // Returns: + * // "api.example.com wants you to sign in with your Solana account: + * // BSmWDgE9ex6dZYbiTsJGcwMEgFp8q4aWh92hdErQPeVW + * // + * // Sign in to access your content + * // + * // URI: https://api.example.com/data + * // Version: 1 + * // Chain ID: mainnet + * // Nonce: abc123 + * // Issued At: 2024-01-01T00:00:00.000Z" + * ``` + */ +export function formatSIWSMessage(info: CompleteSIWxInfo, address: string): string { + const lines: string[] = [ + `${info.domain} wants you to sign in with your Solana account:`, + address, + "", + ]; + + // Statement (optional, with blank line after) + if (info.statement) { + lines.push(info.statement, ""); + } + + // Required fields + lines.push( + `URI: ${info.uri}`, + `Version: ${info.version}`, + `Chain ID: ${extractSolanaChainReference(info.chainId)}`, + `Nonce: ${info.nonce}`, + `Issued At: ${info.issuedAt}`, + ); + + // Optional fields + if (info.expirationTime) { + lines.push(`Expiration Time: ${info.expirationTime}`); + } + if (info.notBefore) { + lines.push(`Not Before: ${info.notBefore}`); + } + if (info.requestId) { + lines.push(`Request ID: ${info.requestId}`); + } + + // Resources (optional) + if (info.resources && info.resources.length > 0) { + lines.push("Resources:"); + for (const resource of info.resources) { + lines.push(`- ${resource}`); + } + } + + return lines.join("\n"); +} + +/** + * Verify Ed25519 signature for SIWS. + * + * @param message - The SIWS message that was signed + * @param signature - Ed25519 signature bytes + * @param publicKey - Solana public key bytes (32 bytes) + * @returns true if signature is valid + * + * @example + * ```typescript + * const messageBytes = new TextEncoder().encode(message); + * const valid = verifySolanaSignature(message, signatureBytes, publicKeyBytes); + * ``` + */ +export function verifySolanaSignature( + message: string, + signature: Uint8Array, + publicKey: Uint8Array, +): boolean { + const messageBytes = new TextEncoder().encode(message); + return nacl.sign.detached.verify(messageBytes, signature, publicKey); +} + +/** + * Decode Base58 string to bytes. + * + * Solana uses Base58 encoding (Bitcoin alphabet) for addresses and signatures. + * + * @param encoded - Base58 encoded string + * @returns Decoded bytes + * @throws Error if string contains invalid Base58 characters + * + * @example + * ```typescript + * const publicKeyBytes = decodeBase58("BSmWDgE9ex6dZYbiTsJGcwMEgFp8q4aWh92hdErQPeVW"); + * // Returns Uint8Array of 32 bytes + * ``` + */ +export function decodeBase58(encoded: string): Uint8Array { + return base58.decode(encoded); +} + +/** + * Encode bytes to Base58 string. + * + * @param bytes - Bytes to encode + * @returns Base58 encoded string + */ +export function encodeBase58(bytes: Uint8Array): string { + return base58.encode(bytes); +} + +/** + * Detect if a signer is Solana-compatible. + * Checks for Solana-specific properties that don't exist on EVM signers. + * + * @param signer - The signer to check + * @returns true if the signer is a Solana signer + */ +export function isSolanaSigner(signer: SIWxSigner): boolean { + // SolanaKitSigner has signMessages method (plural) - only Solana has this + if ("signMessages" in signer && typeof signer.signMessages === "function") { + return true; + } + // WalletAdapterSigner has publicKey property + // But viem also has publicKey (as hex string), so we need to distinguish: + // - Solana wallet adapter: publicKey has toBase58() method OR is a non-hex string + // - viem: publicKey is a hex string starting with 0x + if ("publicKey" in signer && signer.publicKey) { + const pk = signer.publicKey; + // Check for toBase58 method (Solana PublicKey object) + if (typeof pk === "object" && pk !== null && "toBase58" in pk) { + return true; + } + // Check for non-hex string (Solana base58 address) + if (typeof pk === "string" && !pk.startsWith("0x")) { + return true; + } + } + return false; +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/storage.ts b/typescript/packages/extensions/src/sign-in-with-x/storage.ts new file mode 100644 index 0000000..187d392 --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/storage.ts @@ -0,0 +1,81 @@ +/** + * Storage interface for SIWX payment tracking. + * + * Implementations track which addresses have paid for which resources, + * enabling SIWX authentication to grant access without re-payment. + * + * Optionally supports nonce tracking to prevent signature replay attacks. + */ +export interface SIWxStorage { + /** + * Check if an address has paid for a resource. + * + * @param resource - The resource path (e.g., "/weather") + * @param address - The wallet address to check + * @returns True if the address has paid for the resource + */ + hasPaid(resource: string, address: string): boolean | Promise; + + /** + * Record that an address has paid for a resource. + * + * @param resource - The resource path + * @param address - The wallet address that paid + */ + recordPayment(resource: string, address: string): void | Promise; + + /** + * Check if a nonce has already been used (optional). + * + * Implementing this method prevents signature replay attacks where + * an intercepted SIWX header could be reused by an attacker. + * + * @param nonce - The nonce from the SIWX payload + * @returns True if the nonce has been used + */ + hasUsedNonce?(nonce: string): boolean | Promise; + + /** + * Record that a nonce has been used (optional). + * + * Called after successfully granting access via SIWX. + * Implementations should consider adding expiration to avoid unbounded growth. + * + * @param nonce - The nonce to record as used + */ + recordNonce?(nonce: string): void | Promise; +} + +/** + * In-memory implementation of SIWxStorage. + * + * Suitable for development and single-instance deployments. + * For production multi-instance deployments, use a persistent storage implementation. + */ +export class InMemorySIWxStorage implements SIWxStorage { + private paidAddresses = new Map>(); + + /** + * Check if an address has paid for a resource. + * + * @param resource - The resource path + * @param address - The wallet address to check + * @returns True if the address has paid + */ + hasPaid(resource: string, address: string): boolean { + return this.paidAddresses.get(resource)?.has(address.toLowerCase()) ?? false; + } + + /** + * Record that an address has paid for a resource. + * + * @param resource - The resource path + * @param address - The wallet address that paid + */ + recordPayment(resource: string, address: string): void { + if (!this.paidAddresses.has(resource)) { + this.paidAddresses.set(resource, new Set()); + } + this.paidAddresses.get(resource)!.add(address.toLowerCase()); + } +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/types.ts b/typescript/packages/extensions/src/sign-in-with-x/types.ts new file mode 100644 index 0000000..405fa17 --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/types.ts @@ -0,0 +1,241 @@ +/** + * Type definitions for the Sign-In-With-X (SIWX) extension + * + * Implements CAIP-122 standard for chain-agnostic wallet-based identity assertions. + * Per x402 v2 spec: typescript/site/CHANGELOG-v2.md lines 237-341 + */ + +import { z } from "zod"; + +/** + * Extension identifier constant + */ +export const SIGN_IN_WITH_X = "sign-in-with-x"; + +/** + * Supported signature schemes per CHANGELOG-v2.md line 271. + * + * NOTE: This is primarily informational. Actual signature verification + * is determined by the chainId prefix, not this field: + * - `eip155:*` chains use EVM verification (handles eip191, eip712, eip1271, eip6492 automatically) + * - `solana:*` chains use Ed25519 verification (siws) + * + * The signatureScheme field serves as a hint for clients to select + * the appropriate signing UX. + */ +export type SignatureScheme = + | "eip191" // personal_sign (default for EVM EOAs) + | "eip1271" // smart contract wallet verification + | "eip6492" // counterfactual smart wallet verification + | "siws"; // Sign-In-With-Solana + +/** Signature algorithm type per CAIP-122 */ +export type SignatureType = "eip191" | "ed25519"; + +/** + * Supported chain configuration in supportedChains array. + * Specifies which chains the server accepts for authentication. + */ +export interface SupportedChain { + /** CAIP-2 chain identifier (e.g., "eip155:8453", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp") */ + chainId: string; + /** Signature algorithm type per CAIP-122 */ + type: SignatureType; + /** Optional signature scheme hint (informational) */ + signatureScheme?: SignatureScheme; +} + +/** + * Server-declared extension info included in PaymentRequired.extensions. + * Contains message metadata shared across all supported chains. + * Per CHANGELOG-v2.md lines 263-272 + */ +export interface SIWxExtensionInfo { + /** Server's domain */ + domain: string; + /** Full resource URI */ + uri: string; + /** Human-readable purpose for signing */ + statement?: string; + /** CAIP-122 version, always "1" */ + version: string; + /** Cryptographic nonce (SDK auto-generates) */ + nonce: string; + /** ISO 8601 timestamp (SDK auto-generates) */ + issuedAt: string; + /** Optional expiry (default: +5 min) */ + expirationTime?: string; + /** Optional validity start */ + notBefore?: string; + /** Optional correlation ID */ + requestId?: string; + /** Associated resources */ + resources?: string[]; +} + +/** + * JSON Schema for SIWX extension validation + * Per CHANGELOG-v2.md lines 276-292 + */ +export interface SIWxExtensionSchema { + $schema: string; + type: "object"; + properties: { + domain: { type: "string" }; + address: { type: "string" }; + statement?: { type: "string" }; + uri: { type: "string"; format: "uri" }; + version: { type: "string" }; + chainId: { type: "string" }; + type: { type: "string" }; + nonce: { type: "string" }; + issuedAt: { type: "string"; format: "date-time" }; + expirationTime?: { type: "string"; format: "date-time" }; + notBefore?: { type: "string"; format: "date-time" }; + requestId?: { type: "string" }; + resources?: { type: "array"; items: { type: "string"; format: "uri" } }; + signature: { type: "string" }; + }; + required: string[]; +} + +/** + * Complete SIWX extension structure (info + supportedChains + schema). + * Follows standard x402 v2 extension pattern with multi-chain support. + */ +export interface SIWxExtension { + info: SIWxExtensionInfo; + supportedChains: SupportedChain[]; + schema: SIWxExtensionSchema; +} + +/** + * Zod schema for SIWX payload validation + * Client proof payload sent in SIGN-IN-WITH-X header + * Per CHANGELOG-v2.md lines 301-315 + */ +export const SIWxPayloadSchema = z.object({ + domain: z.string(), + address: z.string(), + statement: z.string().optional(), + uri: z.string(), + version: z.string(), + chainId: z.string(), + type: z.enum(["eip191", "ed25519"]), + nonce: z.string(), + issuedAt: z.string(), + expirationTime: z.string().optional(), + notBefore: z.string().optional(), + requestId: z.string().optional(), + resources: z.array(z.string()).optional(), + signatureScheme: z.enum(["eip191", "eip1271", "eip6492", "siws"]).optional(), + signature: z.string(), +}); + +/** + * Client proof payload type (inferred from zod schema) + */ +export type SIWxPayload = z.infer; + +/** + * Options for declaring SIWX extension on server. + * + * Most fields are optional and derived automatically from request context: + * - `domain`: Parsed from resourceUri or request URL + * - `resourceUri`: From request URL + * - `network`: From payment requirements (accepts[].network) + * + * Explicit values override automatic derivation. + */ +export interface DeclareSIWxOptions { + /** Server's domain. If omitted, derived from resourceUri or request URL. */ + domain?: string; + /** Full resource URI. If omitted, derived from request URL. */ + resourceUri?: string; + /** Human-readable purpose */ + statement?: string; + /** CAIP-122 version (default: "1") */ + version?: string; + /** + * Network(s) to support. If omitted, derived from payment requirements. + * - Single chain: "eip155:8453" or "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + * - Multi-chain: ["eip155:8453", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"] + */ + network?: string | string[]; + /** + * Optional expiration duration in seconds. + * - Number (e.g., 300): Signature expires after this many seconds + * - undefined: Infinite expiration (no expirationTime field in wire format) + */ + expirationSeconds?: number; +} + +/** + * Validation result from validateSIWxMessage + */ +export interface SIWxValidationResult { + valid: boolean; + error?: string; +} + +/** + * Options for message validation + */ +export interface SIWxValidationOptions { + /** Maximum age for issuedAt in milliseconds (default: 5 minutes) */ + maxAge?: number; + /** Custom nonce validation function */ + checkNonce?: (nonce: string) => boolean | Promise; +} + +/** + * Result from signature verification + */ +export interface SIWxVerifyResult { + valid: boolean; + /** Recovered/verified address (checksummed) */ + address?: string; + error?: string; +} + +/** + * EVM message verifier function type. + * Compatible with viem's publicClient.verifyMessage(). + * + * When provided to verifySIWxSignature, enables: + * - EIP-1271 (deployed smart contract wallets) + * - EIP-6492 (counterfactual/pre-deploy smart wallets) + * + * Without a verifier, only EOA signatures (EIP-191) can be verified. + * + * @example + * ```typescript + * import { createPublicClient, http } from 'viem'; + * import { base } from 'viem/chains'; + * + * const publicClient = createPublicClient({ chain: base, transport: http() }); + * // publicClient.verifyMessage satisfies EVMMessageVerifier + * ``` + */ +export type EVMMessageVerifier = (args: { + address: `0x${string}`; + message: string; + signature: `0x${string}`; +}) => Promise; + +/** + * Options for SIWX signature verification + */ +export interface SIWxVerifyOptions { + /** + * EVM message verifier for smart wallet support. + * + * Pass `publicClient.verifyMessage` from viem to enable verification of: + * - Smart contract wallets (EIP-1271) + * - Counterfactual/undeployed smart wallets (EIP-6492) + * + * If not provided, only EOA signatures are verified using standalone + * ECDSA recovery (no RPC calls required). + */ + evmVerifier?: EVMMessageVerifier; +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/validate.ts b/typescript/packages/extensions/src/sign-in-with-x/validate.ts new file mode 100644 index 0000000..171626c --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/validate.ts @@ -0,0 +1,139 @@ +/** + * Message validation for SIWX extension + * + * Validates SIWX payload fields before cryptographic verification. + * Per CHANGELOG-v2.md validation rules (lines 318-329). + */ + +import type { SIWxPayload, SIWxValidationResult, SIWxValidationOptions } from "./types"; + +/** Default maximum age for issuedAt: 5 minutes per spec */ +const DEFAULT_MAX_AGE_MS = 5 * 60 * 1000; + +/** + * Validate SIWX message fields. + * + * Performs validation per spec (CHANGELOG-v2.md lines 318-329): + * - Domain binding: domain MUST match server's domain + * - URI validation: uri must refer to base url of resource + * - Temporal validation: + * - issuedAt MUST be recent (< 5 minutes by default) + * - expirationTime MUST be in the future + * - notBefore (if present) MUST be in the past + * - Nonce: MUST be unique (via optional checkNonce callback) + * + * @param message - The SIWX payload to validate + * @param expectedResourceUri - Expected resource URI (for domain/URI matching) + * @param options - Validation options + * @returns Validation result + * + * @example + * ```typescript + * const payload = parseSIWxHeader(header); + * const result = await validateSIWxMessage( + * payload, + * 'https://api.example.com/data', + * { checkNonce: (n) => !usedNonces.has(n) } + * ); + * + * if (!result.valid) { + * return { error: result.error }; + * } + * ``` + */ +export async function validateSIWxMessage( + message: SIWxPayload, + expectedResourceUri: string, + options: SIWxValidationOptions = {}, +): Promise { + const expectedUrl = new URL(expectedResourceUri); + const maxAge = options.maxAge ?? DEFAULT_MAX_AGE_MS; + + // 1. Domain binding (spec: "domain field MUST match server's domain") + // Use hostname (without port) per EIP-4361 convention + if (message.domain !== expectedUrl.hostname) { + return { + valid: false, + error: `Domain mismatch: expected "${expectedUrl.hostname}", got "${message.domain}"`, + }; + } + + // 2. URI validation (spec: "uri and resources must refer to base url of resource") + // Allow the message URI to be the origin or the full resource URL + if (!message.uri.startsWith(expectedUrl.origin)) { + return { + valid: false, + error: `URI mismatch: expected origin "${expectedUrl.origin}", got "${message.uri}"`, + }; + } + + // 3. issuedAt validation (spec: "MUST be recent, recommended < 5 minutes") + const issuedAt = new Date(message.issuedAt); + if (isNaN(issuedAt.getTime())) { + return { + valid: false, + error: "Invalid issuedAt timestamp", + }; + } + + const age = Date.now() - issuedAt.getTime(); + if (age > maxAge) { + return { + valid: false, + error: `Message too old: ${Math.round(age / 1000)}s exceeds ${maxAge / 1000}s limit`, + }; + } + if (age < 0) { + return { + valid: false, + error: "issuedAt is in the future", + }; + } + + // 4. expirationTime validation (spec: "MUST be in the future") + if (message.expirationTime) { + const expiration = new Date(message.expirationTime); + if (isNaN(expiration.getTime())) { + return { + valid: false, + error: "Invalid expirationTime timestamp", + }; + } + if (expiration < new Date()) { + return { + valid: false, + error: "Message expired", + }; + } + } + + // 5. notBefore validation (spec: "if present, MUST be in the past") + if (message.notBefore) { + const notBefore = new Date(message.notBefore); + if (isNaN(notBefore.getTime())) { + return { + valid: false, + error: "Invalid notBefore timestamp", + }; + } + if (new Date() < notBefore) { + return { + valid: false, + error: "Message not yet valid (notBefore is in the future)", + }; + } + } + + // 6. Nonce validation (spec: "MUST be unique per session to prevent replay attacks") + if (options.checkNonce) { + const nonceValid = await options.checkNonce(message.nonce); + if (!nonceValid) { + return { + valid: false, + error: "Nonce validation failed (possible replay attack)", + }; + } + } + + return { valid: true }; +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/verify.ts b/typescript/packages/extensions/src/sign-in-with-x/verify.ts new file mode 100644 index 0000000..34ae338 --- /dev/null +++ b/typescript/packages/extensions/src/sign-in-with-x/verify.ts @@ -0,0 +1,196 @@ +/** + * Signature verification for SIWX extension + * + * Routes to chain-specific verification based on chainId namespace: + * - EVM (eip155:*): EOA by default, smart wallet (EIP-1271/EIP-6492) with verifier + * - Solana (solana:*): Ed25519 signature verification via tweetnacl + */ + +import { formatSIWEMessage, verifyEVMSignature } from "./evm"; +import { formatSIWSMessage, verifySolanaSignature, decodeBase58 } from "./solana"; +import type { SIWxPayload, SIWxVerifyResult, SIWxVerifyOptions, EVMMessageVerifier } from "./types"; + +/** + * Verify SIWX signature cryptographically. + * + * Routes to the appropriate chain-specific verification based on the + * chainId namespace prefix: + * - `eip155:*` → EVM verification (EOA by default, smart wallet with verifier) + * - `solana:*` → Ed25519 signature verification + * + * @param payload - The SIWX payload containing signature + * @param options - Optional verification options + * @returns Verification result with recovered address if valid + * + * @example + * ```typescript + * // EOA-only verification (default) + * const result = await verifySIWxSignature(payload); + * + * // Smart wallet verification + * import { createPublicClient, http } from 'viem'; + * import { base } from 'viem/chains'; + * + * const publicClient = createPublicClient({ chain: base, transport: http() }); + * const result = await verifySIWxSignature(payload, { + * evmVerifier: publicClient.verifyMessage, + * }); + * + * if (result.valid) { + * console.log('Verified wallet:', result.address); + * } else { + * console.error('Verification failed:', result.error); + * } + * ``` + */ +export async function verifySIWxSignature( + payload: SIWxPayload, + options?: SIWxVerifyOptions, +): Promise { + try { + // Route by chain namespace + if (payload.chainId.startsWith("eip155:")) { + return verifyEVMPayload(payload, options?.evmVerifier); + } + + if (payload.chainId.startsWith("solana:")) { + return verifySolanaPayload(payload); + } + + return { + valid: false, + error: `Unsupported chain namespace: ${payload.chainId}. Supported: eip155:* (EVM), solana:* (Solana)`, + }; + } catch (error) { + return { + valid: false, + error: error instanceof Error ? error.message : "Verification failed", + }; + } +} + +/** + * Verify EVM signature with optional smart wallet support. + * + * @param payload - The SIWX payload containing signature and message data + * @param verifier - Optional message verifier for EIP-1271/EIP-6492 support + * @returns Verification result with recovered address if valid + */ +async function verifyEVMPayload( + payload: SIWxPayload, + verifier?: EVMMessageVerifier, +): Promise { + // Reconstruct SIWE message for verification + const message = formatSIWEMessage( + { + domain: payload.domain, + uri: payload.uri, + statement: payload.statement, + version: payload.version, + chainId: payload.chainId, + type: payload.type, + nonce: payload.nonce, + issuedAt: payload.issuedAt, + expirationTime: payload.expirationTime, + notBefore: payload.notBefore, + requestId: payload.requestId, + resources: payload.resources, + }, + payload.address, + ); + + try { + const valid = await verifyEVMSignature(message, payload.address, payload.signature, verifier); + + if (!valid) { + return { + valid: false, + error: "Signature verification failed", + }; + } + + return { + valid: true, + address: payload.address, + }; + } catch (error) { + return { + valid: false, + error: error instanceof Error ? error.message : "Signature verification failed", + }; + } +} + +/** + * Verify Solana Ed25519 signature. + * + * Reconstructs the SIWS message and verifies using tweetnacl. + * + * @param payload - The SIWX payload containing signature and message data + * @returns Verification result with recovered address if valid + */ +function verifySolanaPayload(payload: SIWxPayload): SIWxVerifyResult { + // Reconstruct SIWS message + const message = formatSIWSMessage( + { + domain: payload.domain, + uri: payload.uri, + statement: payload.statement, + version: payload.version, + chainId: payload.chainId, + type: payload.type, + nonce: payload.nonce, + issuedAt: payload.issuedAt, + expirationTime: payload.expirationTime, + notBefore: payload.notBefore, + requestId: payload.requestId, + resources: payload.resources, + }, + payload.address, + ); + + // Decode Base58 signature and public key + let signature: Uint8Array; + let publicKey: Uint8Array; + + try { + signature = decodeBase58(payload.signature); + publicKey = decodeBase58(payload.address); + } catch (error) { + return { + valid: false, + error: `Invalid Base58 encoding: ${error instanceof Error ? error.message : "decode failed"}`, + }; + } + + // Validate signature length (Ed25519 signatures are 64 bytes) + if (signature.length !== 64) { + return { + valid: false, + error: `Invalid signature length: expected 64 bytes, got ${signature.length}`, + }; + } + + // Validate public key length (Ed25519 public keys are 32 bytes) + if (publicKey.length !== 32) { + return { + valid: false, + error: `Invalid public key length: expected 32 bytes, got ${publicKey.length}`, + }; + } + + // Verify Ed25519 signature + const valid = verifySolanaSignature(message, signature, publicKey); + + if (!valid) { + return { + valid: false, + error: "Solana signature verification failed", + }; + } + + return { + valid: true, + address: payload.address, + }; +} diff --git a/typescript/packages/extensions/src/types.ts b/typescript/packages/extensions/src/types.ts new file mode 100644 index 0000000..198b842 --- /dev/null +++ b/typescript/packages/extensions/src/types.ts @@ -0,0 +1,18 @@ +/** + * Shared type utilities for x402 extensions + */ + +/** + * Type utility to merge extensions properly when chaining. + * If T already has extensions, merge them; otherwise add new extensions. + * + * @example + * ```ts + * // Chaining multiple extensions preserves all types: + * const client = withBazaar(withOtherExtension(new HTTPFacilitatorClient())); + * // Type: HTTPFacilitatorClient & { extensions: OtherExtension & BazaarExtension } + * ``` + */ +export type WithExtensions = T extends { extensions: infer Existing } + ? Omit & { extensions: Existing & E } + : T & { extensions: E }; diff --git a/typescript/packages/extensions/test/bazaar.test.ts b/typescript/packages/extensions/test/bazaar.test.ts new file mode 100644 index 0000000..5dbd9dd --- /dev/null +++ b/typescript/packages/extensions/test/bazaar.test.ts @@ -0,0 +1,1291 @@ +/** + * Tests for Bazaar Discovery Extension + */ + +import { describe, it, expect } from "vitest"; +import { + BAZAAR, + declareDiscoveryExtension, + validateDiscoveryExtension, + extractDiscoveryInfo, + extractDiscoveryInfoFromExtension, + extractDiscoveryInfoV1, + validateAndExtract, + bazaarResourceServerExtension, +} from "../src/bazaar/index"; +import type { BodyDiscoveryInfo, DiscoveryExtension } from "../src/bazaar/types"; +import type { HTTPAdapter, HTTPRequestContext } from "@x402/core/http"; + +describe("Bazaar Discovery Extension", () => { + describe("BAZAAR constant", () => { + it("should export the correct extension identifier", () => { + expect(BAZAAR).toBe("bazaar"); + }); + }); + + describe("declareDiscoveryExtension - GET method", () => { + it("should create a valid GET extension with query params", () => { + const result = declareDiscoveryExtension({ + input: { query: "test", limit: 10 }, + inputSchema: { + properties: { + query: { type: "string" }, + limit: { type: "number" }, + }, + required: ["query"], + }, + }); + + expect(result).toHaveProperty("bazaar"); + const extension = result.bazaar; + expect(extension).toHaveProperty("info"); + expect(extension).toHaveProperty("schema"); + expect(extension.info.input.type).toBe("http"); + expect(extension.info.input.queryParams).toEqual({ query: "test", limit: 10 }); + }); + + it("should create a GET extension with output example", () => { + const outputExample = { results: [], total: 0 }; + const result = declareDiscoveryExtension({ + input: { query: "test" }, + inputSchema: { + properties: { + query: { type: "string" }, + }, + }, + output: { + example: outputExample, + }, + }); + + const extension = result.bazaar; + expect(extension.info.output?.example).toEqual(outputExample); + }); + }); + + describe("declareDiscoveryExtension - POST method", () => { + it("should create a valid POST extension with JSON body", () => { + const result = declareDiscoveryExtension({ + input: { name: "John", age: 30 }, + inputSchema: { + properties: { + name: { type: "string" }, + age: { type: "number" }, + }, + required: ["name"], + }, + bodyType: "json", + }); + + const extension = result.bazaar; + expect(extension.info.input.type).toBe("http"); + expect((extension.info as BodyDiscoveryInfo).input.bodyType).toBe("json"); + expect((extension.info as BodyDiscoveryInfo).input.body).toEqual({ name: "John", age: 30 }); + }); + + it("should default to JSON body type if not specified", () => { + const result = declareDiscoveryExtension({ + input: { data: "test" }, + inputSchema: { + properties: { + data: { type: "string" }, + }, + }, + bodyType: "json", + }); + + const extension = result.bazaar; + expect((extension.info as BodyDiscoveryInfo).input.bodyType).toBe("json"); + }); + + it("should support form-data body type", () => { + const result = declareDiscoveryExtension({ + input: { file: "upload.pdf" }, + inputSchema: { + properties: { + file: { type: "string" }, + }, + }, + bodyType: "form-data", + }); + + const extension = result.bazaar; + expect((extension.info as BodyDiscoveryInfo).input.bodyType).toBe("form-data"); + }); + }); + + describe("declareDiscoveryExtension - Other methods", () => { + it("should create a valid PUT extension", () => { + const result = declareDiscoveryExtension({ + input: { id: "123", name: "Updated" }, + inputSchema: { + properties: { + id: { type: "string" }, + name: { type: "string" }, + }, + }, + bodyType: "json", + }); + + const extension = result.bazaar; + expect(extension.info.input.type).toBe("http"); + }); + + it("should create a valid PATCH extension", () => { + const result = declareDiscoveryExtension({ + input: { status: "active" }, + inputSchema: { + properties: { + status: { type: "string" }, + }, + }, + bodyType: "json", + }); + + const extension = result.bazaar; + expect(extension.info.input.type).toBe("http"); + }); + + it("should create a valid DELETE extension", () => { + const result = declareDiscoveryExtension({ + input: { id: "123" }, + inputSchema: { + properties: { + id: { type: "string" }, + }, + }, + }); + + const extension = result.bazaar; + expect(extension.info.input.type).toBe("http"); + }); + + it("should create a valid HEAD extension", () => { + const result = declareDiscoveryExtension({}); + + const extension = result.bazaar; + expect(extension.info.input.type).toBe("http"); + }); + + it("should throw error for unsupported method", () => { + const result = declareDiscoveryExtension({}); + expect(result).toHaveProperty("bazaar"); + }); + }); + + describe("validateDiscoveryExtension", () => { + it("should validate a correct GET extension", () => { + const declared = declareDiscoveryExtension({ + input: { query: "test" }, + inputSchema: { + properties: { + query: { type: "string" }, + }, + }, + }); + + const extension = declared.bazaar; + const result = validateDiscoveryExtension(extension); + expect(result.valid).toBe(true); + expect(result.errors).toBeUndefined(); + }); + + it("should validate a correct POST extension", () => { + const declared = declareDiscoveryExtension({ + input: { name: "John" }, + inputSchema: { + properties: { + name: { type: "string" }, + }, + }, + bodyType: "json", + }); + + const extension = declared.bazaar; + const result = validateDiscoveryExtension(extension); + expect(result.valid).toBe(true); + }); + + it("should detect invalid extension structure", () => { + const invalidExtension = { + info: { + input: { + type: "http", + method: "GET", + }, + }, + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + input: { + type: "object", + properties: { + type: { type: "string", const: "invalid" }, // Should be "http" + method: { type: "string", enum: ["GET"] }, + }, + required: ["type", "method"], + }, + }, + required: ["input"], + }, + } as unknown as DiscoveryExtension; + + const result = validateDiscoveryExtension(invalidExtension); + expect(result.valid).toBe(false); + expect(result.errors).toBeDefined(); + expect(result.errors!.length).toBeGreaterThan(0); + }); + }); + + describe("extractDiscoveryInfoFromExtension", () => { + it("should extract info from a valid extension", () => { + const declared = declareDiscoveryExtension({ + input: { query: "test" }, + inputSchema: { + properties: { + query: { type: "string" }, + }, + }, + }); + + const extension = declared.bazaar; + const info = extractDiscoveryInfoFromExtension(extension); + expect(info).toEqual(extension.info); + expect(info.input.type).toBe("http"); + }); + + it("should extract info without validation when validate=false", () => { + const declared = declareDiscoveryExtension({ + input: { name: "John" }, + inputSchema: { + properties: { + name: { type: "string" }, + }, + }, + bodyType: "json", + }); + + const extension = declared.bazaar; + const info = extractDiscoveryInfoFromExtension(extension, false); + expect(info).toEqual(extension.info); + }); + + it("should throw error for invalid extension when validating", () => { + const invalidExtension = { + info: { + input: { + type: "http", + method: "GET", + }, + }, + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + input: { + type: "object", + properties: { + type: { type: "string", const: "invalid" }, + method: { type: "string", enum: ["GET"] }, + }, + required: ["type", "method"], + }, + }, + required: ["input"], + }, + } as unknown as DiscoveryExtension; + + expect(() => { + extractDiscoveryInfoFromExtension(invalidExtension); + }).toThrow("Invalid discovery extension"); + }); + }); + + describe("extractDiscoveryInfo (full flow)", () => { + it("should extract info from v2 PaymentPayload with extensions", () => { + const declared = declareDiscoveryExtension({ + input: { userId: "123" }, + inputSchema: { + properties: { + userId: { type: "string" }, + }, + }, + bodyType: "json", + }); + + const extension = declared.bazaar; + + const paymentPayload = { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + accepted: {} as unknown, + resource: { url: "http://example.com/test" }, + extensions: { + [BAZAAR]: extension, + }, + }; + + const discovered = extractDiscoveryInfo(paymentPayload, {} as unknown); + + expect(discovered).not.toBeNull(); + expect(discovered!.discoveryInfo.input.type).toBe("http"); + expect(discovered!.resourceUrl).toBe("http://example.com/test"); + }); + + it("should strip query params from v2 resourceUrl", () => { + const declared = declareDiscoveryExtension({ + input: { city: "NYC" }, + inputSchema: { + properties: { + city: { type: "string" }, + }, + }, + }); + + const extension = declared.bazaar; + + const paymentPayload = { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + accepted: {} as unknown, + resource: { + url: "https://api.example.com/weather?city=NYC&units=metric", + description: "Weather API", + mimeType: "application/json", + }, + extensions: { + [BAZAAR]: extension, + }, + }; + + const discovered = extractDiscoveryInfo(paymentPayload, {} as unknown); + + expect(discovered).not.toBeNull(); + expect(discovered!.resourceUrl).toBe("https://api.example.com/weather"); + expect(discovered!.description).toBe("Weather API"); + expect(discovered!.mimeType).toBe("application/json"); + }); + + it("should strip hash sections from v2 resourceUrl", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + + const extension = declared.bazaar; + + const paymentPayload = { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + accepted: {} as unknown, + resource: { + url: "https://api.example.com/docs#section-1", + description: "Docs", + mimeType: "text/html", + }, + extensions: { + [BAZAAR]: extension, + }, + }; + + const discovered = extractDiscoveryInfo(paymentPayload, {} as unknown); + + expect(discovered).not.toBeNull(); + expect(discovered!.resourceUrl).toBe("https://api.example.com/docs"); + }); + + it("should strip both query params and hash sections from v2 resourceUrl", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + + const extension = declared.bazaar; + + const paymentPayload = { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + accepted: {} as unknown, + resource: { + url: "https://api.example.com/page?foo=bar#anchor", + description: "Page", + mimeType: "text/html", + }, + extensions: { + [BAZAAR]: extension, + }, + }; + + const discovered = extractDiscoveryInfo(paymentPayload, {} as unknown); + + expect(discovered).not.toBeNull(); + expect(discovered!.resourceUrl).toBe("https://api.example.com/page"); + }); + + it("should extract info from v1 PaymentRequirements", () => { + const v1Requirements = { + scheme: "exact", + network: "eip155:8453" as unknown, + maxAmountRequired: "10000", + resource: "https://api.example.com/data", + description: "Get data", + mimeType: "application/json", + outputSchema: { + input: { + type: "http", + method: "GET", + discoverable: true, + queryParams: { q: "test" }, + }, + }, + payTo: "0x...", + maxTimeoutSeconds: 300, + asset: "0x...", + extra: {}, + }; + + const v1Payload = { + x402Version: 1, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + }; + + const discovered = extractDiscoveryInfo(v1Payload as unknown, v1Requirements as unknown); + + expect(discovered).not.toBeNull(); + expect(discovered!.discoveryInfo.input.method).toBe("GET"); + expect(discovered!.resourceUrl).toBe("https://api.example.com/data"); + expect(discovered!.method).toBe("GET"); + expect(discovered!.description).toBe("Get data"); + expect(discovered!.mimeType).toBe("application/json"); + }); + + it("should strip query params from v1 resourceUrl", () => { + const v1Requirements = { + scheme: "exact", + network: "eip155:8453" as unknown, + maxAmountRequired: "10000", + resource: "https://api.example.com/search?q=test&page=1", + description: "Search", + mimeType: "application/json", + outputSchema: { + input: { + type: "http", + method: "GET", + discoverable: true, + queryParams: { q: "string", page: "number" }, + }, + }, + payTo: "0x...", + maxTimeoutSeconds: 300, + asset: "0x...", + extra: {}, + }; + + const v1Payload = { + x402Version: 1, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + }; + + const discovered = extractDiscoveryInfo(v1Payload as unknown, v1Requirements as unknown); + + expect(discovered).not.toBeNull(); + expect(discovered!.resourceUrl).toBe("https://api.example.com/search"); + }); + + it("should strip hash sections from v1 resourceUrl", () => { + const v1Requirements = { + scheme: "exact", + network: "eip155:8453" as unknown, + maxAmountRequired: "10000", + resource: "https://api.example.com/docs#section", + description: "Docs", + mimeType: "application/json", + outputSchema: { + input: { + type: "http", + method: "GET", + discoverable: true, + }, + }, + payTo: "0x...", + maxTimeoutSeconds: 300, + asset: "0x...", + extra: {}, + }; + + const v1Payload = { + x402Version: 1, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + }; + + const discovered = extractDiscoveryInfo(v1Payload as unknown, v1Requirements as unknown); + + expect(discovered).not.toBeNull(); + expect(discovered!.resourceUrl).toBe("https://api.example.com/docs"); + }); + + it("should return null when no discovery info is present", () => { + const paymentPayload = { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + accepted: {} as unknown, + }; + + const discovered = extractDiscoveryInfo(paymentPayload, {} as unknown); + + expect(discovered).toBeNull(); + }); + }); + + describe("validateAndExtract", () => { + it("should return valid result with info for correct extension", () => { + const declared = declareDiscoveryExtension({ + input: { query: "test" }, + inputSchema: { + properties: { + query: { type: "string" }, + }, + }, + }); + + const extension = declared.bazaar; + const result = validateAndExtract(extension); + expect(result.valid).toBe(true); + expect(result.info).toEqual(extension.info); + expect(result.errors).toBeUndefined(); + }); + + it("should return invalid result with errors for incorrect extension", () => { + const invalidExtension = { + info: { + input: { + type: "http", + method: "GET", + }, + }, + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + input: { + type: "object", + properties: { + type: { type: "string", const: "invalid" }, + method: { type: "string", enum: ["GET"] }, + }, + required: ["type", "method"], + }, + }, + required: ["input"], + }, + } as unknown as DiscoveryExtension; + + const result = validateAndExtract(invalidExtension); + expect(result.valid).toBe(false); + expect(result.info).toBeUndefined(); + expect(result.errors).toBeDefined(); + expect(result.errors!.length).toBeGreaterThan(0); + }); + }); + + describe("V1 Transformation", () => { + it("should extract discovery info from v1 GET with no params", () => { + const v1Requirements = { + scheme: "exact", + network: "eip155:8453" as unknown, + maxAmountRequired: "100000", + resource: "https://api.example.com/data", + description: "Get data", + mimeType: "application/json", + outputSchema: { + input: { + type: "http", + method: "GET", + discoverable: true, + }, + output: null, + }, + payTo: "0x...", + maxTimeoutSeconds: 300, + asset: "0x...", + extra: {}, + }; + + const info = extractDiscoveryInfoV1(v1Requirements as unknown); + expect(info).not.toBeNull(); + expect(info!.input.method).toBe("GET"); + expect(info!.input.type).toBe("http"); + }); + + it("should extract discovery info from v1 GET with queryParams", () => { + const v1Requirements = { + scheme: "exact", + network: "eip155:8453" as unknown, + maxAmountRequired: "10000", + resource: "https://api.example.com/list", + description: "List items", + mimeType: "application/json", + outputSchema: { + input: { + discoverable: true, + method: "GET", + queryParams: { + limit: "integer parameter", + offset: "integer parameter", + }, + type: "http", + }, + output: { type: "array" }, + }, + payTo: "0x...", + maxTimeoutSeconds: 300, + asset: "0x...", + extra: {}, + }; + + const info = extractDiscoveryInfoV1(v1Requirements as unknown); + expect(info).not.toBeNull(); + expect(info!.input.method).toBe("GET"); + expect(info!.input.queryParams).toEqual({ + limit: "integer parameter", + offset: "integer parameter", + }); + }); + + it("should extract discovery info from v1 POST with bodyFields", () => { + const v1Requirements = { + scheme: "exact", + network: "eip155:8453" as unknown, + maxAmountRequired: "10000", + resource: "https://api.example.com/search", + description: "Search", + mimeType: "application/json", + outputSchema: { + input: { + bodyFields: { + query: { + description: "Search query", + required: true, + type: "string", + }, + }, + bodyType: "json", + discoverable: true, + method: "POST", + type: "http", + }, + }, + payTo: "0x...", + maxTimeoutSeconds: 120, + asset: "0x...", + extra: {}, + }; + + const info = extractDiscoveryInfoV1(v1Requirements as unknown); + expect(info).not.toBeNull(); + expect(info!.input.method).toBe("POST"); + expect((info as BodyDiscoveryInfo).input.bodyType).toBe("json"); + expect((info as BodyDiscoveryInfo).input.body).toEqual({ + query: { + description: "Search query", + required: true, + type: "string", + }, + }); + }); + + it("should extract discovery info from v1 POST with snake_case fields", () => { + const v1Requirements = { + scheme: "exact", + network: "eip155:8453" as unknown, + maxAmountRequired: "1000", + resource: "https://api.example.com/action", + description: "Action", + mimeType: "application/json", + outputSchema: { + input: { + body_fields: null, + body_type: null, + discoverable: true, + header_fields: { + "X-Budget": { + description: "Budget", + required: false, + type: "string", + }, + }, + method: "POST", + query_params: null, + type: "http", + }, + output: null, + }, + payTo: "0x...", + maxTimeoutSeconds: 60, + asset: "0x...", + extra: {}, + }; + + const info = extractDiscoveryInfoV1(v1Requirements as unknown); + expect(info).not.toBeNull(); + expect(info!.input.method).toBe("POST"); + expect(info!.input.headers).toEqual({ + "X-Budget": { + description: "Budget", + required: false, + type: "string", + }, + }); + }); + + it("should extract discovery info from v1 POST with bodyParams", () => { + const v1Requirements = { + scheme: "exact", + network: "eip155:8453" as unknown, + maxAmountRequired: "50000", + resource: "https://api.example.com/query", + description: "Query", + mimeType: "application/json", + outputSchema: { + input: { + bodyParams: { + question: { + description: "Question", + required: true, + type: "string", + maxLength: 500, + }, + }, + discoverable: true, + method: "POST", + type: "http", + }, + }, + payTo: "0x...", + maxTimeoutSeconds: 300, + asset: "0x...", + extra: {}, + }; + + const info = extractDiscoveryInfoV1(v1Requirements as unknown); + expect(info).not.toBeNull(); + expect(info!.input.method).toBe("POST"); + expect((info as BodyDiscoveryInfo).input.body).toEqual({ + question: { + description: "Question", + required: true, + type: "string", + maxLength: 500, + }, + }); + }); + + it("should extract discovery info from v1 POST with properties field", () => { + const v1Requirements = { + scheme: "exact", + network: "eip155:8453" as unknown, + maxAmountRequired: "80000", + resource: "https://api.example.com/chat", + description: "Chat", + mimeType: "application/json", + outputSchema: { + input: { + discoverable: true, + method: "POST", + properties: { + message: { + description: "Message", + type: "string", + }, + stream: { + description: "Stream", + type: "boolean", + }, + }, + required: ["message"], + type: "http", + }, + }, + payTo: "0x...", + maxTimeoutSeconds: 60, + asset: "0x...", + extra: {}, + }; + + const info = extractDiscoveryInfoV1(v1Requirements as unknown); + expect(info).not.toBeNull(); + expect(info!.input.method).toBe("POST"); + expect((info as BodyDiscoveryInfo).input.body).toEqual({ + message: { + description: "Message", + type: "string", + }, + stream: { + description: "Stream", + type: "boolean", + }, + }); + }); + + it("should handle v1 POST with no body content (minimal)", () => { + const v1Requirements = { + scheme: "exact", + network: "eip155:8453" as unknown, + maxAmountRequired: "10000", + resource: "https://api.example.com/action", + description: "Action", + mimeType: "application/json", + outputSchema: { + input: { + discoverable: true, + method: "POST", + type: "http", + }, + }, + payTo: "0x...", + maxTimeoutSeconds: 60, + asset: "0x...", + extra: {}, + }; + + const info = extractDiscoveryInfoV1(v1Requirements as unknown); + expect(info).not.toBeNull(); + expect(info!.input.method).toBe("POST"); + expect((info as BodyDiscoveryInfo).input.bodyType).toBe("json"); + expect((info as BodyDiscoveryInfo).input.body).toEqual({}); + }); + + it("should skip non-discoverable endpoints", () => { + const v1Requirements = { + scheme: "exact", + network: "eip155:8453" as unknown, + maxAmountRequired: "10000", + resource: "https://api.example.com/internal", + description: "Internal", + mimeType: "application/json", + outputSchema: { + input: { + discoverable: false, + method: "POST", + type: "http", + }, + }, + payTo: "0x...", + maxTimeoutSeconds: 60, + asset: "0x...", + extra: {}, + }; + + const info = extractDiscoveryInfoV1(v1Requirements as unknown); + expect(info).toBeNull(); + }); + + it("should handle missing outputSchema", () => { + const v1Requirements = { + scheme: "exact", + network: "eip155:8453" as unknown, + maxAmountRequired: "10000", + resource: "https://api.example.com/resource", + description: "Resource", + mimeType: "application/json", + outputSchema: {}, + payTo: "0x...", + maxTimeoutSeconds: 60, + asset: "0x...", + extra: {}, + }; + + const info = extractDiscoveryInfoV1(v1Requirements as unknown); + expect(info).toBeNull(); + }); + }); + + describe("Integration - Full workflow", () => { + it("should handle GET endpoint with output schema (e2e scenario)", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { + properties: {}, + }, + output: { + example: { + message: "Protected endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + }, + required: ["message", "timestamp"], + }, + }, + }); + + const extension = declared.bazaar; + + const validation = validateDiscoveryExtension(extension); + + if (!validation.valid) { + console.log("Validation errors:", validation.errors); + console.log("Extension info:", extension.info); + console.log("Extension schema:", extension.schema); + } + + expect(validation.valid).toBe(true); + + const info = extractDiscoveryInfoFromExtension(extension, false); + expect(info.input.type).toBe("http"); + expect(info.output?.example).toEqual({ + message: "Protected endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }); + }); + + it("should handle complete v2 server-to-facilitator workflow", () => { + const declared = declareDiscoveryExtension({ + input: { userId: "123", action: "create" }, + inputSchema: { + properties: { + userId: { type: "string" }, + action: { type: "string", enum: ["create", "update", "delete"] }, + }, + required: ["userId", "action"], + }, + bodyType: "json", + output: { + example: { success: true, id: "new-id" }, + }, + }); + + const extension = declared.bazaar; + + const paymentRequired = { + x402Version: 2, + resource: { + url: "/api/action", + description: "Execute an action", + mimeType: "application/json", + }, + accepts: [], + extensions: { + [BAZAAR]: extension, + }, + }; + + const bazaarExt = paymentRequired.extensions?.[BAZAAR] as DiscoveryExtension; + expect(bazaarExt).toBeDefined(); + + const validation = validateDiscoveryExtension(bazaarExt); + expect(validation.valid).toBe(true); + + const info = extractDiscoveryInfoFromExtension(bazaarExt, false); + expect(info.input.type).toBe("http"); + expect((info as BodyDiscoveryInfo).input.bodyType).toBe("json"); + expect((info as BodyDiscoveryInfo).input.body).toEqual({ userId: "123", action: "create" }); + expect(info.output?.example).toEqual({ success: true, id: "new-id" }); + + // Facilitator can now catalog this endpoint in the Bazaar + }); + + it("should handle v1-to-v2 transformation workflow", () => { + // V1 PaymentRequirements from real Bazaar data + const v1Requirements = { + scheme: "exact", + network: "eip155:8453" as unknown, + maxAmountRequired: "10000", + resource: "https://mesh.heurist.xyz/x402/agents/TokenResolverAgent/search", + description: "Find tokens by address, ticker/symbol, or token name", + mimeType: "application/json", + outputSchema: { + input: { + bodyFields: { + chain: { + description: "Optional chain hint", + type: "string", + }, + query: { + description: "Token search query", + required: true, + type: "string", + }, + type_hint: { + description: "Optional type hint", + enum: ["address", "symbol", "name"], + type: "string", + }, + }, + bodyType: "json", + discoverable: true, + method: "POST", + type: "http", + }, + }, + payTo: "0x7d9d1821d15B9e0b8Ab98A058361233E255E405D", + maxTimeoutSeconds: 120, + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + extra: {}, + }; + + const v1Payload = { + x402Version: 1, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + }; + + const discovered = extractDiscoveryInfo(v1Payload as unknown, v1Requirements as unknown); + + expect(discovered).not.toBeNull(); + expect(discovered!.discoveryInfo.input.method).toBe("POST"); + expect(discovered!.method).toBe("POST"); + expect((discovered!.discoveryInfo as BodyDiscoveryInfo).input.bodyType).toBe("json"); + expect((discovered!.discoveryInfo as BodyDiscoveryInfo).input.body).toHaveProperty("query"); + expect((discovered!.discoveryInfo as BodyDiscoveryInfo).input.body).toHaveProperty("chain"); + expect((discovered!.discoveryInfo as BodyDiscoveryInfo).input.body).toHaveProperty( + "type_hint", + ); + }); + + it("should handle unified extraction for both v1 and v2", () => { + const declared = declareDiscoveryExtension({ + input: { limit: 10 }, + inputSchema: { + properties: { + limit: { type: "number" }, + }, + }, + }); + + const v2Extension = declared.bazaar; + + const v2Payload = { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + accepted: {} as unknown, + resource: { url: "http://example.com/v2" }, + extensions: { + [BAZAAR]: v2Extension, + }, + }; + + const v2Discovered = extractDiscoveryInfo(v2Payload, {} as unknown); + + expect(v2Discovered).not.toBeNull(); + expect(v2Discovered!.discoveryInfo.input.type).toBe("http"); + expect(v2Discovered!.resourceUrl).toBe("http://example.com/v2"); + + const v1Requirements = { + scheme: "exact", + network: "eip155:8453" as unknown, + maxAmountRequired: "10000", + resource: "https://api.example.com/list", + description: "List", + mimeType: "application/json", + outputSchema: { + input: { + discoverable: true, + method: "GET", + queryParams: { limit: "number" }, + type: "http", + }, + }, + payTo: "0x...", + maxTimeoutSeconds: 300, + asset: "0x...", + extra: {}, + }; + + const v1Payload = { + x402Version: 1, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + }; + + const v1Discovered = extractDiscoveryInfo(v1Payload as unknown, v1Requirements as unknown); + + expect(v1Discovered).not.toBeNull(); + expect(v1Discovered!.method).toBe("GET"); + expect(v1Discovered!.resourceUrl).toBe("https://api.example.com/list"); + + expect(typeof v2Discovered!.discoveryInfo.input).toBe( + typeof v1Discovered!.discoveryInfo.input, + ); + }); + }); + + describe("bazaarResourceServerExtension", () => { + // Helper to extract method enum from schema + const extractMethodEnum = (schema: Record): string[] => { + const props = schema.properties as Record; + const input = props.input as Record; + const inputProps = input.properties as Record; + const method = inputProps.method as Record; + return method.enum as string[]; + }; + + // Helper to extract required fields from schema + const extractRequiredFields = (schema: Record): string[] => { + const props = schema.properties as Record; + const input = props.input as Record; + return input.required as string[]; + }; + + // Mock adapter for testing + const createMockAdapter = (): HTTPAdapter => ({ + getHeader: () => undefined, + getMethod: () => "POST", + getPath: () => "/test", + getUrl: () => "http://localhost/test", + getAcceptHeader: () => "application/json", + getUserAgent: () => "test-agent", + }); + + it("should narrow method enum in schema for POST request", () => { + const declared = declareDiscoveryExtension({ + input: { prompt: "test" }, + inputSchema: { properties: { prompt: { type: "string" } } }, + bodyType: "json", + }); + + const extension = declared.bazaar; + + // Before enrichment, schema has broad enum + const beforeEnum = extractMethodEnum(extension.schema as Record); + expect(beforeEnum).toEqual(["POST", "PUT", "PATCH"]); + + const httpContext: HTTPRequestContext = { + method: "POST", + path: "/test", + adapter: createMockAdapter(), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as DiscoveryExtension; + + // After enrichment, schema should have narrow enum + const afterEnum = extractMethodEnum(enriched.schema as Record); + expect(afterEnum).toEqual(["POST"]); + }); + + it("should narrow method enum in schema for GET request", () => { + const declared = declareDiscoveryExtension({ + input: { query: "test" }, + inputSchema: { properties: { query: { type: "string" } } }, + }); + + const extension = declared.bazaar; + + // Before enrichment, schema has broad enum + const beforeEnum = extractMethodEnum(extension.schema as Record); + expect(beforeEnum).toEqual(["GET", "HEAD", "DELETE"]); + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/test", + adapter: createMockAdapter(), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as DiscoveryExtension; + + // After enrichment, schema should have narrow enum + const afterEnum = extractMethodEnum(enriched.schema as Record); + expect(afterEnum).toEqual(["GET"]); + }); + + it("should enrich declaration with method in info.input", () => { + const declared = declareDiscoveryExtension({ + input: { data: "test" }, + inputSchema: { properties: { data: { type: "string" } } }, + bodyType: "json", + }); + + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "POST", + path: "/test", + adapter: createMockAdapter(), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as DiscoveryExtension; + + // Method should be set in info.input + expect((enriched.info as BodyDiscoveryInfo).input.method).toBe("POST"); + }); + + it("should add method to required array if not already present", () => { + const declared = declareDiscoveryExtension({ + input: { prompt: "test" }, + inputSchema: { properties: { prompt: { type: "string" } } }, + bodyType: "json", + }); + + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "POST", + path: "/test", + adapter: createMockAdapter(), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as DiscoveryExtension; + + const required = extractRequiredFields(enriched.schema as Record); + expect(required).toContain("method"); + }); + + it("should return unchanged declaration for non-HTTP context", () => { + const declared = declareDiscoveryExtension({ + input: { data: "test" }, + inputSchema: { properties: { data: { type: "string" } } }, + bodyType: "json", + }); + + const extension = declared.bazaar; + + // Non-HTTP context (missing adapter property) + const nonHTTPContext = { method: "POST" }; + + const result = bazaarResourceServerExtension.enrichDeclaration!( + extension, + nonHTTPContext, + ) as DiscoveryExtension; + + // Should return unchanged - schema still has broad enum + const methodEnum = extractMethodEnum(result.schema as Record); + expect(methodEnum).toEqual(["POST", "PUT", "PATCH"]); + }); + }); +}); diff --git a/typescript/packages/extensions/test/facilitatorClient.test.ts b/typescript/packages/extensions/test/facilitatorClient.test.ts new file mode 100644 index 0000000..d816242 --- /dev/null +++ b/typescript/packages/extensions/test/facilitatorClient.test.ts @@ -0,0 +1,272 @@ +/** + * Tests for Bazaar Client Extension - facilitatorClient + * + * Tests the client-side discovery types and withBazaar extension. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { + withBazaar, + type DiscoveryResource, + type DiscoveryResourcesResponse, + type ListDiscoveryResourcesParams, +} from "../src/bazaar/facilitatorClient"; +import { HTTPFacilitatorClient } from "@x402/core/http"; + +describe("Bazaar Client Extension - facilitatorClient", () => { + describe("Type definitions", () => { + it("DiscoveryResource should have correct shape with all required fields", () => { + // Type-level validation - ensures the interface compiles with correct fields + const resource: DiscoveryResource = { + resource: "https://api.example.com/endpoint", + type: "http", + x402Version: 2, + accepts: [ + { + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + amount: "10000", + payTo: "0x1234567890123456789012345678901234567890", + maxTimeoutSeconds: 60, + extra: {}, + }, + ], + lastUpdated: "2024-01-01T00:00:00.000Z", + metadata: { category: "weather" }, + }; + + expect(resource.resource).toBe("https://api.example.com/endpoint"); + expect(resource.type).toBe("http"); + expect(resource.x402Version).toBe(2); + expect(resource.accepts).toHaveLength(1); + expect(resource.lastUpdated).toBe("2024-01-01T00:00:00.000Z"); + expect(resource.metadata).toEqual({ category: "weather" }); + }); + + it("DiscoveryResource should allow optional metadata", () => { + const resource: DiscoveryResource = { + resource: "https://api.example.com/endpoint", + type: "http", + x402Version: 1, + accepts: [], + lastUpdated: "2024-01-01T00:00:00.000Z", + // metadata is optional + }; + + expect(resource.metadata).toBeUndefined(); + }); + + it("DiscoveryResourcesResponse should have correct shape with pagination", () => { + const response: DiscoveryResourcesResponse = { + x402Version: 2, + items: [ + { + resource: "https://api.example.com/endpoint", + type: "http", + x402Version: 1, + accepts: [], + lastUpdated: "2024-01-01T00:00:00.000Z", + }, + ], + pagination: { + limit: 20, + offset: 0, + total: 100, + }, + }; + + expect(response.x402Version).toBe(2); + expect(response.items).toHaveLength(1); + expect(response.pagination.limit).toBe(20); + expect(response.pagination.offset).toBe(0); + expect(response.pagination.total).toBe(100); + }); + + it("ListDiscoveryResourcesParams should accept optional parameters", () => { + const params1: ListDiscoveryResourcesParams = {}; + const params2: ListDiscoveryResourcesParams = { type: "http" }; + const params3: ListDiscoveryResourcesParams = { type: "http", limit: 10, offset: 5 }; + + expect(params1.type).toBeUndefined(); + expect(params2.type).toBe("http"); + expect(params3.limit).toBe(10); + expect(params3.offset).toBe(5); + }); + }); + + describe("withBazaar", () => { + let mockFetch: ReturnType; + let originalFetch: typeof globalThis.fetch; + + beforeEach(() => { + originalFetch = globalThis.fetch; + mockFetch = vi.fn(); + globalThis.fetch = mockFetch; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + vi.restoreAllMocks(); + }); + + it("should extend client with discovery.listResources method", () => { + const facilitatorClient = new HTTPFacilitatorClient({ + url: "https://x402.org/facilitator", + }); + + const extendedClient = withBazaar(facilitatorClient); + + expect(extendedClient.extensions).toBeDefined(); + expect(extendedClient.extensions.discovery).toBeDefined(); + expect(typeof extendedClient.extensions.discovery.listResources).toBe("function"); + }); + + it("should preserve existing extensions when chaining", () => { + const facilitatorClient = new HTTPFacilitatorClient({ + url: "https://x402.org/facilitator", + }); + + // Simulate a client with existing extensions + const clientWithExtensions = facilitatorClient as typeof facilitatorClient & { + extensions: { other: { someMethod: () => string } }; + }; + clientWithExtensions.extensions = { + other: { someMethod: () => "test" }, + }; + + const extendedClient = withBazaar(clientWithExtensions); + + // Should have both the existing and new extensions + expect(extendedClient.extensions.discovery).toBeDefined(); + expect((extendedClient.extensions as { other?: unknown }).other).toBeDefined(); + }); + + it("listResources should call correct endpoint with no params", async () => { + const mockResponse: DiscoveryResourcesResponse = { + x402Version: 2, + items: [], + pagination: { limit: 20, offset: 0, total: 0 }, + }; + + mockFetch.mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockResponse), + }); + + const facilitatorClient = new HTTPFacilitatorClient({ + url: "https://x402.org/facilitator", + }); + const extendedClient = withBazaar(facilitatorClient); + + const result = await extendedClient.extensions.discovery.listResources(); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const [url, options] = mockFetch.mock.calls[0]; + expect(url).toBe("https://x402.org/facilitator/discovery/resources"); + expect(options.method).toBe("GET"); + expect(result).toEqual(mockResponse); + }); + + it("listResources should include query params when provided", async () => { + const mockResponse: DiscoveryResourcesResponse = { + x402Version: 2, + items: [], + pagination: { limit: 10, offset: 5, total: 100 }, + }; + + mockFetch.mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockResponse), + }); + + const facilitatorClient = new HTTPFacilitatorClient({ + url: "https://x402.org/facilitator", + }); + const extendedClient = withBazaar(facilitatorClient); + + await extendedClient.extensions.discovery.listResources({ + type: "http", + limit: 10, + offset: 5, + }); + + const [url] = mockFetch.mock.calls[0]; + expect(url).toContain("type=http"); + expect(url).toContain("limit=10"); + expect(url).toContain("offset=5"); + }); + + it("listResources should throw error on non-ok response", async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 500, + statusText: "Internal Server Error", + text: () => Promise.resolve("Server error"), + }); + + const facilitatorClient = new HTTPFacilitatorClient({ + url: "https://x402.org/facilitator", + }); + const extendedClient = withBazaar(facilitatorClient); + + await expect(extendedClient.extensions.discovery.listResources()).rejects.toThrow( + "Facilitator listDiscoveryResources failed (500)", + ); + }); + + it("listResources should return properly typed response matching CDP API", async () => { + // Mock response matching actual CDP API structure + const mockResponse: DiscoveryResourcesResponse = { + x402Version: 1, + items: [ + { + resource: "https://x402.mode.network/ta/indicators", + type: "http", + x402Version: 1, + accepts: [ + { + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + amount: "1000", + payTo: "0xa2477E16dCB42E2AD80f03FE97D7F1a1646cd1c0", + maxTimeoutSeconds: 60, + extra: { name: "USD Coin", version: "2" }, + }, + ], + lastUpdated: "2024-01-01T00:00:00.000Z", + metadata: {}, + }, + ], + pagination: { + limit: 1, + offset: 0, + total: 12234, + }, + }; + + mockFetch.mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockResponse), + }); + + const facilitatorClient = new HTTPFacilitatorClient({ + url: "https://x402.org/facilitator", + }); + const extendedClient = withBazaar(facilitatorClient); + + const result = await extendedClient.extensions.discovery.listResources({ limit: 1 }); + + // Validate response structure matches our fixed types + expect(result.x402Version).toBe(1); + expect(result.items).toHaveLength(1); + expect(result.items[0].resource).toBe("https://x402.mode.network/ta/indicators"); + expect(result.items[0].type).toBe("http"); + expect(result.items[0].x402Version).toBe(1); + expect(result.items[0].accepts).toHaveLength(1); + expect(result.items[0].lastUpdated).toBe("2024-01-01T00:00:00.000Z"); + expect(result.pagination.total).toBe(12234); + }); + }); +}); diff --git a/typescript/packages/extensions/test/payment-identifier.test.ts b/typescript/packages/extensions/test/payment-identifier.test.ts new file mode 100644 index 0000000..919ffec --- /dev/null +++ b/typescript/packages/extensions/test/payment-identifier.test.ts @@ -0,0 +1,669 @@ +import { describe, it, expect } from "vitest"; +import type { PaymentPayload } from "@x402/core"; +import { + PAYMENT_IDENTIFIER, + PAYMENT_ID_MIN_LENGTH, + PAYMENT_ID_MAX_LENGTH, + PAYMENT_ID_PATTERN, + generatePaymentId, + isValidPaymentId, + appendPaymentIdentifierToExtensions, + declarePaymentIdentifierExtension, + paymentIdentifierResourceServerExtension, + validatePaymentIdentifier, + extractPaymentIdentifier, + extractAndValidatePaymentIdentifier, + hasPaymentIdentifier, + isPaymentIdentifierExtension, + isPaymentIdentifierRequired, + validatePaymentIdentifierRequirement, + paymentIdentifierSchema, +} from "../src/payment-identifier"; + +/** + * Helper to create an extension with ID appended (mimics client flow) + * + * @param id - Optional payment ID to use. If not provided, a new ID will be generated. + * @param required - Whether the payment identifier is required (defaults to false). + * @returns The payment-identifier extension object with the ID appended. + */ +function createExtensionWithId(id?: string, required: boolean = false) { + const extensions: Record = { + [PAYMENT_IDENTIFIER]: declarePaymentIdentifierExtension(required), + }; + appendPaymentIdentifierToExtensions(extensions, id); + return extensions[PAYMENT_IDENTIFIER]; +} + +describe("Payment-Identifier Extension", () => { + describe("Constants", () => { + it("should export the correct extension key", () => { + expect(PAYMENT_IDENTIFIER).toBe("payment-identifier"); + }); + + it("should export correct length constraints", () => { + expect(PAYMENT_ID_MIN_LENGTH).toBe(16); + expect(PAYMENT_ID_MAX_LENGTH).toBe(128); + }); + + it("should export correct pattern", () => { + expect(PAYMENT_ID_PATTERN).toBeInstanceOf(RegExp); + expect(PAYMENT_ID_PATTERN.test("valid_id_123")).toBe(true); + expect(PAYMENT_ID_PATTERN.test("invalid!@#")).toBe(false); + }); + }); + + describe("generatePaymentId", () => { + it("should generate an ID with default prefix", () => { + const id = generatePaymentId(); + expect(id).toMatch(/^pay_[a-f0-9]{32}$/); + }); + + it("should generate an ID with custom prefix", () => { + const id = generatePaymentId("txn_"); + expect(id).toMatch(/^txn_[a-f0-9]{32}$/); + }); + + it("should generate an ID without prefix when empty string provided", () => { + const id = generatePaymentId(""); + expect(id).toMatch(/^[a-f0-9]{32}$/); + }); + + it("should generate unique IDs", () => { + const ids = new Set(); + for (let i = 0; i < 100; i++) { + ids.add(generatePaymentId()); + } + expect(ids.size).toBe(100); + }); + + it("should generate IDs that pass validation", () => { + const id = generatePaymentId(); + expect(isValidPaymentId(id)).toBe(true); + }); + }); + + describe("isValidPaymentId", () => { + it("should accept valid IDs", () => { + expect(isValidPaymentId("pay_7d5d747be160e280")).toBe(true); + expect(isValidPaymentId("1234567890123456")).toBe(true); + expect(isValidPaymentId("abcdefghijklmnop")).toBe(true); + expect(isValidPaymentId("test_with-hyphens")).toBe(true); + expect(isValidPaymentId("test_with_underscores")).toBe(true); + }); + + it("should reject IDs that are too short", () => { + expect(isValidPaymentId("abc")).toBe(false); + expect(isValidPaymentId("123456789012345")).toBe(false); // 15 chars + }); + + it("should reject IDs that are too long", () => { + const longId = "a".repeat(129); + expect(isValidPaymentId(longId)).toBe(false); + }); + + it("should accept IDs at boundary lengths", () => { + const minId = "a".repeat(16); + const maxId = "a".repeat(128); + expect(isValidPaymentId(minId)).toBe(true); + expect(isValidPaymentId(maxId)).toBe(true); + }); + + it("should reject IDs with invalid characters", () => { + expect(isValidPaymentId("pay_abc!@#$%^&*()")).toBe(false); + expect(isValidPaymentId("pay_abc def ghij")).toBe(false); + expect(isValidPaymentId("pay_abc.def.ghij")).toBe(false); + }); + + it("should reject non-string values", () => { + expect(isValidPaymentId(null as unknown as string)).toBe(false); + expect(isValidPaymentId(undefined as unknown as string)).toBe(false); + expect(isValidPaymentId(123 as unknown as string)).toBe(false); + }); + }); + + describe("appendPaymentIdentifierToExtensions", () => { + it("should append auto-generated ID when extension exists", () => { + const extensions: Record = { + [PAYMENT_IDENTIFIER]: declarePaymentIdentifierExtension(), + }; + const result = appendPaymentIdentifierToExtensions(extensions); + + expect(result).toBe(extensions); // Same reference + const ext = extensions[PAYMENT_IDENTIFIER] as { info: { required: boolean; id?: string } }; + expect(ext.info.id).toMatch(/^pay_[a-f0-9]{32}$/); + expect(ext.info.required).toBe(false); + }); + + it("should append custom ID when extension exists", () => { + const customId = "custom_id_1234567890"; + const extensions: Record = { + [PAYMENT_IDENTIFIER]: declarePaymentIdentifierExtension(), + }; + appendPaymentIdentifierToExtensions(extensions, customId); + + const ext = extensions[PAYMENT_IDENTIFIER] as { info: { required: boolean; id?: string } }; + expect(ext.info.id).toBe(customId); + }); + + it("should preserve required=true from server declaration", () => { + const extensions: Record = { + [PAYMENT_IDENTIFIER]: declarePaymentIdentifierExtension(true), + }; + appendPaymentIdentifierToExtensions(extensions); + + const ext = extensions[PAYMENT_IDENTIFIER] as { info: { required: boolean; id?: string } }; + expect(ext.info.required).toBe(true); + expect(ext.info.id).toMatch(/^pay_[a-f0-9]{32}$/); + }); + + it("should not modify extensions when payment-identifier is not present", () => { + const extensions: Record = { other: { foo: "bar" } }; + const result = appendPaymentIdentifierToExtensions(extensions); + + expect(result).toBe(extensions); + expect(extensions[PAYMENT_IDENTIFIER]).toBeUndefined(); + expect(extensions.other).toEqual({ foo: "bar" }); + }); + + it("should not modify extensions when payment-identifier has no info", () => { + const extensions: Record = { + [PAYMENT_IDENTIFIER]: { schema: paymentIdentifierSchema }, + }; + const result = appendPaymentIdentifierToExtensions(extensions); + + expect(result).toBe(extensions); + const ext = extensions[PAYMENT_IDENTIFIER] as { info?: unknown }; + expect(ext.info).toBeUndefined(); + }); + + it("should throw error for invalid custom ID", () => { + const extensions: Record = { + [PAYMENT_IDENTIFIER]: declarePaymentIdentifierExtension(), + }; + expect(() => appendPaymentIdentifierToExtensions(extensions, "short")).toThrow(); + expect(() => appendPaymentIdentifierToExtensions(extensions, "invalid!@#$%^&")).toThrow(); + }); + + it("should not throw when extension doesn't exist and custom ID provided", () => { + const extensions: Record = { other: {} }; + const result = appendPaymentIdentifierToExtensions(extensions, "valid_id_12345678"); + expect(result).toBe(extensions); + expect(extensions[PAYMENT_IDENTIFIER]).toBeUndefined(); + }); + + it("should overwrite existing id if called multiple times", () => { + const extensions: Record = { + [PAYMENT_IDENTIFIER]: declarePaymentIdentifierExtension(), + }; + appendPaymentIdentifierToExtensions(extensions, "first_id_12345678"); + const ext1 = extensions[PAYMENT_IDENTIFIER] as { info: { id?: string } }; + expect(ext1.info.id).toBe("first_id_12345678"); + + appendPaymentIdentifierToExtensions(extensions, "second_id_12345678"); + const ext2 = extensions[PAYMENT_IDENTIFIER] as { info: { id?: string } }; + expect(ext2.info.id).toBe("second_id_12345678"); + }); + }); + + describe("declarePaymentIdentifierExtension", () => { + it("should return a declaration with required=false by default", () => { + const declaration = declarePaymentIdentifierExtension(); + expect(declaration.info).toEqual({ required: false }); + }); + + it("should return a declaration with required=true when specified", () => { + const declaration = declarePaymentIdentifierExtension(true); + expect(declaration.info).toEqual({ required: true }); + }); + + it("should include the schema", () => { + const declaration = declarePaymentIdentifierExtension(); + expect(declaration.schema).toBeDefined(); + expect(declaration.schema.$schema).toBe("https://json-schema.org/draft/2020-12/schema"); + expect(declaration.schema.properties.required.type).toBe("boolean"); + expect(declaration.schema.properties.id.minLength).toBe(16); + expect(declaration.schema.properties.id.maxLength).toBe(128); + }); + }); + + describe("validatePaymentIdentifier", () => { + it("should validate a correct extension", () => { + const extension = createExtensionWithId(); + const result = validatePaymentIdentifier(extension); + expect(result.valid).toBe(true); + expect(result.errors).toBeUndefined(); + }); + + it("should reject non-object extension", () => { + expect(validatePaymentIdentifier(null).valid).toBe(false); + expect(validatePaymentIdentifier(undefined).valid).toBe(false); + expect(validatePaymentIdentifier("string").valid).toBe(false); + }); + + it("should reject extension without info", () => { + const result = validatePaymentIdentifier({ schema: paymentIdentifierSchema }); + expect(result.valid).toBe(false); + expect(result.errors).toContain("Extension must have an 'info' property"); + }); + + it("should reject extension without required in info", () => { + const result = validatePaymentIdentifier({ + info: { id: "pay_valid_id_12345678" }, + schema: paymentIdentifierSchema, + }); + expect(result.valid).toBe(false); + expect(result.errors).toContain("Extension info must have a 'required' boolean property"); + }); + + it("should validate extension with required but no id", () => { + const result = validatePaymentIdentifier({ + info: { required: false }, + schema: paymentIdentifierSchema, + }); + expect(result.valid).toBe(true); + }); + + it("should reject extension with invalid id format", () => { + const result = validatePaymentIdentifier({ + info: { required: false, id: "short" }, + schema: paymentIdentifierSchema, + }); + expect(result.valid).toBe(false); + }); + + it("should reject extension with non-string id", () => { + const result = validatePaymentIdentifier({ + info: { required: false, id: 123 as unknown as string }, + schema: paymentIdentifierSchema, + }); + expect(result.valid).toBe(false); + expect(result.errors).toContain("Extension info 'id' must be a string if provided"); + }); + + it("should validate extension with valid schema", () => { + const result = validatePaymentIdentifier({ + info: { required: false, id: "valid_id_12345678" }, + schema: paymentIdentifierSchema, + }); + expect(result.valid).toBe(true); + }); + + it("should reject extension that fails schema validation", () => { + const invalidSchema = { + ...paymentIdentifierSchema, + properties: { + ...paymentIdentifierSchema.properties, + required: { type: "string" }, // Wrong type + }, + }; + const result = validatePaymentIdentifier({ + info: { required: false }, + schema: invalidSchema, + }); + expect(result.valid).toBe(false); + expect(result.errors).toBeDefined(); + }); + }); + + describe("extractPaymentIdentifier", () => { + const createMockPayload = (extensions?: Record): PaymentPayload => ({ + x402Version: 2, + resource: { url: "https://example.com/resource", method: "GET" }, + accepted: { + scheme: "exact", + network: "base-sepolia", + asset: "0x...", + amount: "1000000", + payTo: "0x...", + maxTimeoutSeconds: 300, + extra: {}, + }, + payload: {}, + extensions, + }); + + it("should extract ID from valid payload", () => { + const extension = createExtensionWithId("pay_test_id_12345678"); + const payload = createMockPayload({ [PAYMENT_IDENTIFIER]: extension }); + const id = extractPaymentIdentifier(payload); + expect(id).toBe("pay_test_id_12345678"); + }); + + it("should return null when no extensions", () => { + const payload = createMockPayload(); + const id = extractPaymentIdentifier(payload); + expect(id).toBeNull(); + }); + + it("should return null when payment-identifier extension is missing", () => { + const payload = createMockPayload({ other: {} }); + const id = extractPaymentIdentifier(payload); + expect(id).toBeNull(); + }); + + it("should return null for invalid ID when validate=true", () => { + const payload = createMockPayload({ + [PAYMENT_IDENTIFIER]: { info: { required: false, id: "short" } }, + }); + const id = extractPaymentIdentifier(payload, true); + expect(id).toBeNull(); + }); + + it("should return ID for invalid format when validate=false", () => { + const payload = createMockPayload({ + [PAYMENT_IDENTIFIER]: { info: { required: false, id: "short" } }, + }); + const id = extractPaymentIdentifier(payload, false); + expect(id).toBe("short"); + }); + + it("should return null when extension has no id", () => { + const payload = createMockPayload({ + [PAYMENT_IDENTIFIER]: { info: { required: false } }, + }); + const id = extractPaymentIdentifier(payload); + expect(id).toBeNull(); + }); + + it("should return null when extensions is null", () => { + const payload = createMockPayload(); + payload.extensions = null as unknown as Record; + const id = extractPaymentIdentifier(payload); + expect(id).toBeNull(); + }); + + it("should return null when extension exists but is null", () => { + const payload = createMockPayload({ + [PAYMENT_IDENTIFIER]: null, + }); + const id = extractPaymentIdentifier(payload); + expect(id).toBeNull(); + }); + + it("should return null when extension exists but info is null", () => { + const payload = createMockPayload({ + [PAYMENT_IDENTIFIER]: { info: null }, + }); + const id = extractPaymentIdentifier(payload); + expect(id).toBeNull(); + }); + + it("should return null when id exists but is not a string", () => { + const payload = createMockPayload({ + [PAYMENT_IDENTIFIER]: { info: { required: false, id: 123 } }, + }); + const id = extractPaymentIdentifier(payload); + expect(id).toBeNull(); + }); + + it("should return null when id exists but is undefined", () => { + const payload = createMockPayload({ + [PAYMENT_IDENTIFIER]: { info: { required: false, id: undefined } }, + }); + const id = extractPaymentIdentifier(payload); + expect(id).toBeNull(); + }); + }); + + describe("extractAndValidatePaymentIdentifier", () => { + const createMockPayload = (extensions?: Record): PaymentPayload => ({ + x402Version: 2, + resource: { url: "https://example.com/resource", method: "GET" }, + accepted: { + scheme: "exact", + network: "base-sepolia", + asset: "0x...", + amount: "1000000", + payTo: "0x...", + maxTimeoutSeconds: 300, + extra: {}, + }, + payload: {}, + extensions, + }); + + it("should extract and validate a valid extension", () => { + const extension = createExtensionWithId("pay_test_id_12345678"); + const payload = createMockPayload({ [PAYMENT_IDENTIFIER]: extension }); + const { id, validation } = extractAndValidatePaymentIdentifier(payload); + expect(id).toBe("pay_test_id_12345678"); + expect(validation.valid).toBe(true); + }); + + it("should return null id and valid=true when no extensions", () => { + const payload = createMockPayload(); + const { id, validation } = extractAndValidatePaymentIdentifier(payload); + expect(id).toBeNull(); + expect(validation.valid).toBe(true); + }); + + it("should return validation errors for invalid extension", () => { + const payload = createMockPayload({ + [PAYMENT_IDENTIFIER]: { info: { id: "short" } }, + }); + const { id, validation } = extractAndValidatePaymentIdentifier(payload); + expect(id).toBeNull(); + expect(validation.valid).toBe(false); + expect(validation.errors).toBeDefined(); + }); + + it("should return null id but valid=true when no id provided", () => { + const payload = createMockPayload({ + [PAYMENT_IDENTIFIER]: { info: { required: false }, schema: paymentIdentifierSchema }, + }); + const { id, validation } = extractAndValidatePaymentIdentifier(payload); + expect(id).toBeNull(); + expect(validation.valid).toBe(true); + }); + + it("should return validation errors when extension structure is invalid", () => { + const payload = createMockPayload({ + [PAYMENT_IDENTIFIER]: { info: { id: "short" } }, // Missing required + }); + const { id, validation } = extractAndValidatePaymentIdentifier(payload); + expect(id).toBeNull(); + expect(validation.valid).toBe(false); + expect(validation.errors).toBeDefined(); + }); + + it("should return null id when extension exists but is null", () => { + const payload = createMockPayload({ + [PAYMENT_IDENTIFIER]: null, + }); + const { id, validation } = extractAndValidatePaymentIdentifier(payload); + expect(id).toBeNull(); + expect(validation.valid).toBe(true); + }); + }); + + describe("hasPaymentIdentifier", () => { + const createMockPayload = (extensions?: Record): PaymentPayload => ({ + x402Version: 2, + resource: { url: "https://example.com/resource", method: "GET" }, + accepted: { + scheme: "exact", + network: "base-sepolia", + asset: "0x...", + amount: "1000000", + payTo: "0x...", + maxTimeoutSeconds: 300, + extra: {}, + }, + payload: {}, + extensions, + }); + + it("should return true when extension is present", () => { + const extension = createExtensionWithId(); + const payload = createMockPayload({ [PAYMENT_IDENTIFIER]: extension }); + expect(hasPaymentIdentifier(payload)).toBe(true); + }); + + it("should return false when no extensions", () => { + const payload = createMockPayload(); + expect(hasPaymentIdentifier(payload)).toBe(false); + }); + + it("should return false when different extension present", () => { + const payload = createMockPayload({ bazaar: {} }); + expect(hasPaymentIdentifier(payload)).toBe(false); + }); + }); + + describe("isPaymentIdentifierExtension", () => { + it("should return true for valid extension with id", () => { + const extension = createExtensionWithId("pay_test_id_12345678"); + expect(isPaymentIdentifierExtension(extension)).toBe(true); + }); + + it("should return true for valid declaration without id", () => { + const declaration = declarePaymentIdentifierExtension(); + expect(isPaymentIdentifierExtension(declaration)).toBe(true); + }); + + it("should return true for declaration with required=true", () => { + const declaration = declarePaymentIdentifierExtension(true); + expect(isPaymentIdentifierExtension(declaration)).toBe(true); + }); + + it("should return false for null/undefined", () => { + expect(isPaymentIdentifierExtension(null)).toBe(false); + expect(isPaymentIdentifierExtension(undefined)).toBe(false); + }); + + it("should return false for non-object", () => { + expect(isPaymentIdentifierExtension("string")).toBe(false); + expect(isPaymentIdentifierExtension(123)).toBe(false); + }); + + it("should return false for object without info", () => { + expect(isPaymentIdentifierExtension({})).toBe(false); + expect(isPaymentIdentifierExtension({ schema: paymentIdentifierSchema })).toBe(false); + }); + + it("should return false for object with invalid info", () => { + expect(isPaymentIdentifierExtension({ info: null })).toBe(false); + expect(isPaymentIdentifierExtension({ info: "string" })).toBe(false); + }); + + it("should return false for info without required boolean", () => { + expect(isPaymentIdentifierExtension({ info: {} })).toBe(false); + expect(isPaymentIdentifierExtension({ info: { id: "pay_test_id_12345678" } })).toBe(false); + expect(isPaymentIdentifierExtension({ info: { required: "false" } })).toBe(false); + }); + }); + + describe("isPaymentIdentifierRequired", () => { + it("should return true when required is true", () => { + const extension = { info: { required: true } }; + expect(isPaymentIdentifierRequired(extension)).toBe(true); + }); + + it("should return false when required is false", () => { + const extension = { info: { required: false } }; + expect(isPaymentIdentifierRequired(extension)).toBe(false); + }); + + it("should return false for invalid extension", () => { + expect(isPaymentIdentifierRequired(null)).toBe(false); + expect(isPaymentIdentifierRequired(undefined)).toBe(false); + expect(isPaymentIdentifierRequired({})).toBe(false); + expect(isPaymentIdentifierRequired({ info: {} })).toBe(false); + }); + }); + + describe("validatePaymentIdentifierRequirement", () => { + const createMockPayload = (extensions?: Record): PaymentPayload => ({ + x402Version: 2, + resource: { url: "https://example.com/resource", method: "GET" }, + accepted: { + scheme: "exact", + network: "base-sepolia", + asset: "0x...", + amount: "1000000", + payTo: "0x...", + maxTimeoutSeconds: 300, + extra: {}, + }, + payload: {}, + extensions, + }); + + it("should pass when required=false and no id provided", () => { + const payload = createMockPayload(); + const result = validatePaymentIdentifierRequirement(payload, false); + expect(result.valid).toBe(true); + }); + + it("should pass when required=true and valid id provided", () => { + const extension = createExtensionWithId("pay_test_id_12345678", true); + const payload = createMockPayload({ [PAYMENT_IDENTIFIER]: extension }); + const result = validatePaymentIdentifierRequirement(payload, true); + expect(result.valid).toBe(true); + }); + + it("should fail when required=true and no id provided", () => { + const payload = createMockPayload(); + const result = validatePaymentIdentifierRequirement(payload, true); + expect(result.valid).toBe(false); + expect(result.errors).toContain("Server requires a payment identifier but none was provided"); + }); + + it("should fail when required=true and invalid id provided", () => { + const payload = createMockPayload({ + [PAYMENT_IDENTIFIER]: { info: { required: true, id: "short" } }, + }); + const result = validatePaymentIdentifierRequirement(payload, true); + expect(result.valid).toBe(false); + expect(result.errors).toBeDefined(); + }); + + it("should pass when required=false even if invalid id provided", () => { + const payload = createMockPayload({ + [PAYMENT_IDENTIFIER]: { info: { required: false, id: "short" } }, + }); + const result = validatePaymentIdentifierRequirement(payload, false); + expect(result.valid).toBe(true); + }); + + it("should pass when required=true and valid id provided", () => { + const extension = createExtensionWithId("valid_id_12345678", true); + const payload = createMockPayload({ [PAYMENT_IDENTIFIER]: extension }); + const result = validatePaymentIdentifierRequirement(payload, true); + expect(result.valid).toBe(true); + }); + }); + + describe("paymentIdentifierSchema", () => { + it("should have correct JSON Schema draft", () => { + expect(paymentIdentifierSchema.$schema).toBe("https://json-schema.org/draft/2020-12/schema"); + }); + + it("should require the required property", () => { + expect(paymentIdentifierSchema.required).toContain("required"); + }); + + it("should have correct required property definition", () => { + expect(paymentIdentifierSchema.properties.required.type).toBe("boolean"); + }); + + it("should have correct id constraints", () => { + expect(paymentIdentifierSchema.properties.id.type).toBe("string"); + expect(paymentIdentifierSchema.properties.id.minLength).toBe(16); + expect(paymentIdentifierSchema.properties.id.maxLength).toBe(128); + expect(paymentIdentifierSchema.properties.id.pattern).toBe("^[a-zA-Z0-9_-]+$"); + }); + }); + + describe("paymentIdentifierResourceServerExtension", () => { + it("should have correct key", () => { + expect(paymentIdentifierResourceServerExtension.key).toBe(PAYMENT_IDENTIFIER); + }); + + it("should be a valid ResourceServerExtension", () => { + expect(paymentIdentifierResourceServerExtension).toBeDefined(); + expect(typeof paymentIdentifierResourceServerExtension.key).toBe("string"); + }); + }); +}); diff --git a/typescript/packages/extensions/test/sign-in-with-x.test.ts b/typescript/packages/extensions/test/sign-in-with-x.test.ts new file mode 100644 index 0000000..94284fe --- /dev/null +++ b/typescript/packages/extensions/test/sign-in-with-x.test.ts @@ -0,0 +1,1438 @@ +/** + * Tests for Sign-In-With-X Extension + */ + +import { describe, it, expect, vi } from "vitest"; +import { + SIWxPayloadSchema, + parseSIWxHeader, + encodeSIWxHeader, + declareSIWxExtension, + validateSIWxMessage, + createSIWxMessage, + createSIWxPayload, + verifySIWxSignature, + SOLANA_MAINNET, + SOLANA_DEVNET, + formatSIWSMessage, + decodeBase58, + encodeBase58, + extractSolanaChainReference, + verifySolanaSignature, + getEVMAddress, + getSolanaAddress, + signSolanaMessage, + InMemorySIWxStorage, + createSIWxSettleHook, + createSIWxRequestHook, + createSIWxClientHook, + siwxResourceServerExtension, + type SIWxHookEvent, + type SolanaSigner, + type EVMSigner, + type EVMMessageVerifier, + type SIWxExtension, +} from "../src/sign-in-with-x/index"; +import { safeBase64Encode } from "@x402/core/utils"; +import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; +import nacl from "tweetnacl"; +import { randomBytes } from "crypto"; + +/** + * Helper to generate fresh time-based fields for tests. + * In production, these are generated by enrichPaymentRequiredResponse. + * + * @param expirationSeconds - Optional expiration duration in seconds + * @returns Time-based fields for SIWX extension + */ +function generateTimeBasedFields(expirationSeconds?: number) { + const nonce = randomBytes(16).toString("hex"); + const issuedAt = new Date().toISOString(); + const expirationTime = expirationSeconds + ? new Date(Date.now() + expirationSeconds * 1000).toISOString() + : undefined; + return { nonce, issuedAt, expirationTime }; +} + +const validPayload = { + domain: "api.example.com", + address: "0x1234567890123456789012345678901234567890", + statement: "Sign in to access your content", + uri: "https://api.example.com/data", + version: "1", + chainId: "eip155:8453", + type: "eip191" as const, + nonce: "abc123def456", + issuedAt: new Date().toISOString(), + expirationTime: new Date(Date.now() + 5 * 60 * 1000).toISOString(), + resources: ["https://api.example.com/data"], + signature: "0xabcdef1234567890", +}; + +describe("Sign-In-With-X Extension", () => { + describe("SIWxPayloadSchema", () => { + it("should validate a correct payload", () => { + const result = SIWxPayloadSchema.safeParse(validPayload); + expect(result.success).toBe(true); + }); + + it("should reject payload missing required fields", () => { + const invalidPayload = { domain: "example.com" }; + const result = SIWxPayloadSchema.safeParse(invalidPayload); + expect(result.success).toBe(false); + }); + + it("should accept payload with optional fields omitted", () => { + const minimalPayload = { + domain: "api.example.com", + address: "0x1234567890123456789012345678901234567890", + uri: "https://api.example.com", + version: "1", + chainId: "eip155:8453", + type: "eip191" as const, + nonce: "abc123", + issuedAt: new Date().toISOString(), + signature: "0xabcdef", + }; + const result = SIWxPayloadSchema.safeParse(minimalPayload); + expect(result.success).toBe(true); + }); + }); + + describe("parseSIWxHeader", () => { + it("should parse base64-encoded header", () => { + const encoded = safeBase64Encode(JSON.stringify(validPayload)); + const parsed = parseSIWxHeader(encoded); + expect(parsed.domain).toBe(validPayload.domain); + expect(parsed.address).toBe(validPayload.address); + expect(parsed.signature).toBe(validPayload.signature); + }); + + it("should throw on invalid base64", () => { + expect(() => parseSIWxHeader("not-valid-base64!@#")).toThrow("not valid base64"); + }); + + it("should throw on invalid JSON in base64", () => { + const invalidJson = safeBase64Encode("not valid json"); + expect(() => parseSIWxHeader(invalidJson)).toThrow("not valid JSON"); + }); + + it("should throw on missing required fields", () => { + const incomplete = safeBase64Encode(JSON.stringify({ domain: "example.com" })); + expect(() => parseSIWxHeader(incomplete)).toThrow("Invalid SIWX header"); + }); + }); + + describe("encodeSIWxHeader", () => { + it("should encode payload as base64 and round-trip correctly", () => { + const encoded = encodeSIWxHeader(validPayload); + const decoded = parseSIWxHeader(encoded); + expect(decoded.domain).toBe(validPayload.domain); + expect(decoded.address).toBe(validPayload.address); + expect(decoded.signature).toBe(validPayload.signature); + }); + }); + + describe("declareSIWxExtension", () => { + it("should create extension with supportedChains array (without time-based fields)", () => { + const result = declareSIWxExtension({ + domain: "api.example.com", + resourceUri: "https://api.example.com/data", + network: "eip155:8453", + statement: "Sign in to access", + expirationSeconds: 300, + }); + + expect(result).toHaveProperty("sign-in-with-x"); + const extension = result["sign-in-with-x"]; + expect(extension.info.domain).toBe("api.example.com"); + expect(extension.info.uri).toBe("https://api.example.com/data"); + expect(extension.schema).toBeDefined(); + + // Time-based fields are NOT generated by declareSIWxExtension + // They are generated per-request by enrichPaymentRequiredResponse + expect(extension.info.nonce).toBeUndefined(); + expect(extension.info.issuedAt).toBeUndefined(); + expect(extension.info.expirationTime).toBeUndefined(); + + // Check supportedChains array + expect(extension.supportedChains).toHaveLength(1); + expect(extension.supportedChains[0].chainId).toBe("eip155:8453"); + expect(extension.supportedChains[0].type).toBe("eip191"); + + // Options are stored for enrichPaymentRequiredResponse + expect(extension._options.expirationSeconds).toBe(300); + }); + + it("should support multiple chains in single extension", () => { + const result = declareSIWxExtension({ + domain: "api.example.com", + resourceUri: "https://api.example.com/data", + network: ["eip155:8453", SOLANA_DEVNET], + expirationSeconds: 300, + }); + + const extension = result["sign-in-with-x"]; + expect(extension.supportedChains).toHaveLength(2); + expect(extension.supportedChains[0].chainId).toBe("eip155:8453"); + expect(extension.supportedChains[0].type).toBe("eip191"); + expect(extension.supportedChains[1].chainId).toBe(SOLANA_DEVNET); + expect(extension.supportedChains[1].type).toBe("ed25519"); + + // Time-based fields are NOT generated - only _options are stored + expect(extension.info.nonce).toBeUndefined(); + expect(extension._options.expirationSeconds).toBe(300); + }); + + it("should support infinite expiration", () => { + const result = declareSIWxExtension({ + domain: "api.example.com", + resourceUri: "https://api.example.com/data", + network: "eip155:8453", + expirationSeconds: undefined, + }); + + const extension = result["sign-in-with-x"]; + expect(extension.info.expirationTime).toBeUndefined(); + }); + }); + + describe("validateSIWxMessage", () => { + it("should validate correct message", async () => { + const now = new Date(); + const payload = { + ...validPayload, + issuedAt: now.toISOString(), + expirationTime: new Date(now.getTime() + 5 * 60 * 1000).toISOString(), + }; + + const result = await validateSIWxMessage(payload, "https://api.example.com/data"); + expect(result.valid).toBe(true); + }); + + it("should reject domain mismatch", async () => { + const result = await validateSIWxMessage(validPayload, "https://different.example.com/data"); + expect(result.valid).toBe(false); + expect(result.error).toContain("Domain mismatch"); + }); + }); + + describe("createSIWxMessage", () => { + it("should create EIP-4361 format message", () => { + const serverInfo = { + domain: "api.example.com", + uri: "https://api.example.com", + statement: "Sign in to access", + version: "1", + chainId: "eip155:8453", + type: "eip191" as const, + nonce: "abc12345def67890", + issuedAt: "2024-01-01T00:00:00.000Z", + resources: ["https://api.example.com"], + }; + + const message = createSIWxMessage(serverInfo, "0x1234567890123456789012345678901234567890"); + + expect(message).toContain("api.example.com wants you to sign in"); + expect(message).toContain("0x1234567890123456789012345678901234567890"); + expect(message).toContain("Nonce: abc12345def67890"); + expect(message).toContain("Chain ID: 8453"); + }); + }); + + describe("Integration - encode/parse roundtrip", () => { + it("should roundtrip through encode and parse", () => { + const encoded = encodeSIWxHeader(validPayload); + const parsed = parseSIWxHeader(encoded); + + expect(parsed.domain).toBe(validPayload.domain); + expect(parsed.address).toBe(validPayload.address); + expect(parsed.signature).toBe(validPayload.signature); + }); + }); + + describe("Integration - full signing and verification", () => { + it("should sign and verify a message with a real wallet", async () => { + const account = privateKeyToAccount(generatePrivateKey()); + + const extension = declareSIWxExtension({ + domain: "api.example.com", + resourceUri: "https://api.example.com/resource", + network: "eip155:8453", + statement: "Sign in to access your content", + }); + + const ext = extension["sign-in-with-x"]; + // Add time-based fields (in production, enrichPaymentRequiredResponse does this) + const completeInfo = { + ...ext.info, + ...generateTimeBasedFields(300), + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + const header = encodeSIWxHeader(payload); + const parsed = parseSIWxHeader(header); + + const validation = await validateSIWxMessage(parsed, "https://api.example.com/resource"); + expect(validation.valid).toBe(true); + + const verification = await verifySIWxSignature(parsed); + expect(verification.valid).toBe(true); + expect(verification.address?.toLowerCase()).toBe(account.address.toLowerCase()); + }); + + it("should reject tampered signature", async () => { + const account = privateKeyToAccount(generatePrivateKey()); + + const extension = declareSIWxExtension({ + domain: "api.example.com", + resourceUri: "https://api.example.com/resource", + network: "eip155:8453", + }); + + const ext = extension["sign-in-with-x"]; + // Add time-based fields (in production, enrichPaymentRequiredResponse does this) + const completeInfo = { + ...ext.info, + ...generateTimeBasedFields(300), + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + payload.signature = "0x" + "00".repeat(65); // Invalid signature + + const verification = await verifySIWxSignature(payload); + expect(verification.valid).toBe(false); + }); + }); + + describe("Smart wallet verification (evmVerifier option)", () => { + it("should use provided verifier for EVM signatures", async () => { + const mockVerifier: EVMMessageVerifier = vi.fn().mockResolvedValue(true); + const account = privateKeyToAccount(generatePrivateKey()); + + const extension = declareSIWxExtension({ + domain: "api.example.com", + resourceUri: "https://api.example.com/resource", + network: "eip155:8453", + }); + + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + ...generateTimeBasedFields(300), + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + + const result = await verifySIWxSignature(payload, { + evmVerifier: mockVerifier, + }); + + expect(mockVerifier).toHaveBeenCalledOnce(); + expect(mockVerifier).toHaveBeenCalledWith({ + address: expect.any(String), + message: expect.any(String), + signature: expect.any(String), + }); + expect(result.valid).toBe(true); + }); + + it("should fallback to EOA verification when no verifier provided", async () => { + const account = privateKeyToAccount(generatePrivateKey()); + + const extension = declareSIWxExtension({ + domain: "api.example.com", + resourceUri: "https://api.example.com/resource", + network: "eip155:8453", + }); + + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + ...generateTimeBasedFields(300), + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + + // No verifier - should still work for EOA + const result = await verifySIWxSignature(payload); + expect(result.valid).toBe(true); + expect(result.address?.toLowerCase()).toBe(account.address.toLowerCase()); + }); + + it("should return error when verifier returns false", async () => { + const mockVerifier: EVMMessageVerifier = vi.fn().mockResolvedValue(false); + const account = privateKeyToAccount(generatePrivateKey()); + + const extension = declareSIWxExtension({ + domain: "api.example.com", + resourceUri: "https://api.example.com/resource", + network: "eip155:8453", + }); + + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + ...generateTimeBasedFields(300), + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + + const result = await verifySIWxSignature(payload, { + evmVerifier: mockVerifier, + }); + + expect(result.valid).toBe(false); + expect(result.error).toContain("Signature verification failed"); + }); + + it("should return error when verifier throws", async () => { + const mockVerifier: EVMMessageVerifier = vi.fn().mockRejectedValue(new Error("RPC error")); + const account = privateKeyToAccount(generatePrivateKey()); + + const extension = declareSIWxExtension({ + domain: "api.example.com", + resourceUri: "https://api.example.com/resource", + network: "eip155:8453", + }); + + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + ...generateTimeBasedFields(300), + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + + const result = await verifySIWxSignature(payload, { + evmVerifier: mockVerifier, + }); + + expect(result.valid).toBe(false); + expect(result.error).toContain("RPC error"); + }); + + it("should not use verifier for Solana signatures", async () => { + const mockVerifier: EVMMessageVerifier = vi.fn(); + const keypair = nacl.sign.keyPair(); + const address = encodeBase58(keypair.publicKey); + + const solanaSigner: SolanaSigner = { + signMessage: async (msg: Uint8Array) => nacl.sign.detached(msg, keypair.secretKey), + publicKey: address, + }; + + const extension = declareSIWxExtension({ + domain: "api.example.com", + resourceUri: "https://api.example.com/resource", + network: SOLANA_MAINNET, + }); + + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + ...generateTimeBasedFields(300), + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, solanaSigner); + + const result = await verifySIWxSignature(payload, { + evmVerifier: mockVerifier, + }); + + // Verifier should NOT be called for Solana + expect(mockVerifier).not.toHaveBeenCalled(); + expect(result.valid).toBe(true); + expect(result.address).toBe(address); + }); + }); + + describe("Solana constants", () => { + it("should export Solana network constants", () => { + expect(SOLANA_MAINNET).toBe("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"); + expect(SOLANA_DEVNET).toBe("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"); + }); + }); + + describe("Base58 encoding/decoding", () => { + it("should roundtrip encode/decode", () => { + const original = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const encoded = encodeBase58(original); + const decoded = decodeBase58(encoded); + expect(decoded).toEqual(original); + }); + + it("should handle leading zeros", () => { + const withLeadingZeros = new Uint8Array([0, 0, 1, 2, 3]); + const encoded = encodeBase58(withLeadingZeros); + const decoded = decodeBase58(encoded); + expect(decoded).toEqual(withLeadingZeros); + }); + + it("should decode known Solana addresses", () => { + // This is a valid 32-byte Solana public key + const address = "11111111111111111111111111111111"; + const decoded = decodeBase58(address); + expect(decoded.length).toBe(32); + }); + + it("should throw on invalid Base58 characters", () => { + expect(() => decodeBase58("invalid0OIl")).toThrow("Unknown letter"); + }); + }); + + describe("extractSolanaChainReference", () => { + it("should extract mainnet reference", () => { + expect(extractSolanaChainReference(SOLANA_MAINNET)).toBe("5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"); + }); + + it("should extract devnet reference", () => { + expect(extractSolanaChainReference(SOLANA_DEVNET)).toBe("EtWTRABZaYq6iMfeYKouRu166VU2xqa1"); + }); + + it("should return reference for custom networks", () => { + expect(extractSolanaChainReference("solana:customnetwork123")).toBe("customnetwork123"); + }); + }); + + describe("formatSIWSMessage", () => { + it("should format SIWS message correctly", () => { + const info = { + domain: "api.example.com", + uri: "https://api.example.com/data", + statement: "Sign in to access", + version: "1", + chainId: SOLANA_MAINNET, + type: "ed25519" as const, + nonce: "abc123", + issuedAt: "2024-01-01T00:00:00.000Z", + resources: ["https://api.example.com/data"], + }; + + const message = formatSIWSMessage(info, "BSmWDgE9ex6dZYbiTsJGcwMEgFp8q4aWh92hdErQPeVW"); + + expect(message).toContain("wants you to sign in with your Solana account:"); + expect(message).toContain("BSmWDgE9ex6dZYbiTsJGcwMEgFp8q4aWh92hdErQPeVW"); + expect(message).toContain("Chain ID: 5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"); + expect(message).toContain("Nonce: abc123"); + expect(message).toContain("Sign in to access"); + }); + + it("should handle message without statement", () => { + const info = { + domain: "api.example.com", + uri: "https://api.example.com", + version: "1", + chainId: SOLANA_DEVNET, + type: "ed25519" as const, + nonce: "xyz789", + issuedAt: "2024-01-01T00:00:00.000Z", + }; + + const message = formatSIWSMessage(info, "TestAddress123"); + + expect(message).toContain("wants you to sign in with your Solana account:"); + expect(message).toContain("Chain ID: EtWTRABZaYq6iMfeYKouRu166VU2xqa1"); + expect(message).not.toContain("Sign in to access"); + }); + }); + + describe("createSIWxMessage - chain routing", () => { + it("should route EVM chains to SIWE format", () => { + const info = { + domain: "api.example.com", + uri: "https://api.example.com", + version: "1", + chainId: "eip155:1", + type: "eip191" as const, + nonce: "abc12345678", + issuedAt: "2024-01-01T00:00:00.000Z", + }; + + const message = createSIWxMessage(info, "0x1234567890123456789012345678901234567890"); + + expect(message).toContain("wants you to sign in with your Ethereum account:"); + expect(message).toContain("Chain ID: 1"); + }); + + it("should route Solana chains to SIWS format", () => { + const info = { + domain: "api.example.com", + uri: "https://api.example.com", + version: "1", + chainId: SOLANA_MAINNET, + type: "ed25519" as const, + nonce: "abc12345678", + issuedAt: "2024-01-01T00:00:00.000Z", + }; + + const message = createSIWxMessage(info, "BSmWDgE9ex6dZYbiTsJGcwMEgFp8q4aWh92hdErQPeVW"); + + expect(message).toContain("wants you to sign in with your Solana account:"); + expect(message).toContain("Chain ID: 5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"); + }); + + it("should throw for unsupported chain namespaces", () => { + const info = { + domain: "api.example.com", + uri: "https://api.example.com", + version: "1", + chainId: "cosmos:cosmoshub-4", + type: "eip191" as const, + nonce: "abc12345678", + issuedAt: "2024-01-01T00:00:00.000Z", + }; + + expect(() => createSIWxMessage(info, "cosmos1...")).toThrow("Unsupported chain namespace"); + }); + }); + + describe("Solana signature verification", () => { + it("should verify valid Ed25519 signature", () => { + // Generate a test keypair + const keypair = nacl.sign.keyPair(); + const message = "Test message for signing"; + const messageBytes = new TextEncoder().encode(message); + const signature = nacl.sign.detached(messageBytes, keypair.secretKey); + + const valid = verifySolanaSignature(message, signature, keypair.publicKey); + expect(valid).toBe(true); + }); + + it("should reject invalid signature", () => { + const keypair = nacl.sign.keyPair(); + const message = "Test message"; + const wrongSignature = new Uint8Array(64).fill(0); + + const valid = verifySolanaSignature(message, wrongSignature, keypair.publicKey); + expect(valid).toBe(false); + }); + + it("should reject signature from different key", () => { + const keypair1 = nacl.sign.keyPair(); + const keypair2 = nacl.sign.keyPair(); + const message = "Test message"; + const messageBytes = new TextEncoder().encode(message); + const signature = nacl.sign.detached(messageBytes, keypair1.secretKey); + + // Verify with different public key + const valid = verifySolanaSignature(message, signature, keypair2.publicKey); + expect(valid).toBe(false); + }); + }); + + describe("verifySIWxSignature - chain routing", () => { + it("should reject unsupported chain namespace", async () => { + const payload = { + ...validPayload, + chainId: "cosmos:cosmoshub-4", + type: "eip191" as const, + }; + + const result = await verifySIWxSignature(payload); + expect(result.valid).toBe(false); + expect(result.error).toContain("Unsupported chain namespace"); + }); + + it("should verify Solana signatures", async () => { + // Generate Solana keypair + const keypair = nacl.sign.keyPair(); + const address = encodeBase58(keypair.publicKey); + + const serverInfo = { + domain: "api.example.com", + uri: "https://api.example.com/data", + version: "1", + chainId: SOLANA_MAINNET, + type: "ed25519" as const, + nonce: "test123", + issuedAt: new Date().toISOString(), + }; + + // Create and sign SIWS message + const message = formatSIWSMessage(serverInfo, address); + const messageBytes = new TextEncoder().encode(message); + const signatureBytes = nacl.sign.detached(messageBytes, keypair.secretKey); + const signature = encodeBase58(signatureBytes); + + const payload = { + ...serverInfo, + address, + signature, + }; + + const result = await verifySIWxSignature(payload); + expect(result.valid).toBe(true); + expect(result.address).toBe(address); + }); + + it("should reject invalid Solana signature length", async () => { + const payload = { + domain: "api.example.com", + uri: "https://api.example.com", + version: "1", + chainId: SOLANA_MAINNET, + type: "ed25519" as const, + nonce: "test123", + issuedAt: new Date().toISOString(), + address: encodeBase58(new Uint8Array(32).fill(1)), // Valid 32-byte key + signature: encodeBase58(new Uint8Array(32).fill(0)), // Invalid 32-byte sig (should be 64) + }; + + const result = await verifySIWxSignature(payload); + expect(result.valid).toBe(false); + expect(result.error).toContain("Invalid signature length"); + }); + }); + + describe("Solana client-side signing", () => { + describe("getSolanaAddress", () => { + it("should get address from string publicKey", () => { + const signer: SolanaSigner = { + signMessage: async () => new Uint8Array(64), + publicKey: "BSmWDgE9ex6dZYbiTsJGcwMEgFp8q4aWh92hdErQPeVW", + }; + expect(getSolanaAddress(signer)).toBe("BSmWDgE9ex6dZYbiTsJGcwMEgFp8q4aWh92hdErQPeVW"); + }); + + it("should get address from PublicKey object", () => { + const signer: SolanaSigner = { + signMessage: async () => new Uint8Array(64), + publicKey: { toBase58: () => "BSmWDgE9ex6dZYbiTsJGcwMEgFp8q4aWh92hdErQPeVW" }, + }; + expect(getSolanaAddress(signer)).toBe("BSmWDgE9ex6dZYbiTsJGcwMEgFp8q4aWh92hdErQPeVW"); + }); + }); + + describe("getEVMAddress", () => { + it("should get address from account property", () => { + const signer: EVMSigner = { + signMessage: async () => "0x...", + account: { address: "0x1234567890123456789012345678901234567890" }, + }; + expect(getEVMAddress(signer)).toBe("0x1234567890123456789012345678901234567890"); + }); + + it("should get address from direct address property", () => { + const signer: EVMSigner = { + signMessage: async () => "0x...", + address: "0xabcdef1234567890123456789012345678901234", + }; + expect(getEVMAddress(signer)).toBe("0xabcdef1234567890123456789012345678901234"); + }); + + it("should throw for signer without address", () => { + const signer: EVMSigner = { + signMessage: async () => "0x...", + }; + expect(() => getEVMAddress(signer)).toThrow("EVM signer missing address"); + }); + }); + + describe("signSolanaMessage", () => { + it("should sign and return Base58 encoded signature", async () => { + const keypair = nacl.sign.keyPair(); + + const solanaSigner: SolanaSigner = { + signMessage: async (msg: Uint8Array) => nacl.sign.detached(msg, keypair.secretKey), + publicKey: encodeBase58(keypair.publicKey), + }; + + const message = "Test message for Solana signing"; + const signature = await signSolanaMessage(message, solanaSigner); + + // Signature should be Base58 encoded + const decoded = decodeBase58(signature); + expect(decoded.length).toBe(64); // Ed25519 signature + + // Verify the signature works + const valid = verifySolanaSignature(message, decoded, keypair.publicKey); + expect(valid).toBe(true); + }); + }); + + describe("createSIWxPayload with Solana signer", () => { + it("should create valid payload with Solana signer", async () => { + const keypair = nacl.sign.keyPair(); + const address = encodeBase58(keypair.publicKey); + + const solanaSigner: SolanaSigner = { + signMessage: async (msg: Uint8Array) => nacl.sign.detached(msg, keypair.secretKey), + publicKey: address, + }; + + const serverInfo = { + domain: "api.example.com", + uri: "https://api.example.com/data", + version: "1", + chainId: SOLANA_MAINNET, + type: "ed25519" as const, + nonce: "test123456789", + issuedAt: new Date().toISOString(), + }; + + const payload = await createSIWxPayload(serverInfo, solanaSigner); + + expect(payload.address).toBe(address); + expect(payload.chainId).toBe(SOLANA_MAINNET); + + // Verify the signature is valid + const result = await verifySIWxSignature(payload); + expect(result.valid).toBe(true); + expect(result.address).toBe(address); + }); + + it("should roundtrip through encode/parse/verify with Solana", async () => { + const keypair = nacl.sign.keyPair(); + const address = encodeBase58(keypair.publicKey); + + const solanaSigner: SolanaSigner = { + signMessage: async (msg: Uint8Array) => nacl.sign.detached(msg, keypair.secretKey), + publicKey: address, + }; + + const extension = declareSIWxExtension({ + domain: "api.example.com", + resourceUri: "https://api.example.com/resource", + network: SOLANA_MAINNET, + statement: "Sign in to access", + }); + + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + ...generateTimeBasedFields(300), + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, solanaSigner); + const header = encodeSIWxHeader(payload); + const parsed = parseSIWxHeader(header); + + const validation = await validateSIWxMessage(parsed, "https://api.example.com/resource"); + expect(validation.valid).toBe(true); + + const verification = await verifySIWxSignature(parsed); + expect(verification.valid).toBe(true); + expect(verification.address).toBe(address); + }); + + it("should work with PublicKey object style signer", async () => { + const keypair = nacl.sign.keyPair(); + const address = encodeBase58(keypair.publicKey); + + // Mimic @solana/wallet-adapter style + const solanaSigner: SolanaSigner = { + signMessage: async (msg: Uint8Array) => nacl.sign.detached(msg, keypair.secretKey), + publicKey: { toBase58: () => address }, + }; + + const extension = declareSIWxExtension({ + domain: "api.example.com", + resourceUri: "https://api.example.com/resource", + network: SOLANA_DEVNET, + }); + + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + ...generateTimeBasedFields(300), + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, solanaSigner); + + expect(payload.address).toBe(address); + expect(payload.chainId).toBe(SOLANA_DEVNET); + + const verification = await verifySIWxSignature(payload); + expect(verification.valid).toBe(true); + }); + }); + + describe("signatureScheme behavior", () => { + it("verification ignores signatureScheme and uses chainId", async () => { + // This test documents that signatureScheme is a hint only + const keypair = nacl.sign.keyPair(); + const address = encodeBase58(keypair.publicKey); + + const serverInfo = { + domain: "api.example.com", + uri: "https://api.example.com", + version: "1", + chainId: SOLANA_MAINNET, + type: "ed25519" as const, + nonce: "test12345", + issuedAt: new Date().toISOString(), + signatureScheme: "eip191" as const, // Wrong hint - should be "siws" + }; + + // Create message and sign + const message = formatSIWSMessage(serverInfo, address); + const messageBytes = new TextEncoder().encode(message); + const signatureBytes = nacl.sign.detached(messageBytes, keypair.secretKey); + + const payload = { + ...serverInfo, + address, + signature: encodeBase58(signatureBytes), + signatureScheme: "eip191" as const, // Wrong hint + }; + + // Verification should still work because it uses chainId, not signatureScheme + const result = await verifySIWxSignature(payload); + expect(result.valid).toBe(true); // Proves signatureScheme is ignored + }); + }); + }); +}); + +describe("SIWxStorage", () => { + describe("InMemorySIWxStorage", () => { + it("should record and check payments", () => { + const storage = new InMemorySIWxStorage(); + + expect(storage.hasPaid("/resource", "0xABC")).toBe(false); + + storage.recordPayment("/resource", "0xABC"); + expect(storage.hasPaid("/resource", "0xABC")).toBe(true); + expect(storage.hasPaid("/resource", "0xDEF")).toBe(false); + expect(storage.hasPaid("/other", "0xABC")).toBe(false); + }); + + it("should normalize addresses to lowercase", () => { + const storage = new InMemorySIWxStorage(); + + storage.recordPayment("/resource", "0xABCDEF"); + expect(storage.hasPaid("/resource", "0xabcdef")).toBe(true); + expect(storage.hasPaid("/resource", "0xABCDEF")).toBe(true); + }); + + it("should handle multiple resources independently", () => { + const storage = new InMemorySIWxStorage(); + + storage.recordPayment("/a", "0x1"); + storage.recordPayment("/b", "0x2"); + + expect(storage.hasPaid("/a", "0x1")).toBe(true); + expect(storage.hasPaid("/a", "0x2")).toBe(false); + expect(storage.hasPaid("/b", "0x1")).toBe(false); + expect(storage.hasPaid("/b", "0x2")).toBe(true); + }); + }); +}); + +describe("SIWX Hooks", () => { + describe("createSIWxSettleHook", () => { + it("should record payment using result.payer (EVM flow)", async () => { + const storage = new InMemorySIWxStorage(); + const hook = createSIWxSettleHook({ storage }); + + // Payer comes from facilitator result, not extracted from payload + await hook({ + paymentPayload: { + payload: { authorization: { from: "0xABC123" } }, + resource: { url: "http://example.com/weather" }, + }, + result: { success: true, payer: "0xABC123" }, + }); + + expect(storage.hasPaid("/weather", "0xABC123")).toBe(true); + }); + + it("should record payment using result.payer (SVM flow)", async () => { + const storage = new InMemorySIWxStorage(); + const hook = createSIWxSettleHook({ storage }); + + // SVM payload is just { transaction: string }, payer comes from facilitator result + await hook({ + paymentPayload: { + payload: { transaction: "base64EncodedTransaction" }, + resource: { url: "http://example.com/data" }, + }, + result: { success: true, payer: "SolanaAddress123" }, + }); + + expect(storage.hasPaid("/data", "SolanaAddress123")).toBe(true); + }); + + it("should call onEvent when payment is recorded", async () => { + const storage = new InMemorySIWxStorage(); + const events: unknown[] = []; + const hook = createSIWxSettleHook({ + storage, + onEvent: e => events.push(e), + }); + + await hook({ + paymentPayload: { + payload: { authorization: { from: "0x123" } }, + resource: { url: "http://example.com/test" }, + }, + result: { success: true, payer: "0x123" }, + }); + + expect(events).toHaveLength(1); + expect(events[0]).toEqual({ + type: "payment_recorded", + resource: "/test", + address: "0x123", + }); + }); + + it("should not record if result.payer is undefined", async () => { + const storage = new InMemorySIWxStorage(); + const hook = createSIWxSettleHook({ storage }); + + // When facilitator doesn't return payer (e.g., older facilitator version) + await hook({ + paymentPayload: { + payload: { transaction: "someTransaction" }, + resource: { url: "http://example.com/test" }, + }, + result: { success: true }, + }); + + // No exception, just silently skips since no payer available + expect(storage.hasPaid("/test", "anything")).toBe(false); + }); + + it("should NOT record payment if settlement failed", async () => { + const storage = new InMemorySIWxStorage(); + const hook = createSIWxSettleHook({ storage }); + + // Even if payer is provided, don't record on failed settlement + await hook({ + paymentPayload: { + payload: { authorization: { from: "0xABC123" } }, + resource: { url: "http://example.com/weather" }, + }, + result: { success: false, payer: "0xABC123" }, + }); + + // Payment should NOT be recorded when settlement fails + expect(storage.hasPaid("/weather", "0xABC123")).toBe(false); + }); + }); + + describe("createSIWxRequestHook", () => { + it("should return undefined when no SIWX header", async () => { + const storage = new InMemorySIWxStorage(); + const hook = createSIWxRequestHook({ storage }); + + const result = await hook({ + adapter: { + getHeader: () => undefined, + getUrl: () => "http://example.com/test", + }, + path: "/test", + }); + + expect(result).toBeUndefined(); + }); + + it("should grant access when address has paid", async () => { + const storage = new InMemorySIWxStorage(); + const account = privateKeyToAccount(generatePrivateKey()); + + // Pre-record payment + storage.recordPayment("/resource", account.address); + + // Create valid SIWX header + const extension = declareSIWxExtension({ + domain: "example.com", + resourceUri: "http://example.com/resource", + network: "eip155:8453", + }); + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + ...generateTimeBasedFields(300), + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + const header = encodeSIWxHeader(payload); + + const hook = createSIWxRequestHook({ storage }); + const result = await hook({ + adapter: { + getHeader: (name: string) => + name === "sign-in-with-x" || name === "SIGN-IN-WITH-X" ? header : undefined, + getUrl: () => "http://example.com/resource", + }, + path: "/resource", + }); + + expect(result).toEqual({ grantAccess: true }); + }); + + it("should return undefined when address has not paid", async () => { + const storage = new InMemorySIWxStorage(); + const account = privateKeyToAccount(generatePrivateKey()); + + // Don't pre-record payment + + const extension = declareSIWxExtension({ + domain: "example.com", + resourceUri: "http://example.com/resource", + network: "eip155:8453", + }); + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + ...generateTimeBasedFields(300), + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + const header = encodeSIWxHeader(payload); + + const hook = createSIWxRequestHook({ storage }); + const result = await hook({ + adapter: { + getHeader: (name: string) => (name === "sign-in-with-x" ? header : undefined), + getUrl: () => "http://example.com/resource", + }, + path: "/resource", + }); + + expect(result).toBeUndefined(); + }); + + it("should emit validation_failed event on invalid signature", async () => { + const storage = new InMemorySIWxStorage(); + const events: unknown[] = []; + const hook = createSIWxRequestHook({ + storage, + onEvent: e => events.push(e), + }); + + // Create invalid header (valid base64/json but bad signature) + const invalidPayload = { + domain: "example.com", + address: "0x1234567890123456789012345678901234567890", + uri: "http://example.com/resource", + version: "1", + chainId: "eip155:8453", + type: "eip191", + nonce: "test123", + issuedAt: new Date().toISOString(), + signature: "0x" + "00".repeat(65), + }; + const header = safeBase64Encode(JSON.stringify(invalidPayload)); + + await hook({ + adapter: { + getHeader: (name: string) => (name === "sign-in-with-x" ? header : undefined), + getUrl: () => "http://example.com/resource", + }, + path: "/resource", + }); + + expect(events.some((e: SIWxHookEvent) => e.type === "validation_failed")).toBe(true); + }); + + describe("nonce tracking", () => { + it("should throw if only hasUsedNonce is implemented", () => { + const storage = new InMemorySIWxStorage(); + const partialStorage = { + ...storage, + hasPaid: storage.hasPaid.bind(storage), + recordPayment: storage.recordPayment.bind(storage), + hasUsedNonce: () => false, + // recordNonce intentionally missing + }; + + expect(() => createSIWxRequestHook({ storage: partialStorage })).toThrow( + "SIWxStorage nonce tracking requires both hasUsedNonce and recordNonce to be implemented", + ); + }); + + it("should throw if only recordNonce is implemented", () => { + const storage = new InMemorySIWxStorage(); + const partialStorage = { + ...storage, + hasPaid: storage.hasPaid.bind(storage), + recordPayment: storage.recordPayment.bind(storage), + // hasUsedNonce intentionally missing + recordNonce: () => {}, + }; + + expect(() => createSIWxRequestHook({ storage: partialStorage })).toThrow( + "SIWxStorage nonce tracking requires both hasUsedNonce and recordNonce to be implemented", + ); + }); + + /** + * Creates a storage implementation with nonce tracking for testing. + * + * @returns Storage with hasUsedNonce/recordNonce methods and exposed _usedNonces set + */ + function createNonceTrackingStorage() { + const storage = new InMemorySIWxStorage(); + const usedNonces = new Set(); + return { + ...storage, + hasPaid: storage.hasPaid.bind(storage), + recordPayment: storage.recordPayment.bind(storage), + hasUsedNonce: (nonce: string) => usedNonces.has(nonce), + recordNonce: (nonce: string) => { + usedNonces.add(nonce); + }, + // Expose for test assertions + _usedNonces: usedNonces, + }; + } + + it("should reject access when nonce is already used", async () => { + const storage = createNonceTrackingStorage(); + const account = privateKeyToAccount(generatePrivateKey()); + const events: SIWxHookEvent[] = []; + + // Pre-record payment + storage.recordPayment("/resource", account.address); + + // Create valid SIWX header + const extension = declareSIWxExtension({ + domain: "example.com", + resourceUri: "http://example.com/resource", + network: "eip155:8453", + }); + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + ...generateTimeBasedFields(300), + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + const header = encodeSIWxHeader(payload); + + // Mark nonce as already used + storage.recordNonce(payload.nonce); + + const hook = createSIWxRequestHook({ storage, onEvent: e => events.push(e) }); + const result = await hook({ + adapter: { + getHeader: (name: string) => + name === "sign-in-with-x" || name === "SIGN-IN-WITH-X" ? header : undefined, + getUrl: () => "http://example.com/resource", + }, + path: "/resource", + }); + + // Should reject even though address has paid + expect(result).toBeUndefined(); + expect(events.some(e => e.type === "nonce_reused")).toBe(true); + }); + + it("should record nonce when granting access", async () => { + const storage = createNonceTrackingStorage(); + const account = privateKeyToAccount(generatePrivateKey()); + + // Pre-record payment + storage.recordPayment("/resource", account.address); + + // Create valid SIWX header + const extension = declareSIWxExtension({ + domain: "example.com", + resourceUri: "http://example.com/resource", + network: "eip155:8453", + }); + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + ...generateTimeBasedFields(300), + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + const header = encodeSIWxHeader(payload); + + const hook = createSIWxRequestHook({ storage }); + const result = await hook({ + adapter: { + getHeader: (name: string) => + name === "sign-in-with-x" || name === "SIGN-IN-WITH-X" ? header : undefined, + getUrl: () => "http://example.com/resource", + }, + path: "/resource", + }); + + // Should grant access + expect(result).toEqual({ grantAccess: true }); + // Nonce should be recorded + expect(storage._usedNonces.has(payload.nonce)).toBe(true); + }); + + it("should work without nonce tracking (InMemorySIWxStorage)", async () => { + // This tests that the hook works when storage doesn't implement nonce methods + const storage = new InMemorySIWxStorage(); + const account = privateKeyToAccount(generatePrivateKey()); + + // Pre-record payment + storage.recordPayment("/resource", account.address); + + // Create valid SIWX header + const extension = declareSIWxExtension({ + domain: "example.com", + resourceUri: "http://example.com/resource", + network: "eip155:8453", + }); + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + ...generateTimeBasedFields(300), + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + const header = encodeSIWxHeader(payload); + + const hook = createSIWxRequestHook({ storage }); + + // First request should succeed + const result1 = await hook({ + adapter: { + getHeader: (name: string) => + name === "sign-in-with-x" || name === "SIGN-IN-WITH-X" ? header : undefined, + getUrl: () => "http://example.com/resource", + }, + path: "/resource", + }); + expect(result1).toEqual({ grantAccess: true }); + + // Second request with same header should also succeed (no nonce tracking) + const result2 = await hook({ + adapter: { + getHeader: (name: string) => + name === "sign-in-with-x" || name === "SIGN-IN-WITH-X" ? header : undefined, + getUrl: () => "http://example.com/resource", + }, + path: "/resource", + }); + expect(result2).toEqual({ grantAccess: true }); + }); + }); + }); + + describe("createSIWxClientHook", () => { + it("should return undefined when no SIWX extension", async () => { + const account = privateKeyToAccount(generatePrivateKey()); + const hook = createSIWxClientHook(account); + + const result = await hook({ + paymentRequired: { extensions: {} }, + }); + + expect(result).toBeUndefined(); + }); + + it("should return headers when SIWX extension present", async () => { + const account = privateKeyToAccount(generatePrivateKey()); + const hook = createSIWxClientHook(account); + + const declaration = declareSIWxExtension({ + domain: "example.com", + resourceUri: "http://example.com/resource", + network: "eip155:1", + }); + + // Simulate what enrichPaymentRequiredResponse does: add time-based fields + const ext = declaration["sign-in-with-x"]; + const enrichedExtension = { + "sign-in-with-x": { + info: { + ...ext.info, + ...generateTimeBasedFields(300), + }, + supportedChains: ext.supportedChains, + schema: ext.schema, + }, + }; + + const result = await hook({ + paymentRequired: { + accepts: [{ network: "eip155:1" }], + extensions: enrichedExtension, + }, + }); + + expect(result).toHaveProperty("headers"); + expect(result!.headers).toHaveProperty("sign-in-with-x"); + + // Verify the header is valid + const parsed = parseSIWxHeader(result!.headers["sign-in-with-x"]); + expect(parsed.address.toLowerCase()).toBe(account.address.toLowerCase()); + }); + }); +}); + +describe("siwxResourceServerExtension", () => { + const mockContext = (networks: string[], url = "https://api.example.com/resource") => ({ + requirements: networks.map(network => ({ network, scheme: "exact" })), + resourceInfo: { url }, + }); + + it("derives single network from requirements", async () => { + const declaration = declareSIWxExtension({}); + const ext = declaration["sign-in-with-x"]; + + const result = (await siwxResourceServerExtension.enrichPaymentRequiredResponse!( + ext, + mockContext(["eip155:8453"]), + )) as SIWxExtension; + + expect(result.supportedChains).toHaveLength(1); + expect(result.supportedChains[0]).toEqual({ chainId: "eip155:8453", type: "eip191" }); + }); + + it("derives multiple networks from requirements (EVM + Solana)", async () => { + const declaration = declareSIWxExtension({}); + const ext = declaration["sign-in-with-x"]; + + const result = (await siwxResourceServerExtension.enrichPaymentRequiredResponse!( + ext, + mockContext(["eip155:8453", SOLANA_MAINNET]), + )) as SIWxExtension; + + expect(result.supportedChains).toHaveLength(2); + expect(result.supportedChains[0]).toEqual({ chainId: "eip155:8453", type: "eip191" }); + expect(result.supportedChains[1]).toEqual({ chainId: SOLANA_MAINNET, type: "ed25519" }); + }); + + it("generates fresh time-based fields", async () => { + const declaration = declareSIWxExtension({ expirationSeconds: 300 }); + const ext = declaration["sign-in-with-x"]; + + const result = (await siwxResourceServerExtension.enrichPaymentRequiredResponse!( + ext, + mockContext(["eip155:8453"]), + )) as SIWxExtension; + + expect(result.info.nonce).toHaveLength(32); + expect(result.info.issuedAt).toBeDefined(); + expect(result.info.expirationTime).toBeDefined(); + }); + + it("derives domain and uri from request context", async () => { + const declaration = declareSIWxExtension({}); + const ext = declaration["sign-in-with-x"]; + + const result = (await siwxResourceServerExtension.enrichPaymentRequiredResponse!( + ext, + mockContext(["eip155:8453"], "https://api.example.com/data"), + )) as SIWxExtension; + + expect(result.info.domain).toBe("api.example.com"); + expect(result.info.uri).toBe("https://api.example.com/data"); + }); +}); diff --git a/typescript/packages/extensions/tsconfig.json b/typescript/packages/extensions/tsconfig.json new file mode 100644 index 0000000..b96e5ce --- /dev/null +++ b/typescript/packages/extensions/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2020", "DOM"], + "allowJs": false, + "checkJs": false + }, + "include": ["src", "test/bazaar.test.ts"] +} diff --git a/typescript/packages/extensions/tsup.config.ts b/typescript/packages/extensions/tsup.config.ts new file mode 100644 index 0000000..f082e07 --- /dev/null +++ b/typescript/packages/extensions/tsup.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from "tsup"; + +const baseConfig = { + entry: { + index: "src/index.ts", + "bazaar/index": "src/bazaar/index.ts", + "sign-in-with-x/index": "src/sign-in-with-x/index.ts", + "payment-identifier/index": "src/payment-identifier/index.ts", + }, + dts: { + resolve: true, + }, + sourcemap: true, + target: "es2020", +}; + +export default defineConfig([ + { + ...baseConfig, + format: "esm", + outDir: "dist/esm", + clean: true, + }, + { + ...baseConfig, + format: "cjs", + outDir: "dist/cjs", + clean: false, + }, +]); diff --git a/typescript/packages/extensions/vitest.config.ts b/typescript/packages/extensions/vitest.config.ts new file mode 100644 index 0000000..156f8c9 --- /dev/null +++ b/typescript/packages/extensions/vitest.config.ts @@ -0,0 +1,10 @@ +import { loadEnv } from "vite"; +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ""), + }, + plugins: [tsconfigPaths({ projects: ["."] })], +})); diff --git a/typescript/packages/http/axios/.prettierignore b/typescript/packages/http/axios/.prettierignore new file mode 100644 index 0000000..5bd240b --- /dev/null +++ b/typescript/packages/http/axios/.prettierignore @@ -0,0 +1,8 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +src/client +**/**/*.json +*.md \ No newline at end of file diff --git a/typescript/packages/http/axios/.prettierrc b/typescript/packages/http/axios/.prettierrc new file mode 100644 index 0000000..ffb416b --- /dev/null +++ b/typescript/packages/http/axios/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/packages/http/axios/CHANGELOG.md b/typescript/packages/http/axios/CHANGELOG.md new file mode 100644 index 0000000..ee014bc --- /dev/null +++ b/typescript/packages/http/axios/CHANGELOG.md @@ -0,0 +1,21 @@ +# @x402/axios Changelog + +## 2.3.0 + +### Minor Changes + +- 51b8445: Bumped @x402/core dependency to 2.3.0 + +### Patch Changes + +- Updated dependencies [51b8445] +- Updated dependencies [51b8445] + - @x402/core@2.3.0 + +## 2.0.0 + +- Implements x402 2.0.0 for the TypeScript SDK. + +## 1.0.0 + +- Implements x402 1.0.0 for the TypeScript SDK. diff --git a/typescript/packages/http/axios/README.md b/typescript/packages/http/axios/README.md new file mode 100644 index 0000000..95ff9c3 --- /dev/null +++ b/typescript/packages/http/axios/README.md @@ -0,0 +1,197 @@ +# x402-axios + +A utility package that extends Axios to automatically handle 402 Payment Required responses using the x402 payment protocol v2. This package enables seamless integration of payment functionality into your applications when making HTTP requests. + +## Installation + +```bash +pnpm install @x402/axios +``` + +## Quick Start + +```typescript +import axios from "axios"; +import { wrapAxiosWithPaymentFromConfig } from "@x402/axios"; +import { ExactEvmScheme } from "@x402/evm"; +import { privateKeyToAccount } from "viem/accounts"; + +// Create an account +const account = privateKeyToAccount("0xYourPrivateKey"); + +// Wrap the axios instance with payment handling +const api = wrapAxiosWithPaymentFromConfig(axios.create(), { + schemes: [ + { + network: "eip155:8453", // Base Sepolia + client: new ExactEvmScheme(account), + }, + ], +}); + +// Make a request that may require payment +const response = await api.get("https://api.example.com/paid-endpoint"); + +const data = response.data; +``` + +## API + +### `wrapAxiosWithPayment(axiosInstance, client)` + +Wraps an Axios instance to handle 402 Payment Required responses automatically. + +#### Parameters + +- `axiosInstance`: The Axios instance to wrap (typically from `axios.create()`) +- `client`: An x402Client instance with registered payment schemes + +### `wrapAxiosWithPaymentFromConfig(axiosInstance, config)` + +Convenience wrapper that creates an x402Client from a configuration object. + +#### Parameters + +- `axiosInstance`: The Axios instance to wrap (typically from `axios.create()`) +- `config`: Configuration object with the following properties: + - `schemes`: Array of scheme registrations, each containing: + - `network`: Network identifier (e.g., 'eip155:8453', 'solana:mainnet', 'eip155:*' for wildcards) + - `client`: The scheme client implementation (e.g., `ExactEvmScheme`, `ExactSvmScheme`) + - `x402Version`: Optional protocol version (defaults to 2, set to 1 for legacy support) + - `paymentRequirementsSelector`: Optional function to select payment requirements from multiple options + +#### Returns + +A wrapped Axios instance that automatically handles 402 responses by: +1. Making the initial request +2. If a 402 response is received, parsing the payment requirements +3. Creating a payment header using the configured scheme client +4. Retrying the request with the payment header + +## Examples + +### Basic Usage with EVM + +```typescript +import { config } from "dotenv"; +import axios from "axios"; +import { wrapAxiosWithPaymentFromConfig, decodePaymentResponseHeader } from "@x402/axios"; +import { privateKeyToAccount } from "viem/accounts"; +import { ExactEvmScheme } from "@x402/evm"; + +config(); + +const { EVM_PRIVATE_KEY, API_URL } = process.env; + +const account = privateKeyToAccount(EVM_PRIVATE_KEY as `0x${string}`); + +const api = wrapAxiosWithPaymentFromConfig(axios.create(), { + schemes: [ + { + network: "eip155:*", // Support all EVM chains + client: new ExactEvmScheme(account), + }, + ], +}); + +// Make a request to a paid API endpoint +api.get(API_URL) + .then(response => { + const data = response.data; + + // Optionally decode the payment response header + const paymentResponse = response.headers["payment-response"]; + if (paymentResponse) { + const decoded = decodePaymentResponseHeader(paymentResponse); + console.log("Payment details:", decoded); + } + + console.log("Response data:", data); + }) + .catch(error => { + console.error(error); + }); +``` + +### Using Builder Pattern + +For more control, you can use the builder pattern to register multiple schemes: + +```typescript +import axios from "axios"; +import { wrapAxiosWithPayment, x402Client } from "@x402/axios"; +import { ExactEvmScheme } from "@x402/evm/exact/client"; +import { ExactSvmScheme } from "@x402/svm/exact/client"; +import { privateKeyToAccount } from "viem/accounts"; +import { createKeyPairSignerFromBytes } from "@solana/kit"; +import { base58 } from "@scure/base"; + +// Create signers +const evmSigner = privateKeyToAccount("0xYourPrivateKey"); +const svmSigner = await createKeyPairSignerFromBytes(base58.decode("YourSvmPrivateKey")); + +// Build client with multiple schemes +const client = new x402Client() + .register("eip155:*", new ExactEvmScheme(evmSigner)) + .register("solana:*", new ExactSvmScheme(svmSigner)); + +// Wrap axios with the client +const api = wrapAxiosWithPayment(axios.create(), client); +``` + +### Multi-Chain Support + +```typescript +import axios from "axios"; +import { wrapAxiosWithPaymentFromConfig } from "@x402/axios"; +import { ExactEvmScheme } from "@x402/evm"; +import { ExactSvmScheme } from "@x402/svm"; + +const api = wrapAxiosWithPaymentFromConfig(axios.create(), { + schemes: [ + // EVM chains + { + network: "eip155:8453", // Base Sepolia + client: new ExactEvmScheme(evmAccount), + }, + // SVM chains + { + network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", // Solana devnet + client: new ExactSvmScheme(svmSigner), + }, + ], +}); +``` + +### Custom Payment Requirements Selector + +```typescript +import axios from "axios"; +import { wrapAxiosWithPaymentFromConfig, type SelectPaymentRequirements } from "@x402/axios"; +import { ExactEvmScheme } from "@x402/evm"; + +// Custom selector that prefers the cheapest option +const selectCheapestOption: SelectPaymentRequirements = (version, accepts) => { + if (!accepts || accepts.length === 0) { + throw new Error("No payment options available"); + } + + // Sort by value and return the cheapest + const sorted = [...accepts].sort((a, b) => + BigInt(a.value) - BigInt(b.value) + ); + + return sorted[0]; +}; + +const api = wrapAxiosWithPaymentFromConfig(axios.create(), { + schemes: [ + { + network: "eip155:8453", + client: new ExactEvmScheme(account), + }, + ], + paymentRequirementsSelector: selectCheapestOption, +}); +``` + diff --git a/typescript/packages/http/axios/eslint.config.js b/typescript/packages/http/axios/eslint.config.js new file mode 100644 index 0000000..ca28b5c --- /dev/null +++ b/typescript/packages/http/axios/eslint.config.js @@ -0,0 +1,72 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/typescript/packages/http/axios/package.json b/typescript/packages/http/axios/package.json new file mode 100644 index 0000000..7faa9fe --- /dev/null +++ b/typescript/packages/http/axios/package.json @@ -0,0 +1,60 @@ +{ + "name": "@x402/axios", + "version": "2.3.0", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "start": "tsx --env-file=.env index.ts", + "test": "vitest run", + "test:watch": "vitest", + "build": "tsup", + "watch": "tsc --watch", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "keywords": [], + "license": "Apache-2.0", + "author": "Coinbase Inc.", + "repository": "https://github.com/coinbase/x402", + "description": "x402 Payment Protocol", + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/node": "^22.13.4", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsup": "^8.4.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.5", + "vite": "^6.2.6" + }, + "dependencies": { + "axios": "^1.7.9", + "@x402/core": "workspace:~", + "zod": "^3.24.2" + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.mts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + } + }, + "files": [ + "dist" + ] +} diff --git a/typescript/packages/http/axios/src/index.test.ts b/typescript/packages/http/axios/src/index.test.ts new file mode 100644 index 0000000..d69f66f --- /dev/null +++ b/typescript/packages/http/axios/src/index.test.ts @@ -0,0 +1,356 @@ +import { + AxiosError, + AxiosHeaders, + AxiosInstance, + AxiosResponse, + InternalAxiosRequestConfig, +} from "axios"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { wrapAxiosWithPayment, wrapAxiosWithPaymentFromConfig } from "./index"; +import type { x402Client, x402ClientConfig } from "@x402/core/client"; +import type { PaymentPayload, PaymentRequired, PaymentRequirements } from "@x402/core/types"; + +// Mock the @x402/core/client module +vi.mock("@x402/core/client", () => { + const MockX402HTTPClient = vi.fn(); + MockX402HTTPClient.prototype.getPaymentRequiredResponse = vi.fn(); + MockX402HTTPClient.prototype.encodePaymentSignatureHeader = vi.fn(); + MockX402HTTPClient.prototype.handlePaymentRequired = vi.fn(); + + const MockX402Client = vi.fn() as ReturnType & { + fromConfig: ReturnType; + }; + MockX402Client.prototype.createPaymentPayload = vi.fn(); + MockX402Client.fromConfig = vi.fn(); + + return { + x402HTTPClient: MockX402HTTPClient, + x402Client: MockX402Client, + }; +}); + +describe("wrapAxiosWithPayment()", () => { + let mockAxiosClient: AxiosInstance; + let mockClient: x402Client; + let interceptor: (error: AxiosError) => Promise; + + const validPaymentRequired: PaymentRequired = { + x402Version: 2, + resource: { + url: "https://api.example.com/resource", + description: "Test payment", + mimeType: "application/json", + }, + accepts: [ + { + scheme: "exact", + network: "eip155:84532" as const, + amount: "1000000", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + payTo: "0x1234567890123456789012345678901234567890", + maxTimeoutSeconds: 300, + extra: {}, + } as PaymentRequirements, + ], + }; + + const validPaymentPayload: PaymentPayload = { + x402Version: 2, + resource: validPaymentRequired.resource, + accepted: validPaymentRequired.accepts[0], + payload: { signature: "0xmocksignature" }, + }; + + const createErrorConfig = (isRetry = false): InternalAxiosRequestConfig => + ({ + headers: new AxiosHeaders(), + url: "https://api.example.com", + method: "GET", + ...(isRetry ? { __is402Retry: true } : {}), + }) as InternalAxiosRequestConfig; + + const createAxiosError = ( + status: number, + config?: InternalAxiosRequestConfig, + data?: PaymentRequired, + headers?: Record, + ): AxiosError => { + return new AxiosError( + "Error", + "ERROR", + config, + {}, + { + status, + statusText: status === 402 ? "Payment Required" : "Not Found", + data, + headers: headers || {}, + config: config || createErrorConfig(), + }, + ); + }; + + beforeEach(async () => { + vi.resetAllMocks(); + + // Mock axios client + mockAxiosClient = { + interceptors: { + response: { + use: vi.fn(), + }, + }, + request: vi.fn(), + } as unknown as AxiosInstance; + + // Create mock client + const { x402Client: MockX402Client, x402HTTPClient: MockX402HTTPClient } = await import( + "@x402/core/client" + ); + + mockClient = new MockX402Client() as unknown as x402Client; + + // Setup default mock implementations + (mockClient.createPaymentPayload as ReturnType).mockResolvedValue( + validPaymentPayload, + ); + + ( + MockX402HTTPClient.prototype.getPaymentRequiredResponse as ReturnType + ).mockReturnValue(validPaymentRequired); + ( + MockX402HTTPClient.prototype.encodePaymentSignatureHeader as ReturnType + ).mockReturnValue({ + "PAYMENT-SIGNATURE": "encoded-payment-header", + }); + ( + MockX402HTTPClient.prototype.handlePaymentRequired as ReturnType + ).mockResolvedValue(null); + + // Set up the interceptor + wrapAxiosWithPayment(mockAxiosClient, mockClient); + interceptor = (mockAxiosClient.interceptors.response.use as ReturnType).mock + .calls[0][1]; + }); + + it("should return the axios client instance", () => { + const result = wrapAxiosWithPayment(mockAxiosClient, mockClient); + expect(result).toBe(mockAxiosClient); + }); + + it("should set up response interceptor", () => { + expect(mockAxiosClient.interceptors.response.use).toHaveBeenCalled(); + }); + + it("should pass through successful responses", async () => { + const successHandler = (mockAxiosClient.interceptors.response.use as ReturnType) + .mock.calls[0][0]; + const response = { data: "success" } as AxiosResponse; + expect(successHandler(response)).toBe(response); + }); + + it("should not handle non-402 errors", async () => { + const error = createAxiosError(404); + await expect(interceptor(error)).rejects.toBe(error); + }); + + it("should not handle errors without response", async () => { + const error = new AxiosError("Network Error", "ECONNREFUSED"); + await expect(interceptor(error)).rejects.toBe(error); + }); + + it("should handle 402 errors and retry with payment header", async () => { + const { x402HTTPClient: MockX402HTTPClient } = await import("@x402/core/client"); + const successResponse = { data: "success" } as AxiosResponse; + + (mockAxiosClient.request as ReturnType).mockResolvedValue(successResponse); + + const error = createAxiosError(402, createErrorConfig(), validPaymentRequired, { + "PAYMENT-REQUIRED": "encoded-payment-required", + }); + + const result = await interceptor(error); + + expect(result).toBe(successResponse); + expect(MockX402HTTPClient.prototype.getPaymentRequiredResponse).toHaveBeenCalled(); + expect(mockClient.createPaymentPayload).toHaveBeenCalledWith(validPaymentRequired); + expect(MockX402HTTPClient.prototype.encodePaymentSignatureHeader).toHaveBeenCalledWith( + validPaymentPayload, + ); + expect(mockAxiosClient.request).toHaveBeenCalled(); + + // Verify the retry config has payment headers and retry flag + const retryConfig = (mockAxiosClient.request as ReturnType).mock.calls[0][0]; + expect(retryConfig.__is402Retry).toBe(true); + }); + + it("should not retry if already retried", async () => { + const error = createAxiosError(402, createErrorConfig(true), validPaymentRequired); + await expect(interceptor(error)).rejects.toBe(error); + }); + + it("should reject if missing request config", async () => { + const error = createAxiosError(402, undefined, validPaymentRequired); + await expect(interceptor(error)).rejects.toThrow("Missing axios request configuration"); + }); + + it("should reject if missing headers in config", async () => { + const configWithoutHeaders = { + url: "https://api.example.com", + method: "GET", + } as InternalAxiosRequestConfig; + + const error = createAxiosError(402, configWithoutHeaders, validPaymentRequired); + await expect(interceptor(error)).rejects.toThrow("Missing axios request configuration"); + }); + + it("should reject with descriptive error if payment requirements parsing fails", async () => { + const { x402HTTPClient: MockX402HTTPClient } = await import("@x402/core/client"); + ( + MockX402HTTPClient.prototype.getPaymentRequiredResponse as ReturnType + ).mockImplementation(() => { + throw new Error("Invalid payment header format"); + }); + + const error = createAxiosError(402, createErrorConfig(), undefined); + await expect(interceptor(error)).rejects.toThrow( + "Failed to parse payment requirements: Invalid payment header format", + ); + }); + + it("should reject with descriptive error if payment payload creation fails", async () => { + const paymentError = new Error("Insufficient funds"); + (mockClient.createPaymentPayload as ReturnType).mockRejectedValue(paymentError); + + const error = createAxiosError(402, createErrorConfig(), validPaymentRequired); + await expect(interceptor(error)).rejects.toThrow( + "Failed to create payment payload: Insufficient funds", + ); + }); + + it("should reject with generic error message for unknown parsing errors", async () => { + const { x402HTTPClient: MockX402HTTPClient } = await import("@x402/core/client"); + ( + MockX402HTTPClient.prototype.getPaymentRequiredResponse as ReturnType + ).mockImplementation(() => { + throw "String error"; // Non-Error thrown + }); + + const error = createAxiosError(402, createErrorConfig(), undefined); + await expect(interceptor(error)).rejects.toThrow( + "Failed to parse payment requirements: Unknown error", + ); + }); + + it("should reject with generic error message for unknown payment creation errors", async () => { + (mockClient.createPaymentPayload as ReturnType).mockRejectedValue("String error"); + + const error = createAxiosError(402, createErrorConfig(), validPaymentRequired); + await expect(interceptor(error)).rejects.toThrow( + "Failed to create payment payload: Unknown error", + ); + }); + + it("should handle v1 payment responses from body", async () => { + const { x402HTTPClient: MockX402HTTPClient } = await import("@x402/core/client"); + const successResponse = { data: "success" } as AxiosResponse; + + const v1PaymentRequired: PaymentRequired = { + ...validPaymentRequired, + x402Version: 1, + }; + + const v1PaymentPayload: PaymentPayload = { + ...validPaymentPayload, + x402Version: 1, + }; + + ( + MockX402HTTPClient.prototype.getPaymentRequiredResponse as ReturnType + ).mockReturnValue(v1PaymentRequired); + ( + MockX402HTTPClient.prototype.encodePaymentSignatureHeader as ReturnType + ).mockReturnValue({ + "X-PAYMENT": "v1-payment-header", + }); + (mockClient.createPaymentPayload as ReturnType).mockResolvedValue( + v1PaymentPayload, + ); + (mockAxiosClient.request as ReturnType).mockResolvedValue(successResponse); + + const error = createAxiosError(402, createErrorConfig(), v1PaymentRequired); + + const result = await interceptor(error); + + expect(result).toBe(successResponse); + expect(MockX402HTTPClient.prototype.encodePaymentSignatureHeader).toHaveBeenCalledWith( + v1PaymentPayload, + ); + }); + + it("should propagate retry errors", async () => { + const retryError = new Error("Retry failed"); + (mockAxiosClient.request as ReturnType).mockRejectedValue(retryError); + + const error = createAxiosError(402, createErrorConfig(), validPaymentRequired); + + await expect(interceptor(error)).rejects.toBe(retryError); + }); + + it("should set Access-Control-Expose-Headers on retry request", async () => { + const successResponse = { data: "success" } as AxiosResponse; + (mockAxiosClient.request as ReturnType).mockResolvedValue(successResponse); + + const error = createAxiosError(402, createErrorConfig(), validPaymentRequired); + + await interceptor(error); + + const retryConfig = (mockAxiosClient.request as ReturnType).mock.calls[0][0]; + expect(retryConfig.headers.get("Access-Control-Expose-Headers")).toBe( + "PAYMENT-RESPONSE,X-PAYMENT-RESPONSE", + ); + }); +}); + +describe("wrapAxiosWithPaymentFromConfig()", () => { + let mockAxiosClient: AxiosInstance; + + beforeEach(async () => { + vi.resetAllMocks(); + + mockAxiosClient = { + interceptors: { + response: { + use: vi.fn(), + }, + }, + request: vi.fn(), + } as unknown as AxiosInstance; + + const { x402Client: MockX402Client } = await import("@x402/core/client"); + (MockX402Client.fromConfig as ReturnType).mockReturnValue(new MockX402Client()); + }); + + it("should create client from config and wrap axios", async () => { + const { x402Client: MockX402Client } = await import("@x402/core/client"); + + const config: x402ClientConfig = { + schemes: [], + }; + + const result = wrapAxiosWithPaymentFromConfig(mockAxiosClient, config); + + expect(MockX402Client.fromConfig).toHaveBeenCalledWith(config); + expect(result).toBe(mockAxiosClient); + expect(mockAxiosClient.interceptors.response.use).toHaveBeenCalled(); + }); + + it("should return the axios client instance", () => { + const config: x402ClientConfig = { + schemes: [], + }; + + const result = wrapAxiosWithPaymentFromConfig(mockAxiosClient, config); + expect(result).toBe(mockAxiosClient); + }); +}); diff --git a/typescript/packages/http/axios/src/index.ts b/typescript/packages/http/axios/src/index.ts new file mode 100644 index 0000000..041ef28 --- /dev/null +++ b/typescript/packages/http/axios/src/index.ts @@ -0,0 +1,195 @@ +import { x402Client, x402ClientConfig, x402HTTPClient } from "@x402/core/client"; +import { type PaymentRequired } from "@x402/core/types"; +import type { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from "axios"; + +/** + * Wraps an Axios instance with x402 payment handling. + * + * This function adds an interceptor to automatically handle 402 Payment Required responses + * by creating and sending payment headers. It will: + * 1. Intercept 402 responses + * 2. Parse the payment requirements + * 3. Create a payment header using the configured x402HTTPClient + * 4. Retry the request with the payment header + * + * @param axiosInstance - The Axios instance to wrap + * @param client - Configured x402Client instance for handling payments + * @returns The wrapped Axios instance that handles 402 responses automatically + * + * @example + * ```typescript + * import axios from 'axios'; + * import { wrapAxiosWithPayment, x402Client } from '@x402/axios'; + * import { ExactEvmScheme } from '@x402/evm'; + * import { privateKeyToAccount } from 'viem/accounts'; + * + * const account = privateKeyToAccount('0x...'); + * const client = new x402Client() + * .register('eip155:*', new ExactEvmScheme(account)); + * + * const api = wrapAxiosWithPayment(axios.create(), client); + * + * // Make a request that may require payment + * const response = await api.get('https://api.example.com/paid-endpoint'); + * ``` + * + * @throws {Error} If no schemes are provided + * @throws {Error} If the request configuration is missing + * @throws {Error} If a payment has already been attempted for this request + * @throws {Error} If there's an error creating the payment header + */ +export function wrapAxiosWithPayment( + axiosInstance: AxiosInstance, + client: x402Client | x402HTTPClient, +): AxiosInstance { + const httpClient = client instanceof x402HTTPClient ? client : new x402HTTPClient(client); + + axiosInstance.interceptors.response.use( + response => response, + async (error: AxiosError) => { + if (!error.response || error.response.status !== 402) { + return Promise.reject(error); + } + + const originalConfig = error.config; + if (!originalConfig || !originalConfig.headers) { + return Promise.reject(new Error("Missing axios request configuration")); + } + + // Check if this is already a retry to prevent infinite loops + if ( + (originalConfig as InternalAxiosRequestConfig & { __is402Retry?: boolean }).__is402Retry + ) { + return Promise.reject(error); + } + + try { + // Parse payment requirements from response + let paymentRequired: PaymentRequired; + try { + const response = error.response!; // Already validated above + + // Create getHeader function for case-insensitive header lookup + const getHeader = (name: string) => { + const value = response.headers[name] ?? response.headers[name.toLowerCase()]; + return typeof value === "string" ? value : undefined; + }; + + // Try to get from headers first (v2), then from body (v1) + const body = response.data as PaymentRequired | undefined; + + paymentRequired = httpClient.getPaymentRequiredResponse(getHeader, body); + } catch (parseError) { + return Promise.reject( + new Error( + `Failed to parse payment requirements: ${parseError instanceof Error ? parseError.message : "Unknown error"}`, + ), + ); + } + + // Run payment required hooks + const hookHeaders = await httpClient.handlePaymentRequired(paymentRequired); + if (hookHeaders) { + const hookConfig = { ...originalConfig }; + hookConfig.headers = { ...originalConfig.headers } as typeof originalConfig.headers; + Object.entries(hookHeaders).forEach(([key, value]) => { + hookConfig.headers.set(key, value); + }); + const hookResponse = await axiosInstance.request(hookConfig); + if (hookResponse.status !== 402) { + return hookResponse; // Hook succeeded + } + // Hook's retry got 402, fall through to payment + } + + // Create payment payload + let paymentPayload; + try { + paymentPayload = await client.createPaymentPayload(paymentRequired); + } catch (paymentError) { + return Promise.reject( + new Error( + `Failed to create payment payload: ${paymentError instanceof Error ? paymentError.message : "Unknown error"}`, + ), + ); + } + + // Encode payment header + const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload); + + // Mark this as a retry + (originalConfig as InternalAxiosRequestConfig & { __is402Retry?: boolean }).__is402Retry = + true; + + // Add payment headers to the request + Object.entries(paymentHeaders).forEach(([key, value]) => { + originalConfig.headers.set(key, value); + }); + + // Add CORS header to expose payment response + originalConfig.headers.set( + "Access-Control-Expose-Headers", + "PAYMENT-RESPONSE,X-PAYMENT-RESPONSE", + ); + + // Retry the request with payment + const secondResponse = await axiosInstance.request(originalConfig); + return secondResponse; + } catch (retryError) { + return Promise.reject(retryError); + } + }, + ); + + return axiosInstance; +} + +/** + * Wraps an Axios instance with x402 payment handling using a configuration object. + * + * @param axiosInstance - The Axios instance to wrap + * @param config - Configuration options including scheme registrations and selectors + * @returns The wrapped Axios instance that handles 402 responses automatically + * + * @example + * ```typescript + * import axios from 'axios'; + * import { wrapAxiosWithPaymentFromConfig } from '@x402/axios'; + * import { ExactEvmScheme } from '@x402/evm'; + * import { privateKeyToAccount } from 'viem/accounts'; + * + * const account = privateKeyToAccount('0x...'); + * + * const api = wrapAxiosWithPaymentFromConfig(axios.create(), { + * schemes: [ + * { network: 'eip155:*', client: new ExactEvmScheme(account) } + * ] + * }); + * + * const response = await api.get('https://api.example.com/paid-endpoint'); + * ``` + */ +export function wrapAxiosWithPaymentFromConfig( + axiosInstance: AxiosInstance, + config: x402ClientConfig, +): AxiosInstance { + const client = x402Client.fromConfig(config); + return wrapAxiosWithPayment(axiosInstance, client); +} + +// Re-export types and utilities for convenience +export { x402Client, x402HTTPClient } from "@x402/core/client"; +export type { + PaymentPolicy, + SchemeRegistration, + SelectPaymentRequirements, + x402ClientConfig, +} from "@x402/core/client"; +export { decodePaymentResponseHeader } from "@x402/core/http"; +export type { + Network, + PaymentPayload, + PaymentRequired, + PaymentRequirements, + SchemeNetworkClient, +} from "@x402/core/types"; diff --git a/typescript/packages/http/axios/tsconfig.json b/typescript/packages/http/axios/tsconfig.json new file mode 100644 index 0000000..1b119d3 --- /dev/null +++ b/typescript/packages/http/axios/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "allowJs": false, + "checkJs": false + }, + "include": ["src"] +} diff --git a/typescript/packages/http/axios/tsup.config.ts b/typescript/packages/http/axios/tsup.config.ts new file mode 100644 index 0000000..f8699f9 --- /dev/null +++ b/typescript/packages/http/axios/tsup.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from "tsup"; + +const baseConfig = { + entry: { + index: "src/index.ts", + }, + dts: { + resolve: true, + }, + sourcemap: true, + target: "node16", +}; + +export default defineConfig([ + { + ...baseConfig, + format: "esm", + outDir: "dist/esm", + clean: true, + }, + { + ...baseConfig, + format: "cjs", + outDir: "dist/cjs", + clean: false, + }, +]); diff --git a/typescript/packages/http/axios/vitest.config.ts b/typescript/packages/http/axios/vitest.config.ts new file mode 100644 index 0000000..156f8c9 --- /dev/null +++ b/typescript/packages/http/axios/vitest.config.ts @@ -0,0 +1,10 @@ +import { loadEnv } from "vite"; +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ""), + }, + plugins: [tsconfigPaths({ projects: ["."] })], +})); diff --git a/typescript/packages/http/express/.prettierignore b/typescript/packages/http/express/.prettierignore new file mode 100644 index 0000000..5bd240b --- /dev/null +++ b/typescript/packages/http/express/.prettierignore @@ -0,0 +1,8 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +src/client +**/**/*.json +*.md \ No newline at end of file diff --git a/typescript/packages/http/express/.prettierrc b/typescript/packages/http/express/.prettierrc new file mode 100644 index 0000000..ffb416b --- /dev/null +++ b/typescript/packages/http/express/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/packages/http/express/CHANGELOG.md b/typescript/packages/http/express/CHANGELOG.md new file mode 100644 index 0000000..3e5517f --- /dev/null +++ b/typescript/packages/http/express/CHANGELOG.md @@ -0,0 +1,26 @@ +# @x402/express Changelog + +## 2.3.0 + +### Minor Changes + +- 51b8445: Bumped @x402/core dependency to 2.3.0 + +### Patch Changes + +- Updated dependencies [51b8445] +- Updated dependencies [51b8445] +- Updated dependencies [51b8445] +- Updated dependencies [fe42994] +- Updated dependencies [51b8445] + - @x402/core@2.3.0 + - @x402/paywall@2.3.0 + - @x402/extensions@2.3.0 + +## 2.0.0 + +- Implements x402 2.0.0 for the TypeScript SDK. + +## 1.0.0 + +- Implements x402 1.0.0 for the TypeScript SDK. diff --git a/typescript/packages/http/express/README.md b/typescript/packages/http/express/README.md new file mode 100644 index 0000000..d9cba68 --- /dev/null +++ b/typescript/packages/http/express/README.md @@ -0,0 +1,281 @@ +# @x402/express + +Express middleware integration for the x402 Payment Protocol. This package provides a simple middleware function for adding x402 payment requirements to your Express.js applications. + +## Installation + +```bash +pnpm install @x402/express +``` + +## Quick Start + +```typescript +import express from "express"; +import { paymentMiddleware, x402ResourceServer } from "@x402/express"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { HTTPFacilitatorClient } from "@x402/core/server"; + +const app = express(); + +const facilitatorClient = new HTTPFacilitatorClient({ url: "https://facilitator.x402.org" }); +const resourceServer = new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()); + +// Apply the payment middleware with your configuration +app.use( + paymentMiddleware( + { + "GET /protected-route": { + accepts: { + scheme: "exact", + price: "$0.10", + network: "eip155:84532", + payTo: "0xYourAddress", + }, + description: "Access to premium content", + }, + }, + resourceServer, + ), +); + +// Implement your protected route +app.get("/protected-route", (req, res) => { + res.json({ message: "This content is behind a paywall" }); +}); + +app.listen(3000); +``` + +## Configuration + +The `paymentMiddleware` function accepts the following parameters: + +```typescript +paymentMiddleware( + routes: RoutesConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart?: boolean +) +``` + +### Parameters + +1. **`routes`** (required): Route configurations for protected endpoints +2. **`server`** (required): Pre-configured x402ResourceServer instance +3. **`paywallConfig`** (optional): Configuration for the built-in paywall UI +4. **`paywall`** (optional): Custom paywall provider +5. **`syncFacilitatorOnStart`** (optional): Whether to sync with facilitator on startup (defaults to true) + +See the sections below for detailed configuration options. + +## API Reference + +### ExpressAdapter + +The `ExpressAdapter` class implements the `HTTPAdapter` interface from `@x402/core`, providing Express-specific request handling: + +```typescript +class ExpressAdapter implements HTTPAdapter { + getHeader(name: string): string | undefined; + getMethod(): string; + getPath(): string; + getUrl(): string; + getAcceptHeader(): string; + getUserAgent(): string; +} +``` + +### Middleware Function + +```typescript +function paymentMiddleware( + routes: RoutesConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart?: boolean, +): (req: Request, res: Response, next: NextFunction) => Promise; +``` + +Creates Express middleware that: + +1. Uses the provided x402ResourceServer for payment processing +2. Checks if the incoming request matches a protected route +3. Validates payment headers if required +4. Returns payment instructions (402 status) if payment is missing or invalid +5. Processes the request if payment is valid +6. Handles settlement after successful response + +### Route Configuration + +Routes are passed as the first parameter to `paymentMiddleware`: + +```typescript +const routes: RoutesConfig = { + "GET /api/protected": { + accepts: { + scheme: "exact", + price: "$0.10", + network: "eip155:84532", + payTo: "0xYourAddress", + maxTimeoutSeconds: 60, + }, + description: "Premium API access", + }, +}; + +app.use(paymentMiddleware(routes, resourceServer)); +``` + +### Paywall Configuration + +The middleware automatically displays a paywall UI when browsers request protected endpoints. + +**Option 1: Full Paywall UI (Recommended)** + +Install the optional `@x402/paywall` package for a complete wallet connection and payment UI: + +```bash +pnpm add @x402/paywall +``` + +Then configure it: + +```typescript +const paywallConfig: PaywallConfig = { + appName: "Your App Name", + appLogo: "/path/to/logo.svg", + testnet: true, +}; + +app.use(paymentMiddleware(routes, resourceServer, paywallConfig)); +``` + +The paywall includes: + +- EVM wallet support (MetaMask, Coinbase Wallet, etc.) +- Solana wallet support (Phantom, Solflare, etc.) +- USDC balance checking +- Chain switching +- Onramp integration for mainnet + +**Option 2: Basic Paywall (No Installation)** + +Without `@x402/paywall` installed, the middleware returns a basic HTML page with payment instructions. This works but doesn't include wallet connections. + +**Option 3: Custom Paywall Provider** + +Provide your own paywall provider: + +```typescript +app.use(paymentMiddleware(routes, resourceServer, paywallConfig, customPaywallProvider)); +``` + +This allows full customization of the paywall UI. + +**For advanced configuration** (builder pattern, network-specific bundles, custom handlers), see the [@x402/paywall README](../paywall/README.md). + +## Advanced Usage + +### Multiple Protected Routes + +```typescript +app.use( + paymentMiddleware( + { + "GET /api/premium/*": { + accepts: { + scheme: "exact", + price: "$1.00", + network: "eip155:8453", + payTo: "0xYourAddress", + }, + description: "Premium API access", + }, + "GET /api/data": { + accepts: { + scheme: "exact", + price: "$0.50", + network: "eip155:84532", + payTo: "0xYourAddress", + maxTimeoutSeconds: 120, + }, + description: "Data endpoint access", + }, + }, + resourceServer, + ), +); +``` + +### Custom Facilitator Client + +If you need to use a custom facilitator server, configure it when creating the x402ResourceServer: + +```typescript +import { HTTPFacilitatorClient } from "@x402/core/server"; +import { x402ResourceServer } from "@x402/express"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; + +const customFacilitator = new HTTPFacilitatorClient({ + url: "https://your-facilitator.com", + createAuthHeaders: async () => ({ + verify: { Authorization: "Bearer your-token" }, + settle: { Authorization: "Bearer your-token" }, + }), +}); + +const resourceServer = new x402ResourceServer(customFacilitator) + .register("eip155:84532", new ExactEvmScheme()); + +app.use(paymentMiddleware(routes, resourceServer, paywallConfig)); +``` + +## Migration from x402-express + +If you're migrating from the legacy `x402-express` package: + +1. **Update imports**: Change from `x402-express` to `@x402/express` +2. **New API**: Create an x402ResourceServer and register payment schemes +3. **Parameter order**: Routes first, then resource server, then optional paywall config + +### Before (x402-express): + +```typescript +import { paymentMiddleware } from "x402-express"; + +app.use( + paymentMiddleware( + payTo, // First param was payTo address + routes, // Second param was routes + facilitator, // Third param was facilitator config + paywall, // Fourth param was paywall config + ), +); +``` + +### After (@x402/express): + +```typescript +import { paymentMiddleware, x402ResourceServer } from "@x402/express"; +import { HTTPFacilitatorClient } from "@x402/core/server"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; + +const facilitator = new HTTPFacilitatorClient({ url: facilitatorUrl }); +const resourceServer = new x402ResourceServer(facilitator) + .register("eip155:84532", new ExactEvmScheme()); + +app.use( + paymentMiddleware( + routes, // First param is routes (payTo is part of route config) + resourceServer, // Second param is resource server (required) + paywallConfig, // Third param is paywall config (optional) + ), +); +``` + +Note: The `payTo` address is now specified within each route configuration rather than as a separate parameter. diff --git a/typescript/packages/http/express/eslint.config.js b/typescript/packages/http/express/eslint.config.js new file mode 100644 index 0000000..044411c --- /dev/null +++ b/typescript/packages/http/express/eslint.config.js @@ -0,0 +1,73 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + BufferEncoding: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/typescript/packages/http/express/package.json b/typescript/packages/http/express/package.json new file mode 100644 index 0000000..add96cb --- /dev/null +++ b/typescript/packages/http/express/package.json @@ -0,0 +1,74 @@ +{ + "name": "@x402/express", + "version": "2.3.0", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "start": "tsx --env-file=.env index.ts", + "test": "vitest run", + "test:watch": "vitest", + "build": "tsup", + "watch": "tsc --watch", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "keywords": [], + "license": "Apache-2.0", + "author": "Coinbase Inc.", + "repository": "https://github.com/coinbase/x402", + "description": "x402 Payment Protocol", + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/express": "^5.0.1", + "@types/node": "^22.13.4", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "express": "^4.18.2", + "prettier": "3.5.2", + "tsup": "^8.4.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vite": "^6.2.6", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.5" + }, + "dependencies": { + "@coinbase/cdp-sdk": "^1.22.0", + "@solana/kit": "^2.1.1", + "@x402/core": "workspace:~", + "@x402/extensions": "workspace:~", + "viem": "^2.39.3", + "zod": "^3.24.2" + }, + "peerDependencies": { + "@x402/paywall": "workspace:*", + "express": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@x402/paywall": { + "optional": true + } + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.mts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + } + }, + "files": [ + "dist" + ] +} diff --git a/typescript/packages/http/express/src/adapter.test.ts b/typescript/packages/http/express/src/adapter.test.ts new file mode 100644 index 0000000..9a2c9f3 --- /dev/null +++ b/typescript/packages/http/express/src/adapter.test.ts @@ -0,0 +1,200 @@ +import { describe, it, expect, vi } from "vitest"; +import { Request } from "express"; +import { ExpressAdapter } from "./adapter"; + +/** + * Factory for creating mock Express Request. + * + * @param options - Configuration options for the mock request. + * @param options.path - The request URL path. + * @param options.method - The HTTP method. + * @param options.headers - Request headers. + * @param options.query - Query parameters. + * @param options.body - Request body. + * @param options.protocol - Request protocol (default: "https"). + * @param options.host - Request host (default: "example.com"). + * @param options.originalUrl - Original URL including query string (defaults to path). + * @returns A mock Express Request. + */ +function createMockRequest( + options: { + path?: string; + originalUrl?: string; + method?: string; + headers?: Record; + query?: Record; + body?: unknown; + protocol?: string; + host?: string; + } = {}, +): Request { + const headers = options.headers || {}; + const path = options.path || "/api/test"; + + const mockRequest = { + header: vi.fn((name: string) => headers[name]), + method: options.method || "GET", + path, + originalUrl: options.originalUrl || path, + protocol: options.protocol || "https", + headers: { + host: options.host || "example.com", + ...headers, + }, + query: options.query || {}, + body: options.body, + } as unknown as Request; + + return mockRequest; +} + +describe("ExpressAdapter", () => { + describe("getHeader", () => { + it("returns header value when present", () => { + const req = createMockRequest({ headers: { "X-Payment": "test-payment" } }); + const adapter = new ExpressAdapter(req); + expect(adapter.getHeader("X-Payment")).toBe("test-payment"); + }); + + it("returns undefined for missing headers", () => { + const req = createMockRequest(); + const adapter = new ExpressAdapter(req); + expect(adapter.getHeader("X-Missing")).toBeUndefined(); + }); + + it("returns first value when header is an array", () => { + const mockReq = { + header: vi.fn().mockReturnValue(["first", "second"]), + method: "GET", + path: "/api/test", + protocol: "https", + headers: { host: "example.com" }, + query: {}, + } as unknown as Request; + const adapter = new ExpressAdapter(mockReq); + expect(adapter.getHeader("X-Multi")).toBe("first"); + }); + }); + + describe("getMethod", () => { + it("returns the HTTP method", () => { + const req = createMockRequest({ method: "POST" }); + const adapter = new ExpressAdapter(req); + expect(adapter.getMethod()).toBe("POST"); + }); + }); + + describe("getPath", () => { + it("returns the pathname", () => { + const req = createMockRequest({ path: "/api/weather" }); + const adapter = new ExpressAdapter(req); + expect(adapter.getPath()).toBe("/api/weather"); + }); + }); + + describe("getUrl", () => { + it("returns the full URL", () => { + const req = createMockRequest({ + path: "/api/test", + protocol: "https", + host: "example.com", + }); + const adapter = new ExpressAdapter(req); + expect(adapter.getUrl()).toBe("https://example.com/api/test"); + }); + + it("returns the full URL including query parameters", () => { + const req = createMockRequest({ + path: "/api/test", + originalUrl: "/api/test?city=NYC&units=metric", + protocol: "https", + host: "example.com", + }); + const adapter = new ExpressAdapter(req); + expect(adapter.getUrl()).toBe("https://example.com/api/test?city=NYC&units=metric"); + }); + }); + + describe("getAcceptHeader", () => { + it("returns Accept header when present", () => { + const req = createMockRequest({ headers: { Accept: "text/html" } }); + const adapter = new ExpressAdapter(req); + expect(adapter.getAcceptHeader()).toBe("text/html"); + }); + + it("returns empty string when missing", () => { + const req = createMockRequest(); + const adapter = new ExpressAdapter(req); + expect(adapter.getAcceptHeader()).toBe(""); + }); + }); + + describe("getUserAgent", () => { + it("returns User-Agent header when present", () => { + const req = createMockRequest({ headers: { "User-Agent": "Mozilla/5.0" } }); + const adapter = new ExpressAdapter(req); + expect(adapter.getUserAgent()).toBe("Mozilla/5.0"); + }); + + it("returns empty string when missing", () => { + const req = createMockRequest(); + const adapter = new ExpressAdapter(req); + expect(adapter.getUserAgent()).toBe(""); + }); + }); + + describe("getQueryParams", () => { + it("returns all query parameters", () => { + const req = createMockRequest({ query: { foo: "bar", baz: "qux" } }); + const adapter = new ExpressAdapter(req); + expect(adapter.getQueryParams()).toEqual({ foo: "bar", baz: "qux" }); + }); + + it("handles multiple values for same key", () => { + const req = createMockRequest({ query: { tag: ["a", "b", "c"] } }); + const adapter = new ExpressAdapter(req); + expect(adapter.getQueryParams()).toEqual({ tag: ["a", "b", "c"] }); + }); + + it("returns empty object when no query params", () => { + const req = createMockRequest({ query: {} }); + const adapter = new ExpressAdapter(req); + expect(adapter.getQueryParams()).toEqual({}); + }); + }); + + describe("getQueryParam", () => { + it("returns single value for single param", () => { + const req = createMockRequest({ query: { city: "NYC" } }); + const adapter = new ExpressAdapter(req); + expect(adapter.getQueryParam("city")).toBe("NYC"); + }); + + it("returns array for multiple values", () => { + const req = createMockRequest({ query: { id: ["1", "2"] } }); + const adapter = new ExpressAdapter(req); + expect(adapter.getQueryParam("id")).toEqual(["1", "2"]); + }); + + it("returns undefined for missing param", () => { + const req = createMockRequest({ query: {} }); + const adapter = new ExpressAdapter(req); + expect(adapter.getQueryParam("missing")).toBeUndefined(); + }); + }); + + describe("getBody", () => { + it("returns parsed body", () => { + const body = { data: "test" }; + const req = createMockRequest({ body }); + const adapter = new ExpressAdapter(req); + expect(adapter.getBody()).toEqual(body); + }); + + it("returns undefined when body is undefined", () => { + const req = createMockRequest(); + const adapter = new ExpressAdapter(req); + expect(adapter.getBody()).toBeUndefined(); + }); + }); +}); diff --git a/typescript/packages/http/express/src/adapter.ts b/typescript/packages/http/express/src/adapter.ts new file mode 100644 index 0000000..8b1eb6f --- /dev/null +++ b/typescript/packages/http/express/src/adapter.ts @@ -0,0 +1,100 @@ +import { HTTPAdapter } from "@x402/core/server"; +import { Request } from "express"; + +/** + * Express adapter implementation + */ +export class ExpressAdapter implements HTTPAdapter { + /** + * Creates a new ExpressAdapter instance. + * + * @param req - The Express request object + */ + constructor(private req: Request) {} + + /** + * Gets a header value from the request. + * + * @param name - The header name + * @returns The header value or undefined + */ + getHeader(name: string): string | undefined { + const value = this.req.header(name); + return Array.isArray(value) ? value[0] : value; + } + + /** + * Gets the HTTP method of the request. + * + * @returns The HTTP method + */ + getMethod(): string { + return this.req.method; + } + + /** + * Gets the path of the request. + * + * @returns The request path + */ + getPath(): string { + return this.req.path; + } + + /** + * Gets the full URL of the request. + * + * @returns The full request URL + */ + getUrl(): string { + return `${this.req.protocol}://${this.req.headers.host}${this.req.originalUrl}`; + } + + /** + * Gets the Accept header from the request. + * + * @returns The Accept header value or empty string + */ + getAcceptHeader(): string { + return this.req.header("Accept") || ""; + } + + /** + * Gets the User-Agent header from the request. + * + * @returns The User-Agent header value or empty string + */ + getUserAgent(): string { + return this.req.header("User-Agent") || ""; + } + + /** + * Gets all query parameters from the request URL. + * + * @returns Record of query parameter key-value pairs + */ + getQueryParams(): Record { + return this.req.query as Record; + } + + /** + * Gets a specific query parameter by name. + * + * @param name - The query parameter name + * @returns The query parameter value(s) or undefined + */ + getQueryParam(name: string): string | string[] | undefined { + const value = this.req.query[name]; + return value as string | string[] | undefined; + } + + /** + * Gets the parsed request body. + * Requires express.json() or express.urlencoded() middleware. + * + * @returns The parsed request body + */ + getBody(): unknown { + return this.req.body; + } +} diff --git a/typescript/packages/http/express/src/index.test.ts b/typescript/packages/http/express/src/index.test.ts new file mode 100644 index 0000000..6d46dd5 --- /dev/null +++ b/typescript/packages/http/express/src/index.test.ts @@ -0,0 +1,667 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { Request, Response } from "express"; +import type { + HTTPProcessResult, + x402HTTPResourceServer, + PaywallProvider, + FacilitatorClient, +} from "@x402/core/server"; +import { + x402ResourceServer, + x402HTTPResourceServer as HTTPResourceServer, +} from "@x402/core/server"; +import type { PaymentPayload, PaymentRequirements, SchemeNetworkServer } from "@x402/core/types"; +import { paymentMiddleware, paymentMiddlewareFromConfig, type SchemeRegistration } from "./index"; + +// --- Test Fixtures --- +const mockRoutes = { + "/api/*": { + accepts: { scheme: "exact", payTo: "0x123", price: "$0.01", network: "eip155:84532" }, + }, +} as const; + +const mockPaymentPayload = { + scheme: "exact", + network: "eip155:84532", + payload: { signature: "0xabc" }, +} as unknown as PaymentPayload; + +const mockPaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + maxAmountRequired: "1000", + payTo: "0x123", +} as unknown as PaymentRequirements; + +// --- Mock setup --- +let mockProcessHTTPRequest: ReturnType; +let mockProcessSettlement: ReturnType; +let mockRegisterPaywallProvider: ReturnType; +let mockRequiresPayment: ReturnType; + +vi.mock("@x402/core/server", () => ({ + x402ResourceServer: vi.fn().mockImplementation(() => ({ + initialize: vi.fn().mockResolvedValue(undefined), + registerExtension: vi.fn(), + register: vi.fn(), + hasExtension: vi.fn().mockReturnValue(false), + })), + x402HTTPResourceServer: vi.fn().mockImplementation((server, routes) => ({ + initialize: vi.fn().mockResolvedValue(undefined), + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + })), +})); + +// --- Mock Factories --- +/** + * Sets up the mock HTTP server to return specified results. + * + * @param processResult - The result to return from processHTTPRequest. + * @param settlementResult - Result to return from processSettlement. + */ +function setupMockHttpServer( + processResult: HTTPProcessResult, + settlementResult: + | { success: true; headers: Record } + | { success: false; errorReason: string } = { success: true, headers: {} }, +): void { + mockProcessHTTPRequest.mockResolvedValue(processResult); + mockProcessSettlement.mockResolvedValue(settlementResult); +} + +/** + * Creates a mock Express Request for testing. + * + * @param options - Configuration options for the mock request. + * @param options.path - The request URL path. + * @param options.method - The HTTP method. + * @param options.headers - Request headers. + * @returns A mock Express Request. + */ +function createMockRequest( + options: { + path?: string; + method?: string; + headers?: Record; + } = {}, +): Request { + const headers = options.headers || {}; + return { + path: options.path || "/api/test", + method: options.method || "GET", + header: vi.fn((name: string) => headers[name.toLowerCase()]), + headers: headers, + } as unknown as Request; +} + +/** + * Creates a mock Express Response for testing. + * + * @returns A mock Express Response with tracking for method calls. + */ +function createMockResponse(): Response & { + _status: number; + _headers: Record; + _body: unknown; + _ended: boolean; +} { + const res = { + _status: 200, + _headers: {} as Record, + _body: undefined as unknown, + _ended: false, + statusCode: 200, + status: vi.fn(function (this: typeof res, code: number) { + this._status = code; + this.statusCode = code; + return this; + }), + setHeader: vi.fn(function (this: typeof res, key: string, value: string) { + this._headers[key] = value; + return this; + }), + json: vi.fn(function (this: typeof res, body: unknown) { + this._body = body; + this._ended = true; + return this; + }), + send: vi.fn(function (this: typeof res, body: unknown) { + this._body = body; + this._ended = true; + return this; + }), + writeHead: vi.fn(function (this: typeof res) { + return this; + }), + write: vi.fn(function () { + return true; + }), + end: vi.fn(function (this: typeof res) { + this._ended = true; + return this; + }), + flushHeaders: vi.fn(), + }; + return res as unknown as Response & typeof res; +} + +// --- Tests --- +describe("paymentMiddleware", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockProcessHTTPRequest = vi.fn(); + mockProcessSettlement = vi.fn(); + mockRegisterPaywallProvider = vi.fn(); + mockRequiresPayment = vi.fn().mockReturnValue(true); + + // Reset the mock implementation + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + }); + + it("calls next() when no-payment-required", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(mockProcessHTTPRequest).toHaveBeenCalled(); + }); + + it("returns 402 HTML for payment-error with isHtml", async () => { + setupMockHttpServer({ + type: "payment-error", + response: { + status: 402, + body: "Paywall", + headers: {}, + isHtml: true, + }, + }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(402); + expect(res.send).toHaveBeenCalledWith("Paywall"); + }); + + it("returns 402 JSON for payment-error", async () => { + setupMockHttpServer({ + type: "payment-error", + response: { + status: 402, + body: { error: "Payment required" }, + headers: {}, + isHtml: false, + }, + }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(402); + expect(res.json).toHaveBeenCalledWith({ error: "Payment required" }); + }); + + it("sets custom headers from payment-error response", async () => { + setupMockHttpServer({ + type: "payment-error", + response: { + status: 402, + body: { error: "Payment required" }, + headers: { "X-Custom-Header": "custom-value" }, + isHtml: false, + }, + }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(res.setHeader).toHaveBeenCalledWith("X-Custom-Header", "custom-value"); + }); + + it("settles and returns response for payment-verified with successful handler", async () => { + setupMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: true, headers: { "PAYMENT-RESPONSE": "settled" } }, + ); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(() => { + // Simulate handler calling res.end() + res.statusCode = 200; + res.end(); + }); + + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(mockProcessSettlement).toHaveBeenCalledWith( + mockPaymentPayload, + mockPaymentRequirements, + undefined, + ); + expect(res.setHeader).toHaveBeenCalledWith("PAYMENT-RESPONSE", "settled"); + }); + + it("skips settlement when handler returns >= 400", async () => { + setupMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: true, headers: { "PAYMENT-RESPONSE": "settled" } }, + ); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(() => { + // Simulate handler returning error + res.statusCode = 500; + res.end(); + }); + + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(mockProcessSettlement).not.toHaveBeenCalled(); + }); + + it("returns 402 when settlement throws error", async () => { + setupMockHttpServer({ + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }); + mockProcessSettlement.mockRejectedValue(new Error("Settlement rejected")); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(() => { + res.statusCode = 200; + res.end(); + }); + + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(402); + expect(res.json).toHaveBeenCalledWith({ + error: "Settlement failed", + details: "Settlement rejected", + }); + }); + + it("returns 402 when settlement returns success: false", async () => { + setupMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: false, errorReason: "Insufficient funds" }, + ); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(() => { + res.statusCode = 200; + res.end(); + }); + + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(402); + expect(res.json).toHaveBeenCalledWith({ + error: "Settlement failed", + details: "Insufficient funds", + }); + }); + + it("passes paywallConfig to processHTTPRequest", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + const paywallConfig = { appName: "test-app" }; + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + paywallConfig, + undefined, + false, + ); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith(expect.anything(), paywallConfig); + }); + + it("registers custom paywall provider", () => { + setupMockHttpServer({ type: "no-payment-required" }); + const paywall: PaywallProvider = { generateHtml: vi.fn() }; + + paymentMiddleware(mockRoutes, {} as unknown as x402ResourceServer, undefined, paywall, false); + + expect(mockRegisterPaywallProvider).toHaveBeenCalledWith(paywall); + }); +}); + +describe("paymentMiddlewareFromConfig", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockProcessHTTPRequest = vi.fn(); + mockProcessSettlement = vi.fn(); + mockRegisterPaywallProvider = vi.fn(); + mockRequiresPayment = vi.fn().mockReturnValue(true); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + initialize: vi.fn().mockResolvedValue(undefined), + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + + vi.mocked(x402ResourceServer).mockImplementation( + () => + ({ + initialize: vi.fn().mockResolvedValue(undefined), + registerExtension: vi.fn(), + register: vi.fn(), + }) as unknown as x402ResourceServer, + ); + }); + + it("creates x402ResourceServer with facilitator clients", () => { + setupMockHttpServer({ type: "no-payment-required" }); + const facilitator = { verify: vi.fn(), settle: vi.fn() } as unknown as FacilitatorClient; + + paymentMiddlewareFromConfig(mockRoutes, facilitator); + + expect(x402ResourceServer).toHaveBeenCalledWith(facilitator); + }); + + it("registers scheme servers for each network", () => { + setupMockHttpServer({ type: "no-payment-required" }); + const schemeServer = { verify: vi.fn(), settle: vi.fn() } as unknown as SchemeNetworkServer; + const schemes: SchemeRegistration[] = [ + { network: "eip155:84532", server: schemeServer }, + { network: "eip155:8453", server: schemeServer }, + ]; + + paymentMiddlewareFromConfig(mockRoutes, undefined, schemes); + + const serverInstance = vi.mocked(x402ResourceServer).mock.results[0].value; + expect(serverInstance.register).toHaveBeenCalledTimes(2); + expect(serverInstance.register).toHaveBeenCalledWith("eip155:84532", schemeServer); + expect(serverInstance.register).toHaveBeenCalledWith("eip155:8453", schemeServer); + }); + + it("returns a working middleware function", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const middleware = paymentMiddlewareFromConfig(mockRoutes); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); +}); + +describe("ExpressAdapter", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockProcessHTTPRequest = vi.fn(); + mockProcessSettlement = vi.fn(); + mockRegisterPaywallProvider = vi.fn(); + mockRequiresPayment = vi.fn().mockReturnValue(true); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + }); + + it("extracts path and method from request", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest({ path: "/api/weather", method: "POST" }); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + path: "/api/weather", + method: "POST", + }), + undefined, + ); + }); + + it("extracts x-payment header", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest({ headers: { "x-payment": "payment-data" } }); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: "payment-data", + }), + undefined, + ); + }); + + it("extracts payment-signature header (v2)", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest({ headers: { "payment-signature": "sig-data" } }); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: "sig-data", + }), + undefined, + ); + }); + + it("prefers payment-signature over x-payment", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest({ + headers: { "payment-signature": "sig-data", "x-payment": "x-payment-data" }, + }); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: "sig-data", + }), + undefined, + ); + }); + + it("returns undefined paymentHeader when no payment headers present", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: undefined, + }), + undefined, + ); + }); +}); diff --git a/typescript/packages/http/express/src/index.ts b/typescript/packages/http/express/src/index.ts new file mode 100644 index 0000000..6a12ce9 --- /dev/null +++ b/typescript/packages/http/express/src/index.ts @@ -0,0 +1,391 @@ +import { + HTTPRequestContext, + PaywallConfig, + PaywallProvider, + x402HTTPResourceServer, + x402ResourceServer, + RoutesConfig, + FacilitatorClient, +} from "@x402/core/server"; +import { SchemeNetworkServer, Network } from "@x402/core/types"; +import { NextFunction, Request, Response } from "express"; +import { ExpressAdapter } from "./adapter"; + +/** + * Check if any routes in the configuration declare bazaar extensions + * + * @param routes - Route configuration + * @returns True if any route has extensions.bazaar defined + */ +function checkIfBazaarNeeded(routes: RoutesConfig): boolean { + // Handle single route config + if ("accepts" in routes) { + return !!(routes.extensions && "bazaar" in routes.extensions); + } + + // Handle multiple routes + return Object.values(routes).some(routeConfig => { + return !!(routeConfig.extensions && "bazaar" in routeConfig.extensions); + }); +} + +/** + * Configuration for registering a payment scheme with a specific network + */ +export interface SchemeRegistration { + /** + * The network identifier (e.g., 'eip155:84532', 'solana:mainnet') + */ + network: Network; + + /** + * The scheme server implementation for this network + */ + server: SchemeNetworkServer; +} + +/** + * Express payment middleware for x402 protocol (direct HTTP server instance). + * + * Use this when you need to configure HTTP-level hooks. + * + * @param httpServer - Pre-configured x402HTTPResourceServer instance + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @returns Express middleware handler + * + * @example + * ```typescript + * import { paymentMiddlewareFromHTTPServer, x402ResourceServer, x402HTTPResourceServer } from "@x402/express"; + * + * const resourceServer = new x402ResourceServer(facilitatorClient) + * .register(NETWORK, new ExactEvmScheme()) + * + * const httpServer = new x402HTTPResourceServer(resourceServer, routes) + * .onProtectedRequest(requestHook); + * + * app.use(paymentMiddlewareFromHTTPServer(httpServer)); + * ``` + */ +export function paymentMiddlewareFromHTTPServer( + httpServer: x402HTTPResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +) { + // Register custom paywall provider if provided + if (paywall) { + httpServer.registerPaywallProvider(paywall); + } + + // Store initialization promise (not the result) + // httpServer.initialize() fetches facilitator support and validates routes + let initPromise: Promise | null = syncFacilitatorOnStart ? httpServer.initialize() : null; + + // Dynamically register bazaar extension if routes declare it and not already registered + // Skip if pre-registered (e.g., in serverless environments where static imports are used) + let bazaarPromise: Promise | null = null; + if (checkIfBazaarNeeded(httpServer.routes) && !httpServer.server.hasExtension("bazaar")) { + bazaarPromise = import("@x402/extensions/bazaar") + .then(({ bazaarResourceServerExtension }) => { + httpServer.server.registerExtension(bazaarResourceServerExtension); + }) + .catch(err => { + console.error("Failed to load bazaar extension:", err); + }); + } + + return async (req: Request, res: Response, next: NextFunction) => { + // Create adapter and context + const adapter = new ExpressAdapter(req); + const context: HTTPRequestContext = { + adapter, + path: req.path, + method: req.method, + paymentHeader: adapter.getHeader("payment-signature") || adapter.getHeader("x-payment"), + }; + + // Check if route requires payment before initializing facilitator + if (!httpServer.requiresPayment(context)) { + return next(); + } + + // Only initialize when processing a protected route + if (initPromise) { + await initPromise; + initPromise = null; // Clear after first await + } + + // Await bazaar extension loading if needed + if (bazaarPromise) { + await bazaarPromise; + bazaarPromise = null; + } + + // Process payment requirement check + const result = await httpServer.processHTTPRequest(context, paywallConfig); + + // Handle the different result types + switch (result.type) { + case "no-payment-required": + // No payment needed, proceed directly to the route handler + return next(); + + case "payment-error": + // Payment required but not provided or invalid + const { response } = result; + res.status(response.status); + Object.entries(response.headers).forEach(([key, value]) => { + res.setHeader(key, value); + }); + if (response.isHtml) { + res.send(response.body); + } else { + res.json(response.body || {}); + } + return; + + case "payment-verified": + // Payment is valid, need to wrap response for settlement + const { paymentPayload, paymentRequirements, declaredExtensions } = result; + + // Intercept and buffer all core methods that can commit response to client + const originalWriteHead = res.writeHead.bind(res); + const originalWrite = res.write.bind(res); + const originalEnd = res.end.bind(res); + const originalFlushHeaders = res.flushHeaders.bind(res); + + type BufferedCall = + | ["writeHead", Parameters] + | ["write", Parameters] + | ["end", Parameters] + | ["flushHeaders", []]; + let bufferedCalls: BufferedCall[] = []; + let settled = false; + + // Create a promise that resolves when the handler finishes and calls res.end() + let endCalled: () => void; + const endPromise = new Promise(resolve => { + endCalled = resolve; + }); + + res.writeHead = function (...args: Parameters) { + if (!settled) { + bufferedCalls.push(["writeHead", args]); + return res; + } + return originalWriteHead(...args); + } as typeof originalWriteHead; + + res.write = function (...args: Parameters) { + if (!settled) { + bufferedCalls.push(["write", args]); + return true; + } + return originalWrite(...args); + } as typeof originalWrite; + + res.end = function (...args: Parameters) { + if (!settled) { + bufferedCalls.push(["end", args]); + // Signal that the handler has finished + endCalled(); + return res; + } + return originalEnd(...args); + } as typeof originalEnd; + + res.flushHeaders = function () { + if (!settled) { + bufferedCalls.push(["flushHeaders", []]); + return; + } + return originalFlushHeaders(); + }; + + // Proceed to the next middleware or route handler + next(); + + // Wait for the handler to actually call res.end() before checking status + await endPromise; + + // If the response from the protected route is >= 400, do not settle payment + if (res.statusCode >= 400) { + settled = true; + res.writeHead = originalWriteHead; + res.write = originalWrite; + res.end = originalEnd; + res.flushHeaders = originalFlushHeaders; + // Replay all buffered calls in order + for (const [method, args] of bufferedCalls) { + if (method === "writeHead") + originalWriteHead(...(args as Parameters)); + else if (method === "write") + originalWrite(...(args as Parameters)); + else if (method === "end") originalEnd(...(args as Parameters)); + else if (method === "flushHeaders") originalFlushHeaders(); + } + bufferedCalls = []; + return; + } + + try { + const settleResult = await httpServer.processSettlement( + paymentPayload, + paymentRequirements, + declaredExtensions, + ); + + // If settlement fails, return an error and do not send the buffered response + if (!settleResult.success) { + bufferedCalls = []; + res.status(402).json({ + error: "Settlement failed", + details: settleResult.errorReason, + }); + return; + } + + // Settlement succeeded - add headers to response + Object.entries(settleResult.headers).forEach(([key, value]) => { + res.setHeader(key, value); + }); + } catch (error) { + console.error(error); + // If settlement fails, don't send the buffered response + bufferedCalls = []; + res.status(402).json({ + error: "Settlement failed", + details: error instanceof Error ? error.message : "Unknown error", + }); + return; + } finally { + settled = true; + res.writeHead = originalWriteHead; + res.write = originalWrite; + res.end = originalEnd; + res.flushHeaders = originalFlushHeaders; + + // Replay all buffered calls in order + for (const [method, args] of bufferedCalls) { + if (method === "writeHead") + originalWriteHead(...(args as Parameters)); + else if (method === "write") + originalWrite(...(args as Parameters)); + else if (method === "end") originalEnd(...(args as Parameters)); + else if (method === "flushHeaders") originalFlushHeaders(); + } + bufferedCalls = []; + } + return; + } + }; +} + +/** + * Express payment middleware for x402 protocol (direct server instance). + * + * Use this when you want to pass a pre-configured x402ResourceServer instance. + * This provides more flexibility for testing, custom configuration, and reusing + * server instances across multiple middlewares. + * + * @param routes - Route configurations for protected endpoints + * @param server - Pre-configured x402ResourceServer instance + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @returns Express middleware handler + * + * @example + * ```typescript + * import { paymentMiddleware } from "@x402/express"; + * + * const server = new x402ResourceServer(myFacilitatorClient) + * .register(NETWORK, new ExactEvmScheme()); + * + * app.use(paymentMiddleware(routes, server, paywallConfig)); + * ``` + */ +export function paymentMiddleware( + routes: RoutesConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +) { + // Create the x402 HTTP server instance with the resource server + const httpServer = new x402HTTPResourceServer(server, routes); + + return paymentMiddlewareFromHTTPServer( + httpServer, + paywallConfig, + paywall, + syncFacilitatorOnStart, + ); +} + +/** + * Express payment middleware for x402 protocol (configuration-based). + * + * Use this when you want to quickly set up middleware with simple configuration. + * This function creates and configures the x402ResourceServer internally. + * + * @param routes - Route configurations for protected endpoints + * @param facilitatorClients - Optional facilitator client(s) for payment processing + * @param schemes - Optional array of scheme registrations for server-side payment processing + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @returns Express middleware handler + * + * @example + * ```typescript + * import { paymentMiddlewareFromConfig } from "@x402/express"; + * + * app.use(paymentMiddlewareFromConfig( + * routes, + * myFacilitatorClient, + * [{ network: "eip155:8453", server: evmSchemeServer }], + * paywallConfig + * )); + * ``` + */ +export function paymentMiddlewareFromConfig( + routes: RoutesConfig, + facilitatorClients?: FacilitatorClient | FacilitatorClient[], + schemes?: SchemeRegistration[], + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +) { + const ResourceServer = new x402ResourceServer(facilitatorClients); + + if (schemes) { + schemes.forEach(({ network, server: schemeServer }) => { + ResourceServer.register(network, schemeServer); + }); + } + + // Use the direct paymentMiddleware with the configured server + // Note: paymentMiddleware handles dynamic bazaar registration + return paymentMiddleware(routes, ResourceServer, paywallConfig, paywall, syncFacilitatorOnStart); +} + +export { x402ResourceServer, x402HTTPResourceServer } from "@x402/core/server"; + +export type { + PaymentRequired, + PaymentRequirements, + PaymentPayload, + Network, + SchemeNetworkServer, +} from "@x402/core/types"; + +export type { PaywallProvider, PaywallConfig } from "@x402/core/server"; + +export { RouteConfigurationError } from "@x402/core/server"; + +export type { RouteValidationError } from "@x402/core/server"; + +export { ExpressAdapter } from "./adapter"; diff --git a/typescript/packages/http/express/tsconfig.json b/typescript/packages/http/express/tsconfig.json new file mode 100644 index 0000000..1b119d3 --- /dev/null +++ b/typescript/packages/http/express/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "allowJs": false, + "checkJs": false + }, + "include": ["src"] +} diff --git a/typescript/packages/http/express/tsup.config.ts b/typescript/packages/http/express/tsup.config.ts new file mode 100644 index 0000000..f8699f9 --- /dev/null +++ b/typescript/packages/http/express/tsup.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from "tsup"; + +const baseConfig = { + entry: { + index: "src/index.ts", + }, + dts: { + resolve: true, + }, + sourcemap: true, + target: "node16", +}; + +export default defineConfig([ + { + ...baseConfig, + format: "esm", + outDir: "dist/esm", + clean: true, + }, + { + ...baseConfig, + format: "cjs", + outDir: "dist/cjs", + clean: false, + }, +]); diff --git a/typescript/packages/http/express/vitest.config.ts b/typescript/packages/http/express/vitest.config.ts new file mode 100644 index 0000000..156f8c9 --- /dev/null +++ b/typescript/packages/http/express/vitest.config.ts @@ -0,0 +1,10 @@ +import { loadEnv } from "vite"; +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ""), + }, + plugins: [tsconfigPaths({ projects: ["."] })], +})); diff --git a/typescript/packages/http/fetch/.prettierignore b/typescript/packages/http/fetch/.prettierignore new file mode 100644 index 0000000..5bd240b --- /dev/null +++ b/typescript/packages/http/fetch/.prettierignore @@ -0,0 +1,8 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +src/client +**/**/*.json +*.md \ No newline at end of file diff --git a/typescript/packages/http/fetch/.prettierrc b/typescript/packages/http/fetch/.prettierrc new file mode 100644 index 0000000..ffb416b --- /dev/null +++ b/typescript/packages/http/fetch/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/packages/http/fetch/CHANGELOG.md b/typescript/packages/http/fetch/CHANGELOG.md new file mode 100644 index 0000000..c3c08f1 --- /dev/null +++ b/typescript/packages/http/fetch/CHANGELOG.md @@ -0,0 +1,21 @@ +# @x402/fetch Changelog + +## 2.3.0 + +### Minor Changes + +- 51b8445: Bumped @x402/core dependency to 2.3.0 + +### Patch Changes + +- Updated dependencies [51b8445] +- Updated dependencies [51b8445] + - @x402/core@2.3.0 + +## 2.0.0 + +- Implements x402 2.0.0 for the TypeScript SDK. + +## 1.0.0 + +- Implements x402 1.0.0 for the TypeScript SDK. diff --git a/typescript/packages/http/fetch/README.md b/typescript/packages/http/fetch/README.md new file mode 100644 index 0000000..d5eebbe --- /dev/null +++ b/typescript/packages/http/fetch/README.md @@ -0,0 +1,196 @@ +# x402-fetch + +A utility package that extends the native `fetch` API to automatically handle 402 Payment Required responses using the x402 payment protocol v2. This package enables seamless integration of payment functionality into your applications when making HTTP requests. + +## Installation + +```bash +pnpm install @x402/fetch +``` + +## Quick Start + +```typescript +import { wrapFetchWithPaymentFromConfig } from "@x402/fetch"; +import { ExactEvmScheme } from "@x402/evm"; +import { privateKeyToAccount } from "viem/accounts"; + +// Create an account +const account = privateKeyToAccount("0xYourPrivateKey"); + +// Wrap the fetch function with payment handling +const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, { + schemes: [ + { + network: "eip155:8453", // Base Sepolia + client: new ExactEvmScheme(account), + }, + ], +}); + +// Make a request that may require payment +const response = await fetchWithPayment("https://api.example.com/paid-endpoint", { + method: "GET", +}); + +const data = await response.json(); +``` + +## API + +### `wrapFetchWithPayment(fetch, client)` + +Wraps the native fetch API to handle 402 Payment Required responses automatically. + +#### Parameters + +- `fetch`: The fetch function to wrap (typically `globalThis.fetch`) +- `client`: An x402Client instance with registered payment schemes + +### `wrapFetchWithPaymentFromConfig(fetch, config)` + +Convenience wrapper that creates an x402Client from a configuration object. + +#### Parameters + +- `fetch`: The fetch function to wrap (typically `globalThis.fetch`) +- `config`: Configuration object with the following properties: + - `schemes`: Array of scheme registrations, each containing: + - `network`: Network identifier (e.g., 'eip155:8453', 'solana:mainnet', 'eip155:*' for wildcards) + - `client`: The scheme client implementation (e.g., `ExactEvmScheme`, `ExactSvmScheme`) + - `x402Version`: Optional protocol version (defaults to 2, set to 1 for legacy support) + - `paymentRequirementsSelector`: Optional function to select payment requirements from multiple options + +#### Returns + +A wrapped fetch function that automatically handles 402 responses by: +1. Making the initial request +2. If a 402 response is received, parsing the payment requirements +3. Creating a payment header using the configured scheme client +4. Retrying the request with the payment header + +## Examples + +### Basic Usage with EVM + +```typescript +import { config } from "dotenv"; +import { wrapFetchWithPaymentFromConfig, decodePaymentResponseHeader } from "@x402/fetch"; +import { privateKeyToAccount } from "viem/accounts"; +import { ExactEvmScheme } from "@x402/evm"; + +config(); + +const { EVM_PRIVATE_KEY, API_URL } = process.env; + +const account = privateKeyToAccount(EVM_PRIVATE_KEY as `0x${string}`); + +const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, { + schemes: [ + { + network: "eip155:*", // Support all EVM chains + client: new ExactEvmScheme(account), + }, + ], +}); + +// Make a request to a paid API endpoint +fetchWithPayment(API_URL, { + method: "GET", +}) + .then(async response => { + const data = await response.json(); + + // Optionally decode the payment response header + const paymentResponse = response.headers.get("PAYMENT-RESPONSE"); + if (paymentResponse) { + const decoded = decodePaymentResponseHeader(paymentResponse); + console.log("Payment details:", decoded); + } + + console.log("Response data:", data); + }) + .catch(error => { + console.error(error); + }); +``` + +### Using Builder Pattern + +For more control, you can use the builder pattern to register multiple schemes: + +```typescript +import { wrapFetchWithPayment, x402Client } from "@x402/fetch"; +import { ExactEvmScheme } from "@x402/evm/exact/client"; +import { ExactSvmScheme } from "@x402/svm/exact/client"; +import { privateKeyToAccount } from "viem/accounts"; +import { createKeyPairSignerFromBytes } from "@solana/kit"; +import { base58 } from "@scure/base"; + +// Create signers +const evmSigner = privateKeyToAccount("0xYourPrivateKey"); +const svmSigner = await createKeyPairSignerFromBytes(base58.decode("YourSvmPrivateKey")); + +// Build client with multiple schemes +const client = new x402Client() + .register("eip155:*", new ExactEvmScheme(evmSigner)) + .register("solana:*", new ExactSvmScheme(svmSigner)); + +// Wrap fetch with the client +const fetchWithPayment = wrapFetchWithPayment(fetch, client); +``` + +### Multi-Chain Support + +```typescript +import { wrapFetchWithPaymentFromConfig } from "@x402/fetch"; +import { ExactEvmScheme } from "@x402/evm"; +import { ExactSvmScheme } from "@x402/svm"; + +const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, { + schemes: [ + // EVM chains + { + network: "eip155:8453", // Base Sepolia + client: new ExactEvmScheme(evmAccount), + }, + // SVM chains + { + network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", // Solana devnet + client: new ExactSvmScheme(svmSigner), + }, + ], +}); +``` + +### Custom Payment Requirements Selector + +```typescript +import { wrapFetchWithPaymentFromConfig, type SelectPaymentRequirements } from "@x402/fetch"; +import { ExactEvmScheme } from "@x402/evm"; + +// Custom selector that prefers the cheapest option +const selectCheapestOption: SelectPaymentRequirements = (version, accepts) => { + if (!accepts || accepts.length === 0) { + throw new Error("No payment options available"); + } + + // Sort by value and return the cheapest + const sorted = [...accepts].sort((a, b) => + BigInt(a.value) - BigInt(b.value) + ); + + return sorted[0]; +}; + +const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, { + schemes: [ + { + network: "eip155:8453", + client: new ExactEvmScheme(account), + }, + ], + paymentRequirementsSelector: selectCheapestOption, +}); +``` + diff --git a/typescript/packages/http/fetch/eslint.config.js b/typescript/packages/http/fetch/eslint.config.js new file mode 100644 index 0000000..7b03c42 --- /dev/null +++ b/typescript/packages/http/fetch/eslint.config.js @@ -0,0 +1,76 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + RequestInfo: "readonly", + RequestInit: "readonly", + Response: "readonly", + Headers: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/typescript/packages/http/fetch/package.json b/typescript/packages/http/fetch/package.json new file mode 100644 index 0000000..1b18c88 --- /dev/null +++ b/typescript/packages/http/fetch/package.json @@ -0,0 +1,60 @@ +{ + "name": "@x402/fetch", + "version": "2.3.0", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "start": "tsx --env-file=.env index.ts", + "test": "vitest run", + "test:watch": "vitest", + "build": "tsup", + "watch": "tsc --watch", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "keywords": [], + "license": "Apache-2.0", + "author": "Coinbase Inc.", + "repository": "https://github.com/coinbase/x402", + "description": "x402 Payment Protocol Fetch Extension", + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/node": "^22.13.4", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsup": "^8.4.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vite": "^6.2.6", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.5" + }, + "dependencies": { + "@x402/core": "workspace:~", + "viem": "^2.39.3", + "zod": "^3.24.2" + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.mts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + } + }, + "files": [ + "dist" + ] +} diff --git a/typescript/packages/http/fetch/src/index.test.ts b/typescript/packages/http/fetch/src/index.test.ts new file mode 100644 index 0000000..f59139c --- /dev/null +++ b/typescript/packages/http/fetch/src/index.test.ts @@ -0,0 +1,483 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { wrapFetchWithPayment, wrapFetchWithPaymentFromConfig } from "./index"; +import type { x402Client, x402HTTPClient, x402ClientConfig } from "@x402/core/client"; +import type { PaymentPayload, PaymentRequired, PaymentRequirements } from "@x402/core/types"; + +// Mock the @x402/core/client module +vi.mock("@x402/core/client", () => { + const MockX402HTTPClient = vi.fn(); + MockX402HTTPClient.prototype.getPaymentRequiredResponse = vi.fn(); + MockX402HTTPClient.prototype.encodePaymentSignatureHeader = vi.fn(); + MockX402HTTPClient.prototype.handlePaymentRequired = vi.fn(); + + const MockX402Client = vi.fn() as ReturnType & { + fromConfig: ReturnType; + }; + MockX402Client.prototype.createPaymentPayload = vi.fn(); + MockX402Client.fromConfig = vi.fn(); + + return { + x402HTTPClient: MockX402HTTPClient, + x402Client: MockX402Client, + }; +}); + +describe("wrapFetchWithPayment()", () => { + let mockFetch: ReturnType; + let mockClient: x402Client; + let wrappedFetch: ReturnType; + + const validPaymentRequired: PaymentRequired = { + x402Version: 2, + resource: { + url: "https://api.example.com/resource", + description: "Test payment", + mimeType: "application/json", + }, + accepts: [ + { + scheme: "exact", + network: "eip155:84532" as const, + amount: "1000000", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + payTo: "0x1234567890123456789012345678901234567890", + maxTimeoutSeconds: 300, + extra: {}, + } as PaymentRequirements, + ], + }; + + const validPaymentPayload: PaymentPayload = { + x402Version: 2, + resource: validPaymentRequired.resource, + accepted: validPaymentRequired.accepts[0], + payload: { signature: "0xmocksignature" }, + }; + + const createResponse = ( + status: number, + data?: unknown, + headers?: Record, + ): Response => { + return new Response(data ? JSON.stringify(data) : null, { + status, + statusText: status === 402 ? "Payment Required" : "OK", + headers: new Headers(headers), + }); + }; + + beforeEach(async () => { + vi.resetAllMocks(); + + mockFetch = vi.fn(); + + // Create mock client + const { x402Client: MockX402Client, x402HTTPClient: MockX402HTTPClient } = await import( + "@x402/core/client" + ); + + mockClient = new MockX402Client() as unknown as x402Client; + + // Setup default mock implementations + (mockClient.createPaymentPayload as ReturnType).mockResolvedValue( + validPaymentPayload, + ); + + ( + MockX402HTTPClient.prototype.getPaymentRequiredResponse as ReturnType + ).mockReturnValue(validPaymentRequired); + ( + MockX402HTTPClient.prototype.encodePaymentSignatureHeader as ReturnType + ).mockReturnValue({ + "PAYMENT-SIGNATURE": "encoded-payment-header", + }); + ( + MockX402HTTPClient.prototype.handlePaymentRequired as ReturnType + ).mockResolvedValue(null); + + wrappedFetch = wrapFetchWithPayment(mockFetch, mockClient); + }); + + it("should return the original response for non-402 status codes", async () => { + const successResponse = createResponse(200, { data: "success" }); + mockFetch.mockResolvedValue(successResponse); + + const result = await wrappedFetch("https://api.example.com", { method: "GET" }); + + expect(result).toBe(successResponse); + expect(mockFetch).toHaveBeenCalledTimes(1); + const request = mockFetch.mock.calls[0][0] as Request; + expect(request.url).toBe("https://api.example.com/"); + expect(request.method).toBe("GET"); + }); + + it("should handle 402 errors and retry with payment header", async () => { + const { x402HTTPClient: MockX402HTTPClient } = await import("@x402/core/client"); + const successResponse = createResponse(200, { data: "success" }); + + mockFetch + .mockResolvedValueOnce( + createResponse(402, validPaymentRequired, { "PAYMENT-REQUIRED": "encoded-header" }), + ) + .mockResolvedValueOnce(successResponse); + + const result = await wrappedFetch("https://api.example.com", { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + + expect(result).toBe(successResponse); + expect(MockX402HTTPClient.prototype.getPaymentRequiredResponse).toHaveBeenCalled(); + expect(mockClient.createPaymentPayload).toHaveBeenCalledWith(validPaymentRequired); + expect(MockX402HTTPClient.prototype.encodePaymentSignatureHeader).toHaveBeenCalledWith( + validPaymentPayload, + ); + expect(mockFetch).toHaveBeenCalledTimes(2); + + // Verify the retry request is a Request object with correct headers + const retryCall = mockFetch.mock.calls[1]; + const retryRequest = retryCall[0] as Request; + expect(retryRequest.headers.get("Content-Type")).toBe("application/json"); + expect(retryRequest.headers.get("PAYMENT-SIGNATURE")).toBe("encoded-payment-header"); + expect(retryRequest.headers.get("Access-Control-Expose-Headers")).toBe( + "PAYMENT-RESPONSE,X-PAYMENT-RESPONSE", + ); + }); + + it("should not retry if already retried (PAYMENT-SIGNATURE header present)", async () => { + mockFetch.mockResolvedValue(createResponse(402, validPaymentRequired)); + + await expect( + wrappedFetch("https://api.example.com", { + method: "GET", + headers: { "PAYMENT-SIGNATURE": "already-present" }, + }), + ).rejects.toThrow("Payment already attempted"); + }); + + it("should not retry if already retried (X-PAYMENT header present)", async () => { + mockFetch.mockResolvedValue(createResponse(402, validPaymentRequired)); + + await expect( + wrappedFetch("https://api.example.com", { + method: "GET", + headers: { "X-PAYMENT": "already-present" }, + }), + ).rejects.toThrow("Payment already attempted"); + }); + + it("should allow optional fetch request config", async () => { + const successResponse = createResponse(200, { data: "success" }); + + mockFetch.mockResolvedValueOnce(createResponse(402, validPaymentRequired)); + mockFetch.mockResolvedValueOnce(successResponse); + + await expect(wrappedFetch("https://api.example.com")).resolves.toBe(successResponse); + }); + + it("should reject with descriptive error if payment requirements parsing fails", async () => { + const { x402HTTPClient: MockX402HTTPClient } = await import("@x402/core/client"); + ( + MockX402HTTPClient.prototype.getPaymentRequiredResponse as ReturnType + ).mockImplementation(() => { + throw new Error("Invalid payment header format"); + }); + + mockFetch.mockResolvedValue(createResponse(402, undefined)); + + await expect(wrappedFetch("https://api.example.com", { method: "GET" })).rejects.toThrow( + "Failed to parse payment requirements: Invalid payment header format", + ); + }); + + it("should reject with descriptive error if payment payload creation fails", async () => { + const paymentError = new Error("Insufficient funds"); + (mockClient.createPaymentPayload as ReturnType).mockRejectedValue(paymentError); + + mockFetch.mockResolvedValue(createResponse(402, validPaymentRequired)); + + await expect(wrappedFetch("https://api.example.com", { method: "GET" })).rejects.toThrow( + "Failed to create payment payload: Insufficient funds", + ); + }); + + it("should reject with generic error message for unknown parsing errors", async () => { + const { x402HTTPClient: MockX402HTTPClient } = await import("@x402/core/client"); + ( + MockX402HTTPClient.prototype.getPaymentRequiredResponse as ReturnType + ).mockImplementation(() => { + throw "String error"; // Non-Error thrown + }); + + mockFetch.mockResolvedValue(createResponse(402, undefined)); + + await expect(wrappedFetch("https://api.example.com", { method: "GET" })).rejects.toThrow( + "Failed to parse payment requirements: Unknown error", + ); + }); + + it("should reject with generic error message for unknown payment creation errors", async () => { + (mockClient.createPaymentPayload as ReturnType).mockRejectedValue("String error"); + + mockFetch.mockResolvedValue(createResponse(402, validPaymentRequired)); + + await expect(wrappedFetch("https://api.example.com", { method: "GET" })).rejects.toThrow( + "Failed to create payment payload: Unknown error", + ); + }); + + it("should handle v1 payment responses from body", async () => { + const { x402HTTPClient: MockX402HTTPClient } = await import("@x402/core/client"); + const successResponse = createResponse(200, { data: "success" }); + + const v1PaymentRequired: PaymentRequired = { + ...validPaymentRequired, + x402Version: 1, + }; + + const v1PaymentPayload: PaymentPayload = { + ...validPaymentPayload, + x402Version: 1, + }; + + ( + MockX402HTTPClient.prototype.getPaymentRequiredResponse as ReturnType + ).mockReturnValue(v1PaymentRequired); + ( + MockX402HTTPClient.prototype.encodePaymentSignatureHeader as ReturnType + ).mockReturnValue({ + "X-PAYMENT": "v1-payment-header", + }); + (mockClient.createPaymentPayload as ReturnType).mockResolvedValue( + v1PaymentPayload, + ); + + mockFetch.mockResolvedValueOnce(createResponse(402, v1PaymentRequired)); + mockFetch.mockResolvedValueOnce(successResponse); + + const result = await wrappedFetch("https://api.example.com", { method: "GET" }); + + expect(result).toBe(successResponse); + expect(MockX402HTTPClient.prototype.encodePaymentSignatureHeader).toHaveBeenCalledWith( + v1PaymentPayload, + ); + + // Verify v1 payment header was set correctly + const retryCall = mockFetch.mock.calls[1]; + const retryRequest = retryCall[0] as Request; + expect(retryRequest.headers.get("X-PAYMENT")).toBe("v1-payment-header"); + }); + + it("should propagate retry errors", async () => { + const retryError = new Error("Network error on retry"); + + mockFetch.mockResolvedValueOnce(createResponse(402, validPaymentRequired)); + mockFetch.mockRejectedValueOnce(retryError); + + await expect(wrappedFetch("https://api.example.com", { method: "GET" })).rejects.toBe( + retryError, + ); + }); + + it("should set Access-Control-Expose-Headers on retry request", async () => { + const successResponse = createResponse(200, { data: "success" }); + + mockFetch.mockResolvedValueOnce(createResponse(402, validPaymentRequired)); + mockFetch.mockResolvedValueOnce(successResponse); + + await wrappedFetch("https://api.example.com", { method: "GET" }); + + const retryCall = mockFetch.mock.calls[1]; + const retryRequest = retryCall[0] as Request; + expect(retryRequest.headers.get("Access-Control-Expose-Headers")).toBe( + "PAYMENT-RESPONSE,X-PAYMENT-RESPONSE", + ); + }); + + it("should preserve Headers object during retry", async () => { + const successResponse = createResponse(200, { data: "success" }); + + mockFetch + .mockResolvedValueOnce(createResponse(402, validPaymentRequired)) + .mockResolvedValueOnce(successResponse); + + // Use a Headers object instead of a plain object + const originalHeaders = new Headers({ + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + "Custom-Header": "custom-value", + }); + + await wrappedFetch("https://api.example.com", { + method: "POST", + headers: originalHeaders, + }); + + // Verify the retry request includes all original headers plus payment headers + const retryCall = mockFetch.mock.calls[1]; + const retryRequest = retryCall[0] as Request; + + // Check payment headers were added + expect(retryRequest.headers.get("PAYMENT-SIGNATURE")).toBe("encoded-payment-header"); + expect(retryRequest.headers.get("Access-Control-Expose-Headers")).toBe( + "PAYMENT-RESPONSE,X-PAYMENT-RESPONSE", + ); + + // Check original headers were preserved (this would fail before the fix) + expect(retryRequest.headers.get("Content-Type")).toBe("application/json"); + expect(retryRequest.headers.get("Accept")).toBe("application/json, text/event-stream"); + expect(retryRequest.headers.get("Custom-Header")).toBe("custom-value"); + }); + + it("should handle empty response body gracefully", async () => { + const { x402HTTPClient: MockX402HTTPClient } = await import("@x402/core/client"); + const successResponse = createResponse(200, { data: "success" }); + + // Response with headers only, no body + const headerOnlyResponse = new Response("", { + status: 402, + headers: new Headers({ "PAYMENT-REQUIRED": "encoded-header" }), + }); + + mockFetch.mockResolvedValueOnce(headerOnlyResponse); + mockFetch.mockResolvedValueOnce(successResponse); + + const result = await wrappedFetch("https://api.example.com", { method: "GET" }); + + expect(result).toBe(successResponse); + expect(MockX402HTTPClient.prototype.getPaymentRequiredResponse).toHaveBeenCalled(); + }); + + it("should handle invalid JSON in response body gracefully", async () => { + const { x402HTTPClient: MockX402HTTPClient } = await import("@x402/core/client"); + const successResponse = createResponse(200, { data: "success" }); + + // Response with invalid JSON body + const invalidJsonResponse = new Response("not valid json", { + status: 402, + headers: new Headers({ "PAYMENT-REQUIRED": "encoded-header" }), + }); + + mockFetch.mockResolvedValueOnce(invalidJsonResponse); + mockFetch.mockResolvedValueOnce(successResponse); + + const result = await wrappedFetch("https://api.example.com", { method: "GET" }); + + expect(result).toBe(successResponse); + expect(MockX402HTTPClient.prototype.getPaymentRequiredResponse).toHaveBeenCalled(); + }); + + it("should accept x402HTTPClient directly", async () => { + const { x402HTTPClient: MockX402HTTPClient } = await import("@x402/core/client"); + + const httpClient = new MockX402HTTPClient(mockClient) as unknown as x402HTTPClient; + const wrappedWithHttpClient = wrapFetchWithPayment(mockFetch, httpClient); + + const successResponse = createResponse(200, { data: "success" }); + mockFetch.mockResolvedValue(successResponse); + + const result = await wrappedWithHttpClient("https://api.example.com", { method: "GET" }); + + expect(result).toBe(successResponse); + }); + + it("should preserve request body on retry (fixes body consumption bug)", async () => { + const successResponse = createResponse(200, { data: "success" }); + + mockFetch + .mockResolvedValueOnce(createResponse(402, validPaymentRequired)) + .mockResolvedValueOnce(successResponse); + + const bodyContent = JSON.stringify({ test: "data" }); + + await wrappedFetch("https://api.example.com", { + method: "POST", + body: bodyContent, + headers: { "Content-Type": "application/json" }, + }); + + // Verify the retry request has the body preserved + const retryCall = mockFetch.mock.calls[1]; + const retryRequest = retryCall[0] as Request; + expect(retryRequest.method).toBe("POST"); + const retryBody = await retryRequest.text(); + expect(retryBody).toBe(bodyContent); + }); + + it("should preserve headers from Request object input", async () => { + const successResponse = createResponse(200, { data: "success" }); + + mockFetch + .mockResolvedValueOnce(createResponse(402, validPaymentRequired)) + .mockResolvedValueOnce(successResponse); + + // Pass a Request object with custom headers (not init) + const request = new Request("https://api.example.com", { + method: "GET", + headers: { "Custom-Header": "custom-value", Authorization: "Bearer token" }, + }); + + await wrappedFetch(request); + + // Verify the retry request has all headers preserved + const retryCall = mockFetch.mock.calls[1]; + const retryRequest = retryCall[0] as Request; + expect(retryRequest.headers.get("Custom-Header")).toBe("custom-value"); + expect(retryRequest.headers.get("Authorization")).toBe("Bearer token"); + expect(retryRequest.headers.get("PAYMENT-SIGNATURE")).toBe("encoded-payment-header"); + }); +}); + +describe("wrapFetchWithPaymentFromConfig()", () => { + let mockFetch: ReturnType; + + beforeEach(async () => { + vi.resetAllMocks(); + + mockFetch = vi.fn(); + + const { x402Client: MockX402Client, x402HTTPClient: MockX402HTTPClient } = await import( + "@x402/core/client" + ); + (MockX402Client.fromConfig as ReturnType).mockReturnValue(new MockX402Client()); + + ( + MockX402HTTPClient.prototype.getPaymentRequiredResponse as ReturnType + ).mockReturnValue({ + x402Version: 2, + resource: { url: "test", description: "test", mimeType: "text/plain" }, + accepts: [], + }); + ( + MockX402HTTPClient.prototype.handlePaymentRequired as ReturnType + ).mockResolvedValue(null); + }); + + it("should create client from config and wrap fetch", async () => { + const { x402Client: MockX402Client } = await import("@x402/core/client"); + + const config: x402ClientConfig = { + schemes: [], + }; + + const wrappedFetch = wrapFetchWithPaymentFromConfig(mockFetch, config); + + expect(MockX402Client.fromConfig).toHaveBeenCalledWith(config); + expect(typeof wrappedFetch).toBe("function"); + }); + + it("should return wrapped fetch function", async () => { + const config: x402ClientConfig = { + schemes: [], + }; + + const wrappedFetch = wrapFetchWithPaymentFromConfig(mockFetch, config); + const successResponse = new Response(JSON.stringify({ data: "success" }), { status: 200 }); + mockFetch.mockResolvedValue(successResponse); + + const result = await wrappedFetch("https://api.example.com", { method: "GET" }); + + expect(result).toBe(successResponse); + expect(mockFetch).toHaveBeenCalled(); + }); +}); diff --git a/typescript/packages/http/fetch/src/index.ts b/typescript/packages/http/fetch/src/index.ts new file mode 100644 index 0000000..63b29cb --- /dev/null +++ b/typescript/packages/http/fetch/src/index.ts @@ -0,0 +1,157 @@ +import { x402Client, x402ClientConfig, x402HTTPClient } from "@x402/core/client"; +import { type PaymentRequired } from "@x402/core/types"; + +/** + * Enables the payment of APIs using the x402 payment protocol v2. + * + * This function wraps the native fetch API to automatically handle 402 Payment Required responses + * by creating and sending payment headers. It will: + * 1. Make the initial request + * 2. If a 402 response is received, parse the payment requirements + * 3. Create a payment header using the configured x402HTTPClient + * 4. Retry the request with the payment header + * + * @param fetch - The fetch function to wrap (typically globalThis.fetch) + * @param client - Configured x402Client or x402HTTPClient instance for handling payments + * @returns A wrapped fetch function that handles 402 responses automatically + * + * @example + * ```typescript + * import { wrapFetchWithPayment, x402Client } from '@x402/fetch'; + * import { ExactEvmScheme } from '@x402/evm'; + * import { ExactSvmScheme } from '@x402/svm'; + * + * const client = new x402Client() + * .register('eip155:8453', new ExactEvmScheme(evmSigner)) + * .register('solana:mainnet', new ExactSvmScheme(svmSigner)) + * .register('eip155:1', new ExactEvmScheme(evmSigner), 1); // v1 protocol + * + * const fetchWithPay = wrapFetchWithPayment(fetch, client); + * + * // Make a request that may require payment + * const response = await fetchWithPay('https://api.example.com/paid-endpoint'); + * ``` + * + * @throws {Error} If no schemes are provided + * @throws {Error} If the request configuration is missing + * @throws {Error} If a payment has already been attempted for this request + * @throws {Error} If there's an error creating the payment header + */ +export function wrapFetchWithPayment( + fetch: typeof globalThis.fetch, + client: x402Client | x402HTTPClient, +) { + const httpClient = client instanceof x402HTTPClient ? client : new x402HTTPClient(client); + + return async (input: RequestInfo | URL, init?: RequestInit) => { + const request = new Request(input, init); + const clonedRequest = request.clone(); + + const response = await fetch(request); + + if (response.status !== 402) { + return response; + } + + // Parse payment requirements from response + let paymentRequired: PaymentRequired; + try { + // Create getHeader function for case-insensitive header lookup + const getHeader = (name: string) => response.headers.get(name); + + // Try to get from headers first (v2), then from body (v1) + let body: PaymentRequired | undefined; + try { + const responseText = await response.text(); + if (responseText) { + body = JSON.parse(responseText) as PaymentRequired; + } + } catch { + // Ignore JSON parse errors - might be header-only response + } + + paymentRequired = httpClient.getPaymentRequiredResponse(getHeader, body); + } catch (error) { + throw new Error( + `Failed to parse payment requirements: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } + + // Run payment required hooks + const hookHeaders = await httpClient.handlePaymentRequired(paymentRequired); + if (hookHeaders) { + const hookRequest = clonedRequest.clone(); + for (const [key, value] of Object.entries(hookHeaders)) { + hookRequest.headers.set(key, value); + } + const hookResponse = await fetch(hookRequest); + if (hookResponse.status !== 402) { + return hookResponse; // Hook succeeded + } + // Hook's retry got 402, fall through to payment + } + + // Create payment payload (copy extensions from PaymentRequired) + let paymentPayload; + try { + paymentPayload = await client.createPaymentPayload(paymentRequired); + } catch (error) { + throw new Error( + `Failed to create payment payload: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } + + // Encode payment header + const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload); + + // Check if this is already a retry to prevent infinite loops + if (clonedRequest.headers.has("PAYMENT-SIGNATURE") || clonedRequest.headers.has("X-PAYMENT")) { + throw new Error("Payment already attempted"); + } + + // Add payment headers to cloned request + for (const [key, value] of Object.entries(paymentHeaders)) { + clonedRequest.headers.set(key, value); + } + clonedRequest.headers.set( + "Access-Control-Expose-Headers", + "PAYMENT-RESPONSE,X-PAYMENT-RESPONSE", + ); + + // Retry the request with payment + const secondResponse = await fetch(clonedRequest); + return secondResponse; + }; +} + +/** + * Creates a payment-enabled fetch function from a configuration object. + * + * @param fetch - The fetch function to wrap (typically globalThis.fetch) + * @param config - Configuration options including scheme registrations and selectors + * @returns A wrapped fetch function that handles 402 responses automatically + */ +export function wrapFetchWithPaymentFromConfig( + fetch: typeof globalThis.fetch, + config: x402ClientConfig, +) { + const client = x402Client.fromConfig(config); + return wrapFetchWithPayment(fetch, client); +} + +// Re-export types and utilities for convenience +export { x402Client, x402HTTPClient } from "@x402/core/client"; +export type { + PaymentPolicy, + SchemeRegistration, + SelectPaymentRequirements, + x402ClientConfig, +} from "@x402/core/client"; +export { decodePaymentResponseHeader } from "@x402/core/http"; +export type { + Network, + PaymentPayload, + PaymentRequired, + PaymentRequirements, + SchemeNetworkClient, +} from "@x402/core/types"; diff --git a/typescript/packages/http/fetch/tsconfig.json b/typescript/packages/http/fetch/tsconfig.json new file mode 100644 index 0000000..afe3217 --- /dev/null +++ b/typescript/packages/http/fetch/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "allowJs": false, + "checkJs": false, + "lib": ["DOM"] + }, + "include": ["src", "test/unit/index.test.ts"] +} diff --git a/typescript/packages/http/fetch/tsup.config.ts b/typescript/packages/http/fetch/tsup.config.ts new file mode 100644 index 0000000..f8699f9 --- /dev/null +++ b/typescript/packages/http/fetch/tsup.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from "tsup"; + +const baseConfig = { + entry: { + index: "src/index.ts", + }, + dts: { + resolve: true, + }, + sourcemap: true, + target: "node16", +}; + +export default defineConfig([ + { + ...baseConfig, + format: "esm", + outDir: "dist/esm", + clean: true, + }, + { + ...baseConfig, + format: "cjs", + outDir: "dist/cjs", + clean: false, + }, +]); diff --git a/typescript/packages/http/fetch/vitest.config.ts b/typescript/packages/http/fetch/vitest.config.ts new file mode 100644 index 0000000..156f8c9 --- /dev/null +++ b/typescript/packages/http/fetch/vitest.config.ts @@ -0,0 +1,10 @@ +import { loadEnv } from "vite"; +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ""), + }, + plugins: [tsconfigPaths({ projects: ["."] })], +})); diff --git a/typescript/packages/http/hono/.prettierignore b/typescript/packages/http/hono/.prettierignore new file mode 100644 index 0000000..5bd240b --- /dev/null +++ b/typescript/packages/http/hono/.prettierignore @@ -0,0 +1,8 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +src/client +**/**/*.json +*.md \ No newline at end of file diff --git a/typescript/packages/http/hono/.prettierrc b/typescript/packages/http/hono/.prettierrc new file mode 100644 index 0000000..ffb416b --- /dev/null +++ b/typescript/packages/http/hono/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/packages/http/hono/CHANGELOG.md b/typescript/packages/http/hono/CHANGELOG.md new file mode 100644 index 0000000..f2e84f0 --- /dev/null +++ b/typescript/packages/http/hono/CHANGELOG.md @@ -0,0 +1,26 @@ +# @x402/hono Changelog + +## 2.3.0 + +### Minor Changes + +- 51b8445: Bumped @x402/core dependency to 2.3.0 + +### Patch Changes + +- Updated dependencies [51b8445] +- Updated dependencies [51b8445] +- Updated dependencies [51b8445] +- Updated dependencies [fe42994] +- Updated dependencies [51b8445] + - @x402/core@2.3.0 + - @x402/paywall@2.3.0 + - @x402/extensions@2.3.0 + +## 2.0.0 + +- Implements x402 2.0.0 for the TypeScript SDK. + +## 1.0.0 + +- Implements x402 1.0.0 for the TypeScript SDK. diff --git a/typescript/packages/http/hono/README.md b/typescript/packages/http/hono/README.md new file mode 100644 index 0000000..b9776b4 --- /dev/null +++ b/typescript/packages/http/hono/README.md @@ -0,0 +1,255 @@ +# @x402/hono + +Hono middleware integration for the x402 Payment Protocol. This package provides a simple middleware function for adding x402 payment requirements to your Hono applications. + +## Installation + +```bash +pnpm install @x402/hono +``` + +## Quick Start + +```typescript +import { Hono } from "hono"; +import { serve } from "@hono/node-server"; +import { paymentMiddleware, x402ResourceServer } from "@x402/hono"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { HTTPFacilitatorClient } from "@x402/core/server"; + +const app = new Hono(); + +const facilitatorClient = new HTTPFacilitatorClient({ url: "https://facilitator.x402.org" }); +const resourceServer = new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()); + +// Apply the payment middleware with your configuration +app.use( + paymentMiddleware( + { + "GET /protected-route": { + accepts: { + scheme: "exact", + price: "$0.10", + network: "eip155:84532", + payTo: "0xYourAddress", + }, + description: "Access to premium content", + }, + }, + resourceServer, + ), +); + +// Implement your protected route +app.get("/protected-route", (c) => { + return c.json({ message: "This content is behind a paywall" }); +}); + +serve({ fetch: app.fetch, port: 3000 }); +``` + +## Configuration + +The `paymentMiddleware` function accepts the following parameters: + +```typescript +paymentMiddleware( + routes: RoutesConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart?: boolean +) +``` + +### Parameters + +1. **`routes`** (required): Route configurations for protected endpoints +2. **`server`** (required): Pre-configured x402ResourceServer instance +3. **`paywallConfig`** (optional): Configuration for the built-in paywall UI +4. **`paywall`** (optional): Custom paywall provider +5. **`syncFacilitatorOnStart`** (optional): Whether to sync with facilitator on startup (defaults to true) + +## API Reference + +### HonoAdapter + +The `HonoAdapter` class implements the `HTTPAdapter` interface from `@x402/core`, providing Hono-specific request handling: + +```typescript +class HonoAdapter implements HTTPAdapter { + getHeader(name: string): string | undefined; + getMethod(): string; + getPath(): string; + getUrl(): string; + getAcceptHeader(): string; + getUserAgent(): string; +} +``` + +### Middleware Function + +```typescript +function paymentMiddleware( + routes: RoutesConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart?: boolean, +): MiddlewareHandler; +``` + +Creates Hono middleware that: + +1. Uses the provided x402ResourceServer for payment processing +2. Checks if the incoming request matches a protected route +3. Validates payment headers if required +4. Returns payment instructions (402 status) if payment is missing or invalid +5. Processes the request if payment is valid +6. Handles settlement after successful response + +### Route Configuration + +Routes are passed as the first parameter to `paymentMiddleware`: + +```typescript +const routes: RoutesConfig = { + "GET /api/protected": { + accepts: { + scheme: "exact", + price: "$0.10", + network: "eip155:84532", + payTo: "0xYourAddress", + maxTimeoutSeconds: 60, + }, + description: "Premium API access", + }, +}; + +app.use(paymentMiddleware(routes, resourceServer)); +``` + +### Paywall Configuration + +The middleware automatically displays a paywall UI when browsers request protected endpoints. + +**Option 1: Full Paywall UI (Recommended)** + +Install the optional `@x402/paywall` package for a complete wallet connection and payment UI: + +```bash +pnpm add @x402/paywall +``` + +Then configure it: + +```typescript +const paywallConfig: PaywallConfig = { + appName: "Your App Name", + appLogo: "/path/to/logo.svg", + testnet: true, +}; + +app.use(paymentMiddleware(routes, resourceServer, paywallConfig)); +``` + +**Option 2: Basic Paywall (No Installation)** + +Without `@x402/paywall` installed, the middleware returns a basic HTML page with payment instructions. + +**Option 3: Custom Paywall Provider** + +Provide your own paywall provider: + +```typescript +app.use(paymentMiddleware(routes, resourceServer, paywallConfig, customPaywallProvider)); +``` + +## Advanced Usage + +### Multiple Protected Routes + +```typescript +app.use( + paymentMiddleware( + { + "GET /api/premium/*": { + accepts: { + scheme: "exact", + price: "$1.00", + network: "eip155:8453", + payTo: "0xYourAddress", + }, + description: "Premium API access", + }, + "GET /api/data": { + accepts: { + scheme: "exact", + price: "$0.50", + network: "eip155:84532", + payTo: "0xYourAddress", + maxTimeoutSeconds: 120, + }, + description: "Data endpoint access", + }, + }, + resourceServer, + ), +); +``` + +### Multiple Payment Networks + +```typescript +app.use( + paymentMiddleware( + { + "GET /weather": { + accepts: [ + { + scheme: "exact", + price: "$0.001", + network: "eip155:84532", + payTo: evmAddress, + }, + { + scheme: "exact", + price: "$0.001", + network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + payTo: svmAddress, + }, + ], + description: "Weather data", + mimeType: "application/json", + }, + }, + new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()) + .register("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", new ExactSvmScheme()), + ), +); +``` + +### Custom Facilitator Client + +If you need to use a custom facilitator server, configure it when creating the x402ResourceServer: + +```typescript +import { HTTPFacilitatorClient } from "@x402/core/server"; +import { x402ResourceServer } from "@x402/hono"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; + +const customFacilitator = new HTTPFacilitatorClient({ + url: "https://your-facilitator.com", + createAuthHeaders: async () => ({ + verify: { Authorization: "Bearer your-token" }, + settle: { Authorization: "Bearer your-token" }, + }), +}); + +const resourceServer = new x402ResourceServer(customFacilitator) + .register("eip155:84532", new ExactEvmScheme()); + +app.use(paymentMiddleware(routes, resourceServer, paywallConfig)); +``` diff --git a/typescript/packages/http/hono/eslint.config.js b/typescript/packages/http/hono/eslint.config.js new file mode 100644 index 0000000..28e5647 --- /dev/null +++ b/typescript/packages/http/hono/eslint.config.js @@ -0,0 +1,73 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + Headers: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/typescript/packages/http/hono/package.json b/typescript/packages/http/hono/package.json new file mode 100644 index 0000000..50b4b99 --- /dev/null +++ b/typescript/packages/http/hono/package.json @@ -0,0 +1,70 @@ +{ + "name": "@x402/hono", + "version": "2.3.0", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "start": "tsx --env-file=.env index.ts", + "test": "vitest run", + "test:watch": "vitest", + "build": "tsup", + "watch": "tsc --watch", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "keywords": [], + "license": "Apache-2.0", + "author": "Coinbase Inc.", + "repository": "https://github.com/coinbase/x402", + "description": "x402 Payment Protocol", + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/node": "^22.13.4", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "hono": "^4.7.1", + "prettier": "3.5.2", + "tsup": "^8.4.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vite": "^6.2.6", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.5" + }, + "dependencies": { + "@x402/core": "workspace:~", + "@x402/extensions": "workspace:~", + "zod": "^3.24.2" + }, + "peerDependencies": { + "hono": "^4.0.0", + "@x402/paywall": "workspace:*" + }, + "peerDependenciesMeta": { + "@x402/paywall": { + "optional": true + } + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.mts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + } + }, + "files": [ + "dist" + ] +} diff --git a/typescript/packages/http/hono/src/adapter.test.ts b/typescript/packages/http/hono/src/adapter.test.ts new file mode 100644 index 0000000..d1e8374 --- /dev/null +++ b/typescript/packages/http/hono/src/adapter.test.ts @@ -0,0 +1,161 @@ +import { describe, it, expect, vi } from "vitest"; +import { Context } from "hono"; +import { HonoAdapter } from "./adapter"; + +/** + * Factory for creating mock Hono Context. + * + * @param options - Configuration options for the mock context. + * @param options.url - The request URL. + * @param options.method - The HTTP method. + * @param options.headers - Request headers. + * @param options.query - Query parameters. + * @param options.body - Request body. + * @returns A mock Hono Context. + */ +function createMockContext( + options: { + url?: string; + method?: string; + headers?: Record; + query?: Record; + body?: unknown; + } = {}, +): Context { + const url = new URL(options.url || "https://example.com/api/test"); + const headers = options.headers || {}; + const query = options.query || {}; + + const mockContext = { + req: { + header: vi.fn((name: string) => headers[name]), + method: options.method || "GET", + path: url.pathname, + url: url.toString(), + query: vi.fn((name?: string) => { + if (name === undefined) { + return query; + } + return query[name]; + }), + json: vi.fn().mockResolvedValue(options.body), + }, + } as unknown as Context; + + return mockContext; +} + +describe("HonoAdapter", () => { + describe("getHeader", () => { + it("returns header value when present", () => { + const c = createMockContext({ headers: { "X-Payment": "test-payment" } }); + const adapter = new HonoAdapter(c); + expect(adapter.getHeader("X-Payment")).toBe("test-payment"); + }); + + it("returns undefined for missing headers", () => { + const c = createMockContext(); + const adapter = new HonoAdapter(c); + expect(adapter.getHeader("X-Missing")).toBeUndefined(); + }); + }); + + describe("getMethod", () => { + it("returns the HTTP method", () => { + const c = createMockContext({ method: "POST" }); + const adapter = new HonoAdapter(c); + expect(adapter.getMethod()).toBe("POST"); + }); + }); + + describe("getPath", () => { + it("returns the pathname", () => { + const c = createMockContext({ url: "https://example.com/api/weather?city=NYC" }); + const adapter = new HonoAdapter(c); + expect(adapter.getPath()).toBe("/api/weather"); + }); + }); + + describe("getUrl", () => { + it("returns the full URL", () => { + const c = createMockContext({ url: "https://example.com/api/test?foo=bar" }); + const adapter = new HonoAdapter(c); + expect(adapter.getUrl()).toBe("https://example.com/api/test?foo=bar"); + }); + }); + + describe("getAcceptHeader", () => { + it("returns Accept header when present", () => { + const c = createMockContext({ headers: { Accept: "text/html" } }); + const adapter = new HonoAdapter(c); + expect(adapter.getAcceptHeader()).toBe("text/html"); + }); + + it("returns empty string when missing", () => { + const c = createMockContext(); + const adapter = new HonoAdapter(c); + expect(adapter.getAcceptHeader()).toBe(""); + }); + }); + + describe("getUserAgent", () => { + it("returns User-Agent header when present", () => { + const c = createMockContext({ headers: { "User-Agent": "Mozilla/5.0" } }); + const adapter = new HonoAdapter(c); + expect(adapter.getUserAgent()).toBe("Mozilla/5.0"); + }); + + it("returns empty string when missing", () => { + const c = createMockContext(); + const adapter = new HonoAdapter(c); + expect(adapter.getUserAgent()).toBe(""); + }); + }); + + describe("getQueryParams", () => { + it("returns all query parameters", () => { + const c = createMockContext({ query: { foo: "bar", baz: "qux" } }); + const adapter = new HonoAdapter(c); + expect(adapter.getQueryParams()).toEqual({ foo: "bar", baz: "qux" }); + }); + + it("returns empty object when no query params", () => { + const c = createMockContext({ query: {} }); + const adapter = new HonoAdapter(c); + expect(adapter.getQueryParams()).toEqual({}); + }); + }); + + describe("getQueryParam", () => { + it("returns single value for single param", () => { + const c = createMockContext({ query: { city: "NYC" } }); + const adapter = new HonoAdapter(c); + expect(adapter.getQueryParam("city")).toBe("NYC"); + }); + + it("returns undefined for missing param", () => { + const c = createMockContext({ query: {} }); + const adapter = new HonoAdapter(c); + expect(adapter.getQueryParam("missing")).toBeUndefined(); + }); + }); + + describe("getBody", () => { + it("returns parsed JSON body", async () => { + const body = { data: "test" }; + const c = createMockContext({ body }); + const adapter = new HonoAdapter(c); + expect(await adapter.getBody()).toEqual(body); + }); + + it("returns undefined when body parsing fails", async () => { + const mockContext = { + req: { + json: vi.fn().mockRejectedValue(new Error("Invalid JSON")), + }, + } as unknown as Context; + const adapter = new HonoAdapter(mockContext); + expect(await adapter.getBody()).toBeUndefined(); + }); + }); +}); diff --git a/typescript/packages/http/hono/src/adapter.ts b/typescript/packages/http/hono/src/adapter.ts new file mode 100644 index 0000000..cfa4a07 --- /dev/null +++ b/typescript/packages/http/hono/src/adapter.ts @@ -0,0 +1,108 @@ +import { HTTPAdapter } from "@x402/core/server"; +import { Context } from "hono"; + +/** + * Hono adapter implementation + */ +export class HonoAdapter implements HTTPAdapter { + /** + * Creates a new HonoAdapter instance. + * + * @param c - The Hono context object + */ + constructor(private c: Context) {} + + /** + * Gets a header value from the request. + * + * @param name - The header name + * @returns The header value or undefined + */ + getHeader(name: string): string | undefined { + return this.c.req.header(name); + } + + /** + * Gets the HTTP method of the request. + * + * @returns The HTTP method + */ + getMethod(): string { + return this.c.req.method; + } + + /** + * Gets the path of the request. + * + * @returns The request path + */ + getPath(): string { + return this.c.req.path; + } + + /** + * Gets the full URL of the request. + * + * @returns The full request URL + */ + getUrl(): string { + return this.c.req.url; + } + + /** + * Gets the Accept header from the request. + * + * @returns The Accept header value or empty string + */ + getAcceptHeader(): string { + return this.c.req.header("Accept") || ""; + } + + /** + * Gets the User-Agent header from the request. + * + * @returns The User-Agent header value or empty string + */ + getUserAgent(): string { + return this.c.req.header("User-Agent") || ""; + } + + /** + * Gets all query parameters from the request URL. + * + * @returns Record of query parameter key-value pairs + */ + getQueryParams(): Record { + const query = this.c.req.query(); + // Convert single values to match the interface + const result: Record = {}; + for (const [key, value] of Object.entries(query)) { + result[key] = value; + } + return result; + } + + /** + * Gets a specific query parameter by name. + * + * @param name - The query parameter name + * @returns The query parameter value(s) or undefined + */ + getQueryParam(name: string): string | string[] | undefined { + return this.c.req.query(name); + } + + /** + * Gets the parsed request body. + * Requires appropriate body parsing middleware. + * + * @returns The parsed request body + */ + async getBody(): Promise { + try { + return await this.c.req.json(); + } catch { + return undefined; + } + } +} diff --git a/typescript/packages/http/hono/src/index.test.ts b/typescript/packages/http/hono/src/index.test.ts new file mode 100644 index 0000000..25b1edb --- /dev/null +++ b/typescript/packages/http/hono/src/index.test.ts @@ -0,0 +1,664 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { Context } from "hono"; +import type { + HTTPProcessResult, + x402HTTPResourceServer, + PaywallProvider, + FacilitatorClient, +} from "@x402/core/server"; +import { + x402ResourceServer, + x402HTTPResourceServer as HTTPResourceServer, +} from "@x402/core/server"; +import type { PaymentPayload, PaymentRequirements, SchemeNetworkServer } from "@x402/core/types"; +import { paymentMiddleware, paymentMiddlewareFromConfig, type SchemeRegistration } from "./index"; + +// --- Test Fixtures --- +const mockRoutes = { + "/api/*": { + accepts: { scheme: "exact", payTo: "0x123", price: "$0.01", network: "eip155:84532" }, + }, +} as const; + +const mockPaymentPayload = { + scheme: "exact", + network: "eip155:84532", + payload: { signature: "0xabc" }, +} as unknown as PaymentPayload; + +const mockPaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + maxAmountRequired: "1000", + payTo: "0x123", +} as unknown as PaymentRequirements; + +// --- Mock setup --- +let mockProcessHTTPRequest: ReturnType; +let mockProcessSettlement: ReturnType; +let mockRegisterPaywallProvider: ReturnType; +let mockRequiresPayment: ReturnType; + +vi.mock("@x402/core/server", () => ({ + x402ResourceServer: vi.fn().mockImplementation(() => ({ + initialize: vi.fn().mockResolvedValue(undefined), + registerExtension: vi.fn(), + register: vi.fn(), + hasExtension: vi.fn().mockReturnValue(false), + })), + x402HTTPResourceServer: vi.fn().mockImplementation((server, routes) => ({ + initialize: vi.fn().mockResolvedValue(undefined), + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + })), +})); + +// --- Mock Factories --- +/** + * Sets up the mock HTTP server to return specified results. + * + * @param processResult - The result to return from processHTTPRequest. + * @param settlementResult - Result to return from processSettlement. + */ +function setupMockHttpServer( + processResult: HTTPProcessResult, + settlementResult: + | { success: true; headers: Record } + | { success: false; errorReason: string } = { success: true, headers: {} }, +): void { + mockProcessHTTPRequest.mockResolvedValue(processResult); + mockProcessSettlement.mockResolvedValue(settlementResult); +} + +/** + * Creates a mock Hono Context for testing. + * + * @param options - Configuration options for the mock context. + * @param options.path - The request URL path. + * @param options.method - The HTTP method. + * @param options.headers - Request headers. + * @returns A mock Hono Context. + */ +function createMockContext( + options: { + path?: string; + method?: string; + headers?: Record; + } = {}, +): Context & { + _status: number; + _headers: Record; + _body: unknown; + _isHtml: boolean; +} { + const headers = options.headers || {}; + const responseHeaders = new Map(); + + const context = { + _status: 200, + _headers: {} as Record, + _body: undefined as unknown, + _isHtml: false, + req: { + path: options.path || "/api/test", + method: options.method || "GET", + header: vi.fn((name: string) => headers[name.toLowerCase()]), + }, + res: undefined as Response | undefined, + header: vi.fn((key: string, value: string) => { + responseHeaders.set(key, value); + context._headers[key] = value; + }), + status: vi.fn((code: number) => { + context._status = code; + }), + html: vi.fn((body: string, status?: number) => { + context._body = body; + context._isHtml = true; + if (status) context._status = status; + const response = new Response(body, { + status: status || context._status, + headers: { "Content-Type": "text/html" }, + }); + context.res = response; + return response; + }), + json: vi.fn((body: unknown, status?: number) => { + context._body = body; + context._isHtml = false; + if (status) context._status = status; + const response = new Response(JSON.stringify(body), { + status: status || context._status, + headers: { "Content-Type": "application/json" }, + }); + // Copy response headers + responseHeaders.forEach((value, key) => { + response.headers.set(key, value); + }); + context.res = response; + return response; + }), + }; + + return context as unknown as Context & typeof context; +} + +// --- Tests --- +describe("paymentMiddleware", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockProcessHTTPRequest = vi.fn(); + mockProcessSettlement = vi.fn(); + mockRegisterPaywallProvider = vi.fn(); + mockRequiresPayment = vi.fn().mockReturnValue(true); + + // Reset the mock implementation + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + }); + + it("calls next() when no-payment-required", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const context = createMockContext(); + const next = vi.fn().mockResolvedValue(undefined); + + await middleware(context, next); + + expect(next).toHaveBeenCalled(); + expect(mockProcessHTTPRequest).toHaveBeenCalled(); + }); + + it("returns 402 HTML for payment-error with isHtml", async () => { + setupMockHttpServer({ + type: "payment-error", + response: { + status: 402, + body: "Paywall", + headers: {}, + isHtml: true, + }, + }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const context = createMockContext(); + const next = vi.fn().mockResolvedValue(undefined); + + await middleware(context, next); + + expect(next).not.toHaveBeenCalled(); + expect(context.html).toHaveBeenCalledWith("Paywall", 402); + }); + + it("returns 402 JSON for payment-error", async () => { + setupMockHttpServer({ + type: "payment-error", + response: { + status: 402, + body: { error: "Payment required" }, + headers: {}, + isHtml: false, + }, + }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const context = createMockContext(); + const next = vi.fn().mockResolvedValue(undefined); + + await middleware(context, next); + + expect(next).not.toHaveBeenCalled(); + expect(context.json).toHaveBeenCalledWith({ error: "Payment required" }, 402); + }); + + it("sets custom headers from payment-error response", async () => { + setupMockHttpServer({ + type: "payment-error", + response: { + status: 402, + body: { error: "Payment required" }, + headers: { "X-Custom-Header": "custom-value" }, + isHtml: false, + }, + }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const context = createMockContext(); + const next = vi.fn().mockResolvedValue(undefined); + + await middleware(context, next); + + expect(context.header).toHaveBeenCalledWith("X-Custom-Header", "custom-value"); + }); + + it("settles and returns response for payment-verified with successful handler", async () => { + setupMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: true, headers: { "PAYMENT-RESPONSE": "settled" } }, + ); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const context = createMockContext(); + + // Create a proper Response mock with headers + const responseHeaders = new Headers(); + const mockResponse = { + status: 200, + headers: responseHeaders, + } as unknown as Response; + + const next = vi.fn().mockImplementation(async () => { + context.res = mockResponse; + }); + + await middleware(context, next); + + expect(next).toHaveBeenCalled(); + expect(mockProcessSettlement).toHaveBeenCalledWith( + mockPaymentPayload, + mockPaymentRequirements, + undefined, + ); + expect(responseHeaders.get("PAYMENT-RESPONSE")).toBe("settled"); + }); + + it("skips settlement when handler returns >= 400", async () => { + setupMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: true, headers: { "PAYMENT-RESPONSE": "settled" } }, + ); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const context = createMockContext(); + + const next = vi.fn().mockImplementation(async () => { + context.res = new Response("Error", { status: 500 }); + }); + + await middleware(context, next); + + expect(next).toHaveBeenCalled(); + expect(mockProcessSettlement).not.toHaveBeenCalled(); + }); + + it("returns 402 when settlement throws error", async () => { + setupMockHttpServer({ + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }); + mockProcessSettlement.mockRejectedValue(new Error("Settlement rejected")); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const context = createMockContext(); + + const responseHeaders = new Headers(); + const next = vi.fn().mockImplementation(async () => { + context.res = { + status: 200, + headers: responseHeaders, + } as unknown as Response; + }); + + await middleware(context, next); + + expect(context.json).toHaveBeenCalledWith( + { + error: "Settlement failed", + details: "Settlement rejected", + }, + 402, + ); + }); + + it("returns 402 when settlement returns success: false", async () => { + setupMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: false, errorReason: "Insufficient funds" }, + ); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const context = createMockContext(); + + const responseHeaders = new Headers(); + const next = vi.fn().mockImplementation(async () => { + context.res = { + status: 200, + headers: responseHeaders, + } as unknown as Response; + }); + + await middleware(context, next); + + expect(context.json).toHaveBeenCalledWith( + { + error: "Settlement failed", + details: "Insufficient funds", + }, + 402, + ); + }); + + it("passes paywallConfig to processHTTPRequest", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + const paywallConfig = { appName: "test-app" }; + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + paywallConfig, + undefined, + false, + ); + const context = createMockContext(); + const next = vi.fn().mockResolvedValue(undefined); + + await middleware(context, next); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith(expect.anything(), paywallConfig); + }); + + it("registers custom paywall provider", () => { + setupMockHttpServer({ type: "no-payment-required" }); + const paywall: PaywallProvider = { generateHtml: vi.fn() }; + + paymentMiddleware(mockRoutes, {} as unknown as x402ResourceServer, undefined, paywall, false); + + expect(mockRegisterPaywallProvider).toHaveBeenCalledWith(paywall); + }); +}); + +describe("paymentMiddlewareFromConfig", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockProcessHTTPRequest = vi.fn(); + mockProcessSettlement = vi.fn(); + mockRegisterPaywallProvider = vi.fn(); + mockRequiresPayment = vi.fn().mockReturnValue(true); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + initialize: vi.fn().mockResolvedValue(undefined), + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + + vi.mocked(x402ResourceServer).mockImplementation( + () => + ({ + initialize: vi.fn().mockResolvedValue(undefined), + registerExtension: vi.fn(), + register: vi.fn(), + }) as unknown as x402ResourceServer, + ); + }); + + it("creates x402ResourceServer with facilitator clients", () => { + setupMockHttpServer({ type: "no-payment-required" }); + const facilitator = { verify: vi.fn(), settle: vi.fn() } as unknown as FacilitatorClient; + + paymentMiddlewareFromConfig(mockRoutes, facilitator); + + expect(x402ResourceServer).toHaveBeenCalledWith(facilitator); + }); + + it("registers scheme servers for each network", () => { + setupMockHttpServer({ type: "no-payment-required" }); + const schemeServer = { verify: vi.fn(), settle: vi.fn() } as unknown as SchemeNetworkServer; + const schemes: SchemeRegistration[] = [ + { network: "eip155:84532", server: schemeServer }, + { network: "eip155:8453", server: schemeServer }, + ]; + + paymentMiddlewareFromConfig(mockRoutes, undefined, schemes); + + const serverInstance = vi.mocked(x402ResourceServer).mock.results[0].value; + expect(serverInstance.register).toHaveBeenCalledTimes(2); + expect(serverInstance.register).toHaveBeenCalledWith("eip155:84532", schemeServer); + expect(serverInstance.register).toHaveBeenCalledWith("eip155:8453", schemeServer); + }); + + it("returns a working middleware function", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const middleware = paymentMiddlewareFromConfig(mockRoutes); + const context = createMockContext(); + const next = vi.fn().mockResolvedValue(undefined); + + await middleware(context, next); + + expect(next).toHaveBeenCalled(); + }); +}); + +describe("HonoAdapter", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockProcessHTTPRequest = vi.fn(); + mockProcessSettlement = vi.fn(); + mockRegisterPaywallProvider = vi.fn(); + mockRequiresPayment = vi.fn().mockReturnValue(true); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + }); + + it("extracts path and method from context", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const context = createMockContext({ path: "/api/weather", method: "POST" }); + const next = vi.fn().mockResolvedValue(undefined); + + await middleware(context, next); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + path: "/api/weather", + method: "POST", + }), + undefined, + ); + }); + + it("extracts x-payment header", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const context = createMockContext({ headers: { "x-payment": "payment-data" } }); + const next = vi.fn().mockResolvedValue(undefined); + + await middleware(context, next); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: "payment-data", + }), + undefined, + ); + }); + + it("extracts payment-signature header (v2)", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const context = createMockContext({ headers: { "payment-signature": "sig-data" } }); + const next = vi.fn().mockResolvedValue(undefined); + + await middleware(context, next); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: "sig-data", + }), + undefined, + ); + }); + + it("prefers payment-signature over x-payment", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const context = createMockContext({ + headers: { "payment-signature": "sig-data", "x-payment": "x-payment-data" }, + }); + const next = vi.fn().mockResolvedValue(undefined); + + await middleware(context, next); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: "sig-data", + }), + undefined, + ); + }); + + it("returns undefined paymentHeader when no payment headers present", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const context = createMockContext(); + const next = vi.fn().mockResolvedValue(undefined); + + await middleware(context, next); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: undefined, + }), + undefined, + ); + }); +}); diff --git a/typescript/packages/http/hono/src/index.ts b/typescript/packages/http/hono/src/index.ts new file mode 100644 index 0000000..5a02b06 --- /dev/null +++ b/typescript/packages/http/hono/src/index.ts @@ -0,0 +1,311 @@ +import { + HTTPRequestContext, + PaywallConfig, + PaywallProvider, + x402HTTPResourceServer, + x402ResourceServer, + RoutesConfig, + FacilitatorClient, +} from "@x402/core/server"; +import { SchemeNetworkServer, Network } from "@x402/core/types"; +import { Context, MiddlewareHandler } from "hono"; +import { HonoAdapter } from "./adapter"; + +/** + * Check if any routes in the configuration declare bazaar extensions + * + * @param routes - Route configuration + * @returns True if any route has extensions.bazaar defined + */ +function checkIfBazaarNeeded(routes: RoutesConfig): boolean { + // Handle single route config + if ("accepts" in routes) { + return !!(routes.extensions && "bazaar" in routes.extensions); + } + + // Handle multiple routes + return Object.values(routes).some(routeConfig => { + return !!(routeConfig.extensions && "bazaar" in routeConfig.extensions); + }); +} + +/** + * Configuration for registering a payment scheme with a specific network + */ +export interface SchemeRegistration { + /** + * The network identifier (e.g., 'eip155:84532', 'solana:mainnet') + */ + network: Network; + + /** + * The scheme server implementation for this network + */ + server: SchemeNetworkServer; +} + +/** + * Hono payment middleware for x402 protocol (direct HTTP server instance). + * + * Use this when you need to configure HTTP-level hooks. + * + * @param httpServer - Pre-configured x402HTTPResourceServer instance + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @returns Hono middleware handler + * + * @example + * ```typescript + * import { paymentMiddlewareFromHTTPServer, x402ResourceServer, x402HTTPResourceServer } from "@x402/hono"; + * + * const resourceServer = new x402ResourceServer(facilitatorClient) + * .register(NETWORK, new ExactEvmScheme()); + * + * const httpServer = new x402HTTPResourceServer(resourceServer, routes) + * .onProtectedRequest(requestHook); + * + * app.use(paymentMiddlewareFromHTTPServer(httpServer)); + * ``` + */ +export function paymentMiddlewareFromHTTPServer( + httpServer: x402HTTPResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +): MiddlewareHandler { + // Register custom paywall provider if provided + if (paywall) { + httpServer.registerPaywallProvider(paywall); + } + + // Store initialization promise (not the result) + // httpServer.initialize() fetches facilitator support and validates routes + let initPromise: Promise | null = syncFacilitatorOnStart ? httpServer.initialize() : null; + + // Dynamically register bazaar extension if routes declare it and not already registered + // Skip if pre-registered (e.g., in serverless environments where static imports are used) + let bazaarPromise: Promise | null = null; + if (checkIfBazaarNeeded(httpServer.routes) && !httpServer.server.hasExtension("bazaar")) { + bazaarPromise = import("@x402/extensions/bazaar") + .then(({ bazaarResourceServerExtension }) => { + httpServer.server.registerExtension(bazaarResourceServerExtension); + }) + .catch(err => { + console.error("Failed to load bazaar extension:", err); + }); + } + + return async (c: Context, next: () => Promise) => { + // Create adapter and context + const adapter = new HonoAdapter(c); + const context: HTTPRequestContext = { + adapter, + path: c.req.path, + method: c.req.method, + paymentHeader: adapter.getHeader("payment-signature") || adapter.getHeader("x-payment"), + }; + + // Check if route requires payment before initializing facilitator + if (!httpServer.requiresPayment(context)) { + return next(); + } + + // Only initialize when processing a protected route + if (initPromise) { + await initPromise; + initPromise = null; // Clear after first await + } + + // Await bazaar extension loading if needed + if (bazaarPromise) { + await bazaarPromise; + bazaarPromise = null; + } + + // Process payment requirement check + const result = await httpServer.processHTTPRequest(context, paywallConfig); + + // Handle the different result types + switch (result.type) { + case "no-payment-required": + // No payment needed, proceed directly to the route handler + return next(); + + case "payment-error": + // Payment required but not provided or invalid + const { response } = result; + Object.entries(response.headers).forEach(([key, value]) => { + c.header(key, value); + }); + if (response.isHtml) { + return c.html(response.body as string, response.status as 402); + } else { + return c.json(response.body || {}, response.status as 402); + } + + case "payment-verified": + // Payment is valid, need to wrap response for settlement + const { paymentPayload, paymentRequirements, declaredExtensions } = result; + + // Proceed to the next middleware or route handler + await next(); + + // Get the current response + let res = c.res; + + // If the response from the protected route is >= 400, do not settle payment + if (res.status >= 400) { + return; + } + + // Clear the response so we can modify headers + c.res = undefined; + + try { + const settleResult = await httpServer.processSettlement( + paymentPayload, + paymentRequirements, + declaredExtensions, + ); + + if (!settleResult.success) { + // Settlement failed - do not return the protected resource + res = c.json( + { + error: "Settlement failed", + details: settleResult.errorReason, + }, + 402, + ); + } else { + // Settlement succeeded - add headers to response + Object.entries(settleResult.headers).forEach(([key, value]) => { + res.headers.set(key, value); + }); + } + } catch (error) { + console.error(error); + // If settlement fails, return an error response + res = c.json( + { + error: "Settlement failed", + details: error instanceof Error ? error.message : "Unknown error", + }, + 402, + ); + } + + // Restore the response (potentially modified with settlement headers) + c.res = res; + return; + } + }; +} + +/** + * Hono payment middleware for x402 protocol (direct server instance). + * + * Use this when you want to pass a pre-configured x402ResourceServer instance. + * This provides more flexibility for testing, custom configuration, and reusing + * server instances across multiple middlewares. + * + * @param routes - Route configurations for protected endpoints + * @param server - Pre-configured x402ResourceServer instance + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @returns Hono middleware handler + * + * @example + * ```typescript + * import { paymentMiddleware } from "@x402/hono"; + * + * const server = new x402ResourceServer(myFacilitatorClient) + * .register(NETWORK, new ExactEvmScheme()); + * + * app.use(paymentMiddleware(routes, server, paywallConfig)); + * ``` + */ +export function paymentMiddleware( + routes: RoutesConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +): MiddlewareHandler { + // Create the x402 HTTP server instance with the resource server + const httpServer = new x402HTTPResourceServer(server, routes); + + return paymentMiddlewareFromHTTPServer( + httpServer, + paywallConfig, + paywall, + syncFacilitatorOnStart, + ); +} + +/** + * Hono payment middleware for x402 protocol (configuration-based). + * + * Use this when you want to quickly set up middleware with simple configuration. + * This function creates and configures the x402ResourceServer internally. + * + * @param routes - Route configurations for protected endpoints + * @param facilitatorClients - Optional facilitator client(s) for payment processing + * @param schemes - Optional array of scheme registrations for server-side payment processing + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @returns Hono middleware handler + * + * @example + * ```typescript + * import { paymentMiddlewareFromConfig } from "@x402/hono"; + * + * app.use(paymentMiddlewareFromConfig( + * routes, + * myFacilitatorClient, + * [{ network: "eip155:8453", server: evmSchemeServer }], + * paywallConfig + * )); + * ``` + */ +export function paymentMiddlewareFromConfig( + routes: RoutesConfig, + facilitatorClients?: FacilitatorClient | FacilitatorClient[], + schemes?: SchemeRegistration[], + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +): MiddlewareHandler { + const ResourceServer = new x402ResourceServer(facilitatorClients); + + if (schemes) { + schemes.forEach(({ network, server: schemeServer }) => { + ResourceServer.register(network, schemeServer); + }); + } + + // Use the direct paymentMiddleware with the configured server + // Note: paymentMiddleware handles dynamic bazaar registration + return paymentMiddleware(routes, ResourceServer, paywallConfig, paywall, syncFacilitatorOnStart); +} + +export { x402ResourceServer, x402HTTPResourceServer } from "@x402/core/server"; + +export type { + PaymentRequired, + PaymentRequirements, + PaymentPayload, + Network, + SchemeNetworkServer, +} from "@x402/core/types"; + +export type { PaywallProvider, PaywallConfig } from "@x402/core/server"; + +export { RouteConfigurationError } from "@x402/core/server"; + +export type { RouteValidationError } from "@x402/core/server"; + +export { HonoAdapter } from "./adapter"; diff --git a/typescript/packages/http/hono/src/malformedPathBypass.test.ts b/typescript/packages/http/hono/src/malformedPathBypass.test.ts new file mode 100644 index 0000000..e7398f6 --- /dev/null +++ b/typescript/packages/http/hono/src/malformedPathBypass.test.ts @@ -0,0 +1,128 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import type { Context } from "hono"; +import { paymentMiddleware } from "./index"; +import { + x402HTTPResourceServer, + x402ResourceServer, + type HTTPRequestContext, +} from "@x402/core/server"; + +/** + * Creates a mock Hono context for testing + * + * @param options - Configuration options + * @param options.path - Request path + * @param options.method - HTTP method + * @param options.headers - Request headers + * @returns Mock context object + */ +function createMockContext(options: { + path: string; + method?: string; + headers?: Record; +}): Context & { + _status: number; + _headers: Record; + _body: unknown; + _isHtml: boolean; +} { + const headers = options.headers || {}; + const responseHeaders = new Map(); + + const context = { + _status: 200, + _headers: {} as Record, + _body: undefined as unknown, + _isHtml: false, + req: { + path: options.path, + method: options.method || "GET", + header: vi.fn((name: string) => headers[name.toLowerCase()]), + }, + res: undefined as Response | undefined, + header: vi.fn((key: string, value: string) => { + responseHeaders.set(key, value); + context._headers[key] = value; + }), + html: vi.fn((body: string, status?: number) => { + context._body = body; + context._isHtml = true; + if (status) context._status = status; + const response = new Response(body, { + status: status || context._status, + headers: { "Content-Type": "text/html" }, + }); + context.res = response; + return response; + }), + json: vi.fn((body: unknown, status?: number) => { + context._body = body; + context._isHtml = false; + if (status) context._status = status; + const response = new Response(JSON.stringify(body), { + status: status || context._status, + headers: { "Content-Type": "application/json" }, + }); + responseHeaders.forEach((value, key) => { + response.headers.set(key, value); + }); + context.res = response; + return response; + }), + }; + + return context as unknown as Context & typeof context; +} + +describe("paymentMiddleware malformed path bypass", () => { + let processSpy: ReturnType; + + beforeEach(() => { + processSpy = vi + .spyOn(x402HTTPResourceServer.prototype, "processHTTPRequest") + .mockImplementation(async (context: HTTPRequestContext) => { + return { + type: "payment-error", + response: { + status: 402, + body: { error: "Payment required", path: context.path }, + headers: {}, + isHtml: false, + }, + }; + }); + }); + + afterEach(() => { + processSpy.mockRestore(); + }); + + it.each(["/paywall/some-param%", "/paywall/some-param%c0"])( + "does not call next() and returns 402 for %s", + async path => { + const routes = { + "/paywall/*": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00", + network: "eip155:8453", + }, + }, + }; + + const server = new x402ResourceServer(); + const middleware = paymentMiddleware(routes, server, undefined, undefined, false); + + const context = createMockContext({ path }); + const next = vi.fn().mockResolvedValue(undefined); + + await middleware(context, next); + + expect(next).not.toHaveBeenCalled(); + expect(processSpy).toHaveBeenCalled(); + expect(processSpy.mock.calls[0]?.[0]).toEqual(expect.objectContaining({ path })); + expect(context._status).toBe(402); + }, + ); +}); diff --git a/typescript/packages/http/hono/tsconfig.json b/typescript/packages/http/hono/tsconfig.json new file mode 100644 index 0000000..1b119d3 --- /dev/null +++ b/typescript/packages/http/hono/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "allowJs": false, + "checkJs": false + }, + "include": ["src"] +} diff --git a/typescript/packages/http/hono/tsup.config.ts b/typescript/packages/http/hono/tsup.config.ts new file mode 100644 index 0000000..f8699f9 --- /dev/null +++ b/typescript/packages/http/hono/tsup.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from "tsup"; + +const baseConfig = { + entry: { + index: "src/index.ts", + }, + dts: { + resolve: true, + }, + sourcemap: true, + target: "node16", +}; + +export default defineConfig([ + { + ...baseConfig, + format: "esm", + outDir: "dist/esm", + clean: true, + }, + { + ...baseConfig, + format: "cjs", + outDir: "dist/cjs", + clean: false, + }, +]); diff --git a/typescript/packages/http/hono/vitest.config.ts b/typescript/packages/http/hono/vitest.config.ts new file mode 100644 index 0000000..156f8c9 --- /dev/null +++ b/typescript/packages/http/hono/vitest.config.ts @@ -0,0 +1,10 @@ +import { loadEnv } from "vite"; +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ""), + }, + plugins: [tsconfigPaths({ projects: ["."] })], +})); diff --git a/typescript/packages/http/next/.prettierignore b/typescript/packages/http/next/.prettierignore new file mode 100644 index 0000000..5bd240b --- /dev/null +++ b/typescript/packages/http/next/.prettierignore @@ -0,0 +1,8 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +src/client +**/**/*.json +*.md \ No newline at end of file diff --git a/typescript/packages/http/next/.prettierrc b/typescript/packages/http/next/.prettierrc new file mode 100644 index 0000000..ffb416b --- /dev/null +++ b/typescript/packages/http/next/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/packages/http/next/CHANGELOG.md b/typescript/packages/http/next/CHANGELOG.md new file mode 100644 index 0000000..4dd58c9 --- /dev/null +++ b/typescript/packages/http/next/CHANGELOG.md @@ -0,0 +1,26 @@ +# @x402/next Changelog + +## 2.3.0 + +### Minor Changes + +- 51b8445: Bumped @x402/core dependency to 2.3.0 + +### Patch Changes + +- Updated dependencies [51b8445] +- Updated dependencies [51b8445] +- Updated dependencies [51b8445] +- Updated dependencies [fe42994] +- Updated dependencies [51b8445] + - @x402/core@2.3.0 + - @x402/paywall@2.3.0 + - @x402/extensions@2.3.0 + +## 2.0.0 + +- Implements x402 2.0.0 for the TypeScript SDK. + +## 1.0.0 + +- Implements x402 1.0.0 for the TypeScript SDK. diff --git a/typescript/packages/http/next/README.md b/typescript/packages/http/next/README.md new file mode 100644 index 0000000..db13527 --- /dev/null +++ b/typescript/packages/http/next/README.md @@ -0,0 +1,278 @@ +# @x402/next + +Next.js integration for the x402 Payment Protocol. This package allows you to easily add paywall functionality to your Next.js applications using the x402 protocol. + +## Installation + +```bash +pnpm install @x402/next +``` + +## Quick Start + +### Protecting Page Routes + +Page routes are protected using the `paymentProxy`. Create a proxy (middleware) file in your Next.js project (`proxy.ts`): + +```typescript +import { paymentProxy, x402ResourceServer } from "@x402/next"; +import { HTTPFacilitatorClient } from "@x402/core/server"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; + +const facilitatorClient = new HTTPFacilitatorClient({ url: "https://facilitator.x402.org" }); +const resourceServer = new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()); + +export const proxy = paymentProxy( + { + "/protected": { + accepts: { + scheme: "exact", + price: "$0.01", + network: "eip155:84532", + payTo: "0xYourAddress", + }, + description: "Access to protected content", + }, + }, + resourceServer, +); + +// Configure which paths the middleware should run on +export const config = { + matcher: ["/protected/:path*"], +}; +``` + +### Protecting API Routes + +API routes are protected using the `withX402` route wrapper. This is the recommended approach to protect API routes as it guarantees payment settlement only AFTER successful API responses (status < 400). API routes can also be protected by `paymentProxy`, however this will charge clients for failed API responses: + +```typescript +// app/api/your-endpoint/route.ts +import { NextRequest, NextResponse } from "next/server"; +import { withX402 } from "@x402/next"; + +const handler = async (_: NextRequest) => { + return NextResponse.json({ data: "your response" }); +}; + +export const GET = withX402( + handler, + { + accepts: { + scheme: "exact", + price: "$0.01", + network: "eip155:84532", + payTo: "0xYourAddress", + }, + description: "Access to API endpoint", + }, + server, // your configured x402ResourceServer +); +``` + +## Configuration + +### paymentProxy + +The `paymentProxy` function is used to protect page routes. It can also protect API routes, however this will charge clients for failed API responses. + +```typescript +paymentProxy( + routes: RoutesConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart?: boolean +) +``` + +#### Parameters + +1. **`routes`** (required): Route configurations for protected endpoints +2. **`server`** (required): Pre-configured x402ResourceServer instance +3. **`paywallConfig`** (optional): Configuration for the built-in paywall UI +4. **`paywall`** (optional): Custom paywall provider +5. **`syncFacilitatorOnStart`** (optional): Whether to sync with facilitator on startup (defaults to true) + +### withX402 + +The `withX402` function wraps API route handlers. This is the recommended approach to protect API routes as it guarantees payment settlement only AFTER successful API responses (status < 400). + +```typescript +withX402( + routeHandler: (request: NextRequest) => Promise, + routeConfig: RouteConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart?: boolean +) +``` + +#### Parameters + +1. **`routeHandler`** (required): Your API route handler function +2. **`routeConfig`** (required): Payment configuration for this specific route +3. **`server`** (required): Pre-configured x402ResourceServer instance +4. **`paywallConfig`** (optional): Configuration for the built-in paywall UI +5. **`paywall`** (optional): Custom paywall provider +6. **`syncFacilitatorOnStart`** (optional): Whether to sync with facilitator on startup (defaults to true) + +## API Reference + +### NextAdapter + +The `NextAdapter` class implements the `HTTPAdapter` interface from `@x402/core`, providing Next.js-specific request handling: + +```typescript +class NextAdapter implements HTTPAdapter { + getHeader(name: string): string | undefined; + getMethod(): string; + getPath(): string; + getUrl(): string; + getAcceptHeader(): string; + getUserAgent(): string; +} +``` + +### Route Configuration + +```typescript +const routes: RoutesConfig = { + "/api/protected": { + accepts: { + scheme: "exact", + price: "$0.10", + network: "eip155:84532", + payTo: "0xYourAddress", + maxTimeoutSeconds: 60, + }, + description: "Premium API access", + }, +}; +``` + +## Advanced Usage + +### Multiple Payment Networks + +```typescript +import { paymentProxy, x402ResourceServer } from "@x402/next"; +import { HTTPFacilitatorClient } from "@x402/core/server"; +import { registerExactEvmScheme } from "@x402/evm/exact/server"; +import { registerExactSvmScheme } from "@x402/svm/exact/server"; + +const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); +const server = new x402ResourceServer(facilitatorClient); + +registerExactEvmScheme(server); +registerExactSvmScheme(server); + +export const middleware = paymentProxy( + { + "/protected": { + accepts: [ + { + scheme: "exact", + price: "$0.001", + network: "eip155:84532", + payTo: evmAddress, + }, + { + scheme: "exact", + price: "$0.001", + network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + payTo: svmAddress, + }, + ], + description: "Premium content", + mimeType: "text/html", + }, + }, + server, +); +``` + +### Custom Paywall + +```typescript +import { createPaywall } from "@x402/paywall"; +import { evmPaywall } from "@x402/paywall/evm"; +import { svmPaywall } from "@x402/paywall/svm"; + +const paywall = createPaywall() + .withNetwork(evmPaywall) + .withNetwork(svmPaywall) + .withConfig({ + appName: "My App", + appLogo: "/logo.png", + testnet: true, + }) + .build(); + +export const middleware = paymentProxy( + routes, + server, + undefined, // paywallConfig (using custom paywall instead) + paywall, +); +``` +## Migration from x402-next + +If you're migrating from the legacy `x402-next` package: + +1. **Update imports**: Change from `x402-next` to `@x402/next` +2. **New API**: Create an x402ResourceServer and register payment schemes +3. **Function rename**: `paymentMiddleware` is now `paymentProxy` +4. **Parameter order**: Routes first, then resource server + +### Before (x402-next): + +```typescript +import { paymentMiddleware } from "x402-next"; + +export const middleware = paymentMiddleware( + "0xYourAddress", + { + "/protected": { + price: "$0.01", + network: "base-sepolia", + config: { description: "Access to protected content" }, + }, + }, + facilitator, + paywall, +); +``` + +### After (@x402/next): + +```typescript +import { paymentProxy, x402ResourceServer } from "@x402/next"; +import { HTTPFacilitatorClient } from "@x402/core/server"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; + +const facilitator = new HTTPFacilitatorClient({ url: facilitatorUrl }); +const resourceServer = new x402ResourceServer(facilitator) + .register("eip155:84532", new ExactEvmScheme()); + +export const middleware = paymentProxy( + { + "/protected": { + accepts: { + scheme: "exact", + price: "$0.01", + network: "eip155:84532", + payTo: "0xYourAddress", + }, + description: "Access to protected content", + }, + }, + resourceServer, +); +``` + +Note: The `payTo` address is now specified within each route configuration rather than as a separate parameter. + diff --git a/typescript/packages/http/next/eslint.config.js b/typescript/packages/http/next/eslint.config.js new file mode 100644 index 0000000..ca28b5c --- /dev/null +++ b/typescript/packages/http/next/eslint.config.js @@ -0,0 +1,72 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/typescript/packages/http/next/package.json b/typescript/packages/http/next/package.json new file mode 100644 index 0000000..0e74cb0 --- /dev/null +++ b/typescript/packages/http/next/package.json @@ -0,0 +1,71 @@ +{ + "name": "@x402/next", + "version": "2.3.0", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "start": "tsx --env-file=.env index.ts", + "test": "vitest run", + "test:watch": "vitest", + "build": "tsup", + "watch": "tsc --watch", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "keywords": [], + "license": "Apache-2.0", + "author": "Coinbase Inc.", + "repository": "https://github.com/coinbase/x402", + "description": "x402 Payment Protocol", + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/node": "^22.13.4", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsup": "^8.4.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vite": "^6.2.6", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.5" + }, + "dependencies": { + "@coinbase/cdp-sdk": "^1.22.0", + "@x402/core": "workspace:~", + "@x402/extensions": "workspace:~", + "zod": "^3.24.2" + }, + "peerDependencies": { + "next": "^16.0.10", + "@x402/paywall": "workspace:*" + }, + "peerDependenciesMeta": { + "@x402/paywall": { + "optional": true + } + }, + "type": "module", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + } + }, + "files": [ + "dist" + ] +} diff --git a/typescript/packages/http/next/src/adapter.test.ts b/typescript/packages/http/next/src/adapter.test.ts new file mode 100644 index 0000000..1e13e88 --- /dev/null +++ b/typescript/packages/http/next/src/adapter.test.ts @@ -0,0 +1,154 @@ +import { describe, it, expect } from "vitest"; +import { NextRequest } from "next/server"; +import { NextAdapter } from "./adapter"; + +/** + * Factory for creating mock NextRequest. + * + * @param options - Configuration options for the mock request. + * @param options.url - The request URL. + * @param options.method - The HTTP method. + * @param options.headers - Request headers. + * @returns A mock NextRequest. + */ +function createMockRequest( + options: { + url?: string; + method?: string; + headers?: Record; + } = {}, +): NextRequest { + const url = options.url || "https://example.com/api/test"; + const req = new NextRequest(url, { + method: options.method || "GET", + headers: options.headers, + }); + return req; +} + +describe("NextAdapter", () => { + describe("getHeader", () => { + it("returns header value when present", () => { + const req = createMockRequest({ headers: { "X-Payment": "test-payment" } }); + const adapter = new NextAdapter(req); + expect(adapter.getHeader("X-Payment")).toBe("test-payment"); + }); + + it("returns undefined for missing headers", () => { + const req = createMockRequest(); + const adapter = new NextAdapter(req); + expect(adapter.getHeader("X-Missing")).toBeUndefined(); + }); + }); + + describe("getMethod", () => { + it("returns the HTTP method", () => { + const req = createMockRequest({ method: "POST" }); + const adapter = new NextAdapter(req); + expect(adapter.getMethod()).toBe("POST"); + }); + }); + + describe("getPath", () => { + it("returns the pathname", () => { + const req = createMockRequest({ url: "https://example.com/api/weather?city=NYC" }); + const adapter = new NextAdapter(req); + expect(adapter.getPath()).toBe("/api/weather"); + }); + }); + + describe("getUrl", () => { + it("returns the full URL", () => { + const req = createMockRequest({ url: "https://example.com/api/test?foo=bar" }); + const adapter = new NextAdapter(req); + expect(adapter.getUrl()).toBe("https://example.com/api/test?foo=bar"); + }); + }); + + describe("getAcceptHeader", () => { + it("returns Accept header when present", () => { + const req = createMockRequest({ headers: { Accept: "text/html" } }); + const adapter = new NextAdapter(req); + expect(adapter.getAcceptHeader()).toBe("text/html"); + }); + + it("returns empty string when missing", () => { + const req = createMockRequest(); + const adapter = new NextAdapter(req); + expect(adapter.getAcceptHeader()).toBe(""); + }); + }); + + describe("getUserAgent", () => { + it("returns User-Agent header when present", () => { + const req = createMockRequest({ headers: { "User-Agent": "Mozilla/5.0" } }); + const adapter = new NextAdapter(req); + expect(adapter.getUserAgent()).toBe("Mozilla/5.0"); + }); + + it("returns empty string when missing", () => { + const req = createMockRequest(); + const adapter = new NextAdapter(req); + expect(adapter.getUserAgent()).toBe(""); + }); + }); + + describe("getQueryParams", () => { + it("returns all query parameters", () => { + const req = createMockRequest({ url: "https://example.com/api?foo=bar&baz=qux" }); + const adapter = new NextAdapter(req); + expect(adapter.getQueryParams()).toEqual({ foo: "bar", baz: "qux" }); + }); + + it("handles multiple values for same key", () => { + const req = createMockRequest({ url: "https://example.com/api?tag=a&tag=b&tag=c" }); + const adapter = new NextAdapter(req); + expect(adapter.getQueryParams()).toEqual({ tag: ["a", "b", "c"] }); + }); + + it("returns empty object when no query params", () => { + const req = createMockRequest({ url: "https://example.com/api" }); + const adapter = new NextAdapter(req); + expect(adapter.getQueryParams()).toEqual({}); + }); + }); + + describe("getQueryParam", () => { + it("returns single value for single param", () => { + const req = createMockRequest({ url: "https://example.com/api?city=NYC" }); + const adapter = new NextAdapter(req); + expect(adapter.getQueryParam("city")).toBe("NYC"); + }); + + it("returns array for multiple values", () => { + const req = createMockRequest({ url: "https://example.com/api?id=1&id=2" }); + const adapter = new NextAdapter(req); + expect(adapter.getQueryParam("id")).toEqual(["1", "2"]); + }); + + it("returns undefined for missing param", () => { + const req = createMockRequest({ url: "https://example.com/api" }); + const adapter = new NextAdapter(req); + expect(adapter.getQueryParam("missing")).toBeUndefined(); + }); + }); + + describe("getBody", () => { + it("returns parsed JSON body", async () => { + const body = { data: "test" }; + const req = new NextRequest("https://example.com/api", { + method: "POST", + body: JSON.stringify(body), + headers: { "Content-Type": "application/json" }, + }); + const adapter = new NextAdapter(req); + expect(await adapter.getBody()).toEqual(body); + }); + + it("returns undefined when body parsing fails", async () => { + const req = new NextRequest("https://example.com/api", { method: "GET" }); + const adapter = new NextAdapter(req); + expect(await adapter.getBody()).toBeUndefined(); + }); + }); +}); diff --git a/typescript/packages/http/next/src/adapter.ts b/typescript/packages/http/next/src/adapter.ts new file mode 100644 index 0000000..180ac1e --- /dev/null +++ b/typescript/packages/http/next/src/adapter.ts @@ -0,0 +1,117 @@ +import { HTTPAdapter } from "@x402/core/server"; +import { NextRequest } from "next/server"; + +/** + * Next.js adapter implementation + */ +export class NextAdapter implements HTTPAdapter { + /** + * Creates a new NextAdapter instance. + * + * @param req - The Next.js request object + */ + constructor(private req: NextRequest) {} + + /** + * Gets a header value from the request. + * + * @param name - The header name + * @returns The header value or undefined + */ + getHeader(name: string): string | undefined { + return this.req.headers.get(name) || undefined; + } + + /** + * Gets the HTTP method of the request. + * + * @returns The HTTP method + */ + getMethod(): string { + return this.req.method; + } + + /** + * Gets the path of the request. + * + * @returns The request path + */ + getPath(): string { + return this.req.nextUrl.pathname; + } + + /** + * Gets the full URL of the request. + * + * @returns The full request URL + */ + getUrl(): string { + return this.req.url; + } + + /** + * Gets the Accept header from the request. + * + * @returns The Accept header value or empty string + */ + getAcceptHeader(): string { + return this.req.headers.get("Accept") || ""; + } + + /** + * Gets the User-Agent header from the request. + * + * @returns The User-Agent header value or empty string + */ + getUserAgent(): string { + return this.req.headers.get("User-Agent") || ""; + } + + /** + * Gets all query parameters from the request URL. + * + * @returns Record of query parameter key-value pairs + */ + getQueryParams(): Record { + const params: Record = {}; + this.req.nextUrl.searchParams.forEach((value, key) => { + const existing = params[key]; + if (existing) { + if (Array.isArray(existing)) { + existing.push(value); + } else { + params[key] = [existing, value]; + } + } else { + params[key] = value; + } + }); + return params; + } + + /** + * Gets a specific query parameter by name. + * + * @param name - The query parameter name + * @returns The query parameter value(s) or undefined + */ + getQueryParam(name: string): string | string[] | undefined { + const all = this.req.nextUrl.searchParams.getAll(name); + if (all.length === 0) return undefined; + if (all.length === 1) return all[0]; + return all; + } + + /** + * Gets the parsed request body. + * + * @returns Promise resolving to the parsed request body + */ + async getBody(): Promise { + try { + return await this.req.json(); + } catch { + return undefined; + } + } +} diff --git a/typescript/packages/http/next/src/index.test.ts b/typescript/packages/http/next/src/index.test.ts new file mode 100644 index 0000000..b1bd6d1 --- /dev/null +++ b/typescript/packages/http/next/src/index.test.ts @@ -0,0 +1,461 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { NextRequest, NextResponse } from "next/server"; +import type { + HTTPProcessResult, + x402HTTPResourceServer, + PaywallProvider, + FacilitatorClient, +} from "@x402/core/server"; +import { x402ResourceServer, x402HTTPResourceServer } from "@x402/core/server"; +import type { PaymentPayload, PaymentRequirements, SchemeNetworkServer } from "@x402/core/types"; +import { paymentProxy, paymentProxyFromConfig, withX402, type SchemeRegistration } from "./index"; + +import { createHttpServer } from "./utils"; + +// Mock utils +vi.mock("./utils", async () => { + const actual = await vi.importActual("./utils"); + return { + ...actual, + createHttpServer: vi.fn(), + }; +}); + +// Shared mock functions storage - will be populated by tests +const mockFunctions = { + processHTTPRequest: vi.fn(), + processSettlement: vi.fn(), + requiresPayment: vi.fn().mockReturnValue(true), +}; + +// Mock @x402/core/server +vi.mock("@x402/core/server", () => ({ + x402ResourceServer: vi.fn().mockImplementation(() => ({ + initialize: vi.fn().mockResolvedValue(undefined), + registerExtension: vi.fn(), + register: vi.fn(), + hasExtension: vi.fn().mockReturnValue(false), + })), + x402HTTPResourceServer: vi.fn().mockImplementation((server, routes) => ({ + initialize: vi.fn().mockResolvedValue(undefined), + registerPaywallProvider: vi.fn(), + processHTTPRequest: (...args: unknown[]) => mockFunctions.processHTTPRequest(...args), + processSettlement: (...args: unknown[]) => mockFunctions.processSettlement(...args), + requiresPayment: (...args: unknown[]) => mockFunctions.requiresPayment(...args), + routes: routes || {}, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + })), +})); + +// --- Test Fixtures --- +const mockRoutes = { + "/api/*": { + accepts: { scheme: "exact", payTo: "0x123", price: "$0.01", network: "eip155:84532" }, + }, +} as const; + +const mockRouteConfig = { + accepts: { scheme: "exact", payTo: "0x123", price: "$0.01", network: "eip155:84532" }, + description: "Test route", +} as const; + +const mockPaymentPayload = { + scheme: "exact", + network: "eip155:84532", + payload: { signature: "0xabc" }, +} as unknown as PaymentPayload; + +const mockPaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + maxAmountRequired: "1000", + payTo: "0x123", +} as unknown as PaymentRequirements; + +// --- Mock Factories --- +/** + * Creates a mock HTTP server for testing. + * + * @param processResult - The result to return from processHTTPRequest. + * @param settlementResult - Result to return from processSettlement (success with headers or failure). + * @returns A mock x402HTTPResourceServer. + */ +function createMockHttpServer( + processResult: HTTPProcessResult, + settlementResult: + | { success: true; headers: Record } + | { success: false; errorReason: string } = { success: true, headers: {} }, +): x402HTTPResourceServer { + return { + processHTTPRequest: vi.fn().mockResolvedValue(processResult), + processSettlement: vi.fn().mockResolvedValue(settlementResult), + registerPaywallProvider: vi.fn(), + initialize: vi.fn().mockResolvedValue(undefined), + requiresPayment: vi.fn().mockReturnValue(true), + routes: mockRoutes, + server: { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + } as unknown as x402HTTPResourceServer; +} + +/** + * Creates a mock Next.js request for testing. + * + * @param options - Configuration options for the mock request. + * @param options.url - The request URL. + * @param options.method - The HTTP method. + * @param options.headers - Request headers. + * @returns A mock NextRequest. + */ +function createMockRequest( + options: { + url?: string; + method?: string; + headers?: Record; + } = {}, +): NextRequest { + const url = options.url || "https://example.com/api/test"; + return new NextRequest(url, { + method: options.method || "GET", + headers: options.headers, + }); +} + +/** + * Sets up the mock x402HTTPResourceServer constructor to return instances + * with the provided mock server's behavior. + * + * @param mockServer - The mock x402HTTPResourceServer to use as template. + */ +function setupMockCreateHttpServer(mockServer: x402HTTPResourceServer): void { + // Replace the shared mock functions with the ones from mockServer + // This allows the mock constructor to use the configured mocks + mockFunctions.processHTTPRequest = mockServer.processHTTPRequest as ReturnType; + mockFunctions.processSettlement = mockServer.processSettlement as ReturnType; + mockFunctions.requiresPayment = mockServer.requiresPayment as ReturnType; + + // Also set up createHttpServer mock for backward compatibility + vi.mocked(createHttpServer).mockReturnValue({ + httpServer: mockServer, + init: vi.fn().mockResolvedValue(undefined), + }); +} + +describe("paymentProxy", () => { + beforeEach(() => { + vi.clearAllMocks(); + // Reset shared mock functions + mockFunctions.processHTTPRequest = vi.fn(); + mockFunctions.processSettlement = vi.fn(); + mockFunctions.requiresPayment = vi.fn().mockReturnValue(true); + }); + + it("returns NextResponse.next() when no-payment-required", async () => { + const mockServer = createMockHttpServer({ type: "no-payment-required" }); + setupMockCreateHttpServer(mockServer); + + const proxy = paymentProxy(mockRoutes, {} as unknown as x402ResourceServer); + const response = await proxy(createMockRequest()); + + expect(response.status).toBe(200); + }); + + it("returns 402 HTML for payment-error with isHtml", async () => { + const mockServer = createMockHttpServer({ + type: "payment-error", + response: { + status: 402, + headers: { "Content-Type": "text/html" }, + body: "Payment Required", + isHtml: true, + }, + }); + setupMockCreateHttpServer(mockServer); + + const proxy = paymentProxy(mockRoutes, {} as unknown as x402ResourceServer); + const response = await proxy(createMockRequest()); + + expect(response.status).toBe(402); + expect(response.headers.get("Content-Type")).toBe("text/html"); + expect(await response.text()).toBe("Payment Required"); + }); + + it("returns 402 JSON for payment-error", async () => { + const mockServer = createMockHttpServer({ + type: "payment-error", + response: { + status: 402, + headers: { "X-Custom-Header": "custom-value" }, + body: { error: "Payment required" }, + isHtml: false, + }, + }); + setupMockCreateHttpServer(mockServer); + + const proxy = paymentProxy(mockRoutes, {} as unknown as x402ResourceServer); + const response = await proxy(createMockRequest()); + + expect(response.status).toBe(402); + expect(response.headers.get("Content-Type")).toBe("application/json"); + const body = await response.json(); + expect(body).toEqual({ error: "Payment required" }); + }); + + it("settles and returns response for payment-verified", async () => { + const mockServer = createMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: true, headers: { "X-Settlement": "complete" } }, + ); + setupMockCreateHttpServer(mockServer); + + const proxy = paymentProxy(mockRoutes, {} as unknown as x402ResourceServer); + const response = await proxy(createMockRequest()); + + expect(response.status).toBe(200); + expect(response.headers.get("X-Settlement")).toBe("complete"); + expect(mockServer.processSettlement).toHaveBeenCalledWith( + mockPaymentPayload, + mockPaymentRequirements, + undefined, + ); + }); + + it("passes paywallConfig to processHTTPRequest", async () => { + const mockServer = createMockHttpServer({ type: "no-payment-required" }); + setupMockCreateHttpServer(mockServer); + const paywallConfig = { appName: "test-app", testnet: true }; + + const proxy = paymentProxy(mockRoutes, {} as unknown as x402ResourceServer, paywallConfig); + await proxy(createMockRequest()); + + expect(mockServer.processHTTPRequest).toHaveBeenCalledWith(expect.anything(), paywallConfig); + }); + + it("registers custom paywall provider", () => { + const mockServer = createMockHttpServer({ type: "no-payment-required" }); + setupMockCreateHttpServer(mockServer); + const paywall: PaywallProvider = { generateHtml: vi.fn() }; + + paymentProxy(mockRoutes, {} as unknown as x402ResourceServer, undefined, paywall); + + expect(x402HTTPResourceServer).toHaveBeenCalledWith(expect.anything(), mockRoutes); + }); + + it("returns 402 when settlement throws error", async () => { + const mockServer = createMockHttpServer({ + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }); + vi.mocked(mockServer.processSettlement).mockRejectedValue(new Error("Settlement rejected")); + setupMockCreateHttpServer(mockServer); + + const proxy = paymentProxy(mockRoutes, {} as unknown as x402ResourceServer); + const response = await proxy(createMockRequest()); + + expect(response.status).toBe(402); + const body = await response.json(); + expect(body.error).toBe("Settlement failed"); + }); + + it("returns 402 when settlement returns success: false, not the resource", async () => { + const mockServer = createMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: false, errorReason: "Insufficient funds" }, + ); + setupMockCreateHttpServer(mockServer); + + const proxy = paymentProxy(mockRoutes, {} as unknown as x402ResourceServer); + const response = await proxy(createMockRequest()); + + expect(response.status).toBe(402); + const body = await response.json(); + expect(body.error).toBe("Settlement failed"); + expect(body.details).toBe("Insufficient funds"); + }); +}); + +describe("withX402", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("calls handler when no-payment-required", async () => { + const mockServer = createMockHttpServer({ type: "no-payment-required" }); + setupMockCreateHttpServer(mockServer); + const handler = vi.fn().mockResolvedValue(NextResponse.json({ data: "protected" })); + + const wrappedHandler = withX402(handler, mockRouteConfig, {} as unknown as x402ResourceServer); + const response = await wrappedHandler(createMockRequest()); + + expect(handler).toHaveBeenCalled(); + expect(response.status).toBe(200); + }); + + it("returns 402 without calling handler for payment-error", async () => { + const mockServer = createMockHttpServer({ + type: "payment-error", + response: { + status: 402, + headers: {}, + body: { error: "Payment required" }, + isHtml: false, + }, + }); + setupMockCreateHttpServer(mockServer); + const handler = vi.fn().mockResolvedValue(NextResponse.json({ data: "protected" })); + + const wrappedHandler = withX402(handler, mockRouteConfig, {} as unknown as x402ResourceServer); + const response = await wrappedHandler(createMockRequest()); + + expect(handler).not.toHaveBeenCalled(); + expect(response.status).toBe(402); + }); + + it("calls handler and settles for payment-verified", async () => { + const mockServer = createMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: true, headers: { "X-Settlement": "complete" } }, + ); + setupMockCreateHttpServer(mockServer); + const handler = vi.fn().mockResolvedValue(NextResponse.json({ data: "protected" })); + + const wrappedHandler = withX402(handler, mockRouteConfig, {} as unknown as x402ResourceServer); + const response = await wrappedHandler(createMockRequest()); + + expect(handler).toHaveBeenCalled(); + expect(response.status).toBe(200); + expect(response.headers.get("X-Settlement")).toBe("complete"); + }); + + it("skips settlement when handler returns >= 400", async () => { + const mockServer = createMockHttpServer({ + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }); + setupMockCreateHttpServer(mockServer); + const handler = vi + .fn() + .mockResolvedValue( + new NextResponse(JSON.stringify({ error: "Bad request" }), { status: 400 }), + ); + + const wrappedHandler = withX402(handler, mockRouteConfig, {} as unknown as x402ResourceServer); + const response = await wrappedHandler(createMockRequest()); + + expect(handler).toHaveBeenCalled(); + expect(response.status).toBe(400); + expect(mockServer.processSettlement).not.toHaveBeenCalled(); + }); + + it("returns 402 when settlement throws error, not the handler response", async () => { + const mockServer = createMockHttpServer({ + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }); + vi.mocked(mockServer.processSettlement).mockRejectedValue(new Error("Settlement rejected")); + setupMockCreateHttpServer(mockServer); + const handler = vi.fn().mockResolvedValue(NextResponse.json({ data: "protected" })); + + const wrappedHandler = withX402(handler, mockRouteConfig, {} as unknown as x402ResourceServer); + const response = await wrappedHandler(createMockRequest()); + + expect(handler).toHaveBeenCalled(); + expect(response.status).toBe(402); + const body = await response.json(); + expect(body.error).toBe("Settlement failed"); + }); + + it("returns 402 when settlement returns success: false, not the handler response", async () => { + const mockServer = createMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: false, errorReason: "Insufficient funds" }, + ); + setupMockCreateHttpServer(mockServer); + const handler = vi.fn().mockResolvedValue(NextResponse.json({ data: "protected" })); + + const wrappedHandler = withX402(handler, mockRouteConfig, {} as unknown as x402ResourceServer); + const response = await wrappedHandler(createMockRequest()); + + expect(handler).toHaveBeenCalled(); + expect(response.status).toBe(402); + const body = await response.json(); + expect(body.error).toBe("Settlement failed"); + expect(body.details).toBe("Insufficient funds"); + }); +}); + +describe("paymentProxyFromConfig", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("creates x402ResourceServer with facilitator clients", () => { + const mockServer = createMockHttpServer({ type: "no-payment-required" }); + setupMockCreateHttpServer(mockServer); + const facilitator = { verify: vi.fn(), settle: vi.fn() } as unknown as FacilitatorClient; + + paymentProxyFromConfig(mockRoutes, facilitator); + + expect(x402ResourceServer).toHaveBeenCalledWith(facilitator); + }); + + it("registers scheme servers for each network", () => { + const mockServer = createMockHttpServer({ type: "no-payment-required" }); + setupMockCreateHttpServer(mockServer); + const schemeServer = { verify: vi.fn(), settle: vi.fn() } as unknown as SchemeNetworkServer; + const schemes: SchemeRegistration[] = [ + { network: "eip155:84532", server: schemeServer }, + { network: "eip155:8453", server: schemeServer }, + ]; + + paymentProxyFromConfig(mockRoutes, undefined, schemes); + + const serverInstance = vi.mocked(x402ResourceServer).mock.results[0].value; + expect(serverInstance.register).toHaveBeenCalledTimes(2); + expect(serverInstance.register).toHaveBeenCalledWith("eip155:84532", schemeServer); + expect(serverInstance.register).toHaveBeenCalledWith("eip155:8453", schemeServer); + }); + + it("returns a working proxy function", async () => { + const mockServer = createMockHttpServer({ type: "no-payment-required" }); + setupMockCreateHttpServer(mockServer); + + const proxy = paymentProxyFromConfig(mockRoutes); + const response = await proxy(createMockRequest()); + + expect(response.status).toBe(200); + }); + + it("passes all config options through to paymentProxy", () => { + const paywall: PaywallProvider = { generateHtml: vi.fn() }; + const paywallConfig = { appName: "test-app" }; + + paymentProxyFromConfig(mockRoutes, undefined, undefined, paywallConfig, paywall, false); + + expect(x402HTTPResourceServer).toHaveBeenCalledWith(expect.anything(), mockRoutes); + }); +}); diff --git a/typescript/packages/http/next/src/index.ts b/typescript/packages/http/next/src/index.ts new file mode 100644 index 0000000..0b76466 --- /dev/null +++ b/typescript/packages/http/next/src/index.ts @@ -0,0 +1,401 @@ +import { + PaywallConfig, + PaywallProvider, + x402ResourceServer, + RoutesConfig, + RouteConfig, + FacilitatorClient, +} from "@x402/core/server"; +import { SchemeNetworkServer, Network } from "@x402/core/types"; +import { NextRequest, NextResponse } from "next/server"; +import { + prepareHttpServer, + createRequestContext, + handlePaymentError, + handleSettlement, +} from "./utils"; +import { x402HTTPResourceServer } from "@x402/core/server"; + +/** + * Configuration for registering a payment scheme with a specific network + */ +export interface SchemeRegistration { + /** + * The network identifier (e.g., 'eip155:84532', 'solana:mainnet') + */ + network: Network; + + /** + * The scheme server implementation for this network + */ + server: SchemeNetworkServer; +} + +/** + * Next.js payment proxy for x402 protocol (direct HTTP server instance). + * + * Use this when you need to configure HTTP-level hooks. + * + * @param httpServer - Pre-configured x402HTTPResourceServer instance + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @returns Next.js proxy handler + * + * @example + * ```typescript + * import { paymentProxyFromHTTPServer, x402ResourceServer, x402HTTPResourceServer } from "@x402/next"; + * + * const resourceServer = new x402ResourceServer(facilitatorClient) + * .register(NETWORK, new ExactEvmScheme()); + * + * const httpServer = new x402HTTPResourceServer(resourceServer, routes) + * .onProtectedRequest(requestHook); + * + * export const proxy = paymentProxyFromHTTPServer(httpServer); + * ``` + */ +export function paymentProxyFromHTTPServer( + httpServer: x402HTTPResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +) { + const { init } = prepareHttpServer(httpServer, paywall, syncFacilitatorOnStart); + + // Dynamically register bazaar extension if routes declare it and not already registered + // Skip if pre-registered (e.g., in serverless environments where static imports are used) + let bazaarPromise: Promise | null = null; + if (checkIfBazaarNeeded(httpServer.routes) && !httpServer.server.hasExtension("bazaar")) { + bazaarPromise = import(/* webpackIgnore: true */ "@x402/extensions/bazaar") + .then(({ bazaarResourceServerExtension }) => { + httpServer.server.registerExtension(bazaarResourceServerExtension); + }) + .catch(err => { + console.error("Failed to load bazaar extension:", err); + }); + } + + return async (req: NextRequest) => { + const context = createRequestContext(req); + + // Check if route requires payment before initializing facilitator + if (!httpServer.requiresPayment(context)) { + return NextResponse.next(); + } + + // Only initialize when processing a protected route + await init(); + + // Await bazaar extension loading if needed + if (bazaarPromise) { + await bazaarPromise; + bazaarPromise = null; + } + + // Process payment requirement check + const result = await httpServer.processHTTPRequest(context, paywallConfig); + + // Handle the different result types + switch (result.type) { + case "no-payment-required": + // No payment needed, proceed directly to the route handler + return NextResponse.next(); + + case "payment-error": + return handlePaymentError(result.response); + + case "payment-verified": { + // Payment is valid, need to wrap response for settlement + const { paymentPayload, paymentRequirements, declaredExtensions } = result; + + // Proceed to the next proxy or route handler + const nextResponse = NextResponse.next(); + return handleSettlement( + httpServer, + nextResponse, + paymentPayload, + paymentRequirements, + declaredExtensions, + ); + } + } + }; +} + +/** + * Next.js payment proxy for x402 protocol (direct server instance). + * + * Use this when you want to pass a pre-configured x402ResourceServer instance. + * This provides more flexibility for testing, custom configuration, and reusing + * server instances across multiple proxies. + * + * @param routes - Route configurations for protected endpoints + * @param server - Pre-configured x402ResourceServer instance + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @returns Next.js proxy handler + * + * @example + * ```typescript + * import { paymentProxy } from "@x402/next"; + * + * const server = new x402ResourceServer(myFacilitatorClient) + * .register(NETWORK, new ExactEvmScheme()); + * + * export const proxy = paymentProxy(routes, server, paywallConfig); + * ``` + */ +export function paymentProxy( + routes: RoutesConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +) { + // Create the x402 HTTP server instance with the resource server + const httpServer = new x402HTTPResourceServer(server, routes); + + return paymentProxyFromHTTPServer(httpServer, paywallConfig, paywall, syncFacilitatorOnStart); +} + +/** + * Next.js payment proxy for x402 protocol (configuration-based). + * + * Use this when you want to quickly set up proxy with simple configuration. + * This function creates and configures the x402ResourceServer internally. + * + * @param routes - Route configurations for protected endpoints + * @param facilitatorClients - Optional facilitator client(s) for payment processing + * @param schemes - Optional array of scheme registrations for server-side payment processing + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @returns Next.js proxy handler + * + * @example + * ```typescript + * import { paymentProxyFromConfig } from "@x402/next"; + * + * export const proxy = paymentProxyFromConfig( + * routes, + * myFacilitatorClient, + * [{ network: "eip155:8453", server: evmSchemeServer }], + * paywallConfig + * ); + * ``` + */ +export function paymentProxyFromConfig( + routes: RoutesConfig, + facilitatorClients?: FacilitatorClient | FacilitatorClient[], + schemes?: SchemeRegistration[], + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +) { + const ResourceServer = new x402ResourceServer(facilitatorClients); + + if (schemes) { + schemes.forEach(({ network, server: schemeServer }) => { + ResourceServer.register(network, schemeServer); + }); + } + + // Use the direct paymentProxy with the configured server + // Note: paymentProxy handles dynamic bazaar registration + return paymentProxy(routes, ResourceServer, paywallConfig, paywall, syncFacilitatorOnStart); +} + +/** + * Wraps a Next.js App Router API route handler with x402 payment protection (HTTP server instance). + * + * Use this when you need to configure HTTP-level hooks. + * + * @param routeHandler - The API route handler function to wrap + * @param httpServer - Pre-configured x402HTTPResourceServer instance + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @returns A wrapped Next.js route handler + * + * @example + * ```typescript + * import { NextRequest, NextResponse } from "next/server"; + * import { withX402FromHTTPServer, x402ResourceServer, x402HTTPResourceServer } from "@x402/next"; + * + * const resourceServer = new x402ResourceServer(facilitatorClient) + * .register(NETWORK, new ExactEvmScheme()); + * + * const httpServer = new x402HTTPResourceServer(resourceServer, { "*": routeConfig }) + * .onProtectedRequest(requestHook); + * + * const handler = async (request: NextRequest) => { + * return NextResponse.json({ data: "protected content" }); + * }; + * + * export const GET = withX402FromHTTPServer(handler, httpServer); + * ``` + */ +export function withX402FromHTTPServer( + routeHandler: (request: NextRequest) => Promise>, + httpServer: x402HTTPResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +): (request: NextRequest) => Promise> { + const { init } = prepareHttpServer(httpServer, paywall, syncFacilitatorOnStart); + + // Dynamically register bazaar extension if route declares it and not already registered + // Skip if pre-registered (e.g., in serverless environments where static imports are used) + let bazaarPromise: Promise | null = null; + if (checkIfBazaarNeeded(httpServer.routes) && !httpServer.server.hasExtension("bazaar")) { + bazaarPromise = import(/* webpackIgnore: true */ "@x402/extensions/bazaar") + .then(({ bazaarResourceServerExtension }) => { + httpServer.server.registerExtension(bazaarResourceServerExtension); + }) + .catch(err => { + console.error("Failed to load bazaar extension:", err); + }); + } + + return async (request: NextRequest): Promise> => { + // Only initialize when processing a protected route + await init(); + + // Await bazaar extension loading if needed + if (bazaarPromise) { + await bazaarPromise; + bazaarPromise = null; + } + + const context = createRequestContext(request); + + // Process payment requirement check + const result = await httpServer.processHTTPRequest(context, paywallConfig); + + // Handle the different result types + switch (result.type) { + case "no-payment-required": + // No payment needed, proceed directly to the route handler + return routeHandler(request); + + case "payment-error": + return handlePaymentError(result.response) as NextResponse; + + case "payment-verified": { + // Payment is valid, need to wrap response for settlement + const { paymentPayload, paymentRequirements, declaredExtensions } = result; + const handlerResponse = await routeHandler(request); + return handleSettlement( + httpServer, + handlerResponse, + paymentPayload, + paymentRequirements, + declaredExtensions, + ) as Promise>; + } + } + }; +} + +/** + * Wraps a Next.js App Router API route handler with x402 payment protection. + * + * Unlike `paymentProxy` which works as middleware, `withX402` wraps individual route handlers + * and guarantees that payment settlement only occurs after the handler returns a successful + * response (status < 400). This provides more precise control over when payments are settled. + * + * @param routeHandler - The API route handler function to wrap + * @param routeConfig - Payment configuration for this specific route + * @param server - Pre-configured x402ResourceServer instance + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * @returns A wrapped Next.js route handler + * + * @example + * ```typescript + * import { NextRequest, NextResponse } from "next/server"; + * import { withX402 } from "@x402/next"; + * + * const server = new x402ResourceServer(myFacilitatorClient) + * .register(NETWORK, new ExactEvmScheme()); + * + * const handler = async (request: NextRequest) => { + * return NextResponse.json({ data: "protected content" }); + * }; + * + * export const GET = withX402( + * handler, + * { + * accepts: { + * scheme: "exact", + * payTo: "0x123...", + * price: "$0.01", + * network: "eip155:84532", + * }, + * description: "Access to protected API", + * }, + * server, + * ); + * ``` + */ +export function withX402( + routeHandler: (request: NextRequest) => Promise>, + routeConfig: RouteConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +): (request: NextRequest) => Promise> { + const routes = { "*": routeConfig }; + // Create the x402 HTTP server instance with the resource server + const httpServer = new x402HTTPResourceServer(server, routes); + + return withX402FromHTTPServer( + routeHandler, + httpServer, + paywallConfig, + paywall, + syncFacilitatorOnStart, + ); +} + +/** + * Check if any routes in the configuration declare bazaar extensions + * + * @param routes - Route configuration + * @returns True if any route has extensions.bazaar defined + */ +function checkIfBazaarNeeded(routes: RoutesConfig): boolean { + // Handle single route config + if ("accepts" in routes) { + return !!(routes.extensions && "bazaar" in routes.extensions); + } + + // Handle multiple routes + return Object.values(routes).some(routeConfig => { + return !!(routeConfig.extensions && "bazaar" in routeConfig.extensions); + }); +} + +export type { + PaymentRequired, + PaymentRequirements, + PaymentPayload, + Network, + SchemeNetworkServer, +} from "@x402/core/types"; + +export type { PaywallProvider, PaywallConfig, RouteConfig } from "@x402/core/server"; + +export { + x402ResourceServer, + x402HTTPResourceServer, + RouteConfigurationError, +} from "@x402/core/server"; + +export type { RouteValidationError } from "@x402/core/server"; + +export { NextAdapter } from "./adapter"; diff --git a/typescript/packages/http/next/src/utils.test.ts b/typescript/packages/http/next/src/utils.test.ts new file mode 100644 index 0000000..78dd4af --- /dev/null +++ b/typescript/packages/http/next/src/utils.test.ts @@ -0,0 +1,300 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { NextRequest, NextResponse } from "next/server"; +import type { + x402HTTPResourceServer, + x402ResourceServer, + PaywallProvider, +} from "@x402/core/server"; +import type { PaymentPayload, PaymentRequirements } from "@x402/core/types"; +import { + createHttpServer, + createRequestContext, + handlePaymentError, + handleSettlement, +} from "./utils"; + +// Mock @x402/core/server +vi.mock("@x402/core/server", () => { + const MockHTTPResourceServer = vi.fn().mockImplementation(() => ({ + initialize: vi.fn().mockResolvedValue(undefined), + registerPaywallProvider: vi.fn(), + processSettlement: vi.fn(), + requiresPayment: vi.fn().mockReturnValue(true), + })); + return { + x402HTTPResourceServer: MockHTTPResourceServer, + x402ResourceServer: vi.fn(), + }; +}); + +/** + * Factory for creating mock NextRequest. + * + * @param options - Configuration options for the mock request. + * @param options.url - The request URL. + * @param options.method - The HTTP method. + * @param options.headers - Request headers. + * @returns A mock NextRequest. + */ +function createMockRequest( + options: { + url?: string; + method?: string; + headers?: Record; + } = {}, +): NextRequest { + const url = options.url || "https://example.com/api/test"; + return new NextRequest(url, { + method: options.method || "GET", + headers: options.headers, + }); +} + +/** + * Factory for creating a mock x402ResourceServer. + * + * @returns A mock x402ResourceServer. + */ +function createMockResourceServer(): x402ResourceServer { + return { + initialize: vi.fn().mockResolvedValue(undefined), + } as unknown as x402ResourceServer; +} + +describe("createHttpServer", () => { + it("creates server and initializes on start by default", async () => { + const routes = { + "/api/*": { + accepts: { scheme: "exact", payTo: "0x123", price: "$0.01", network: "eip155:84532" }, + }, + } as const; + const server = createMockResourceServer(); + + const { httpServer, init } = createHttpServer(routes, server); + + expect(httpServer).toBeDefined(); + await init(); + // httpServer.initialize() is called (which internally calls server.initialize() and validates) + expect(httpServer.initialize).toHaveBeenCalled(); + }); + + it("does not initialize when syncFacilitatorOnStart is false", async () => { + const routes = { + "/api/*": { + accepts: { scheme: "exact", payTo: "0x123", price: "$0.01", network: "eip155:84532" }, + }, + } as const; + const server = createMockResourceServer(); + + const { httpServer, init } = createHttpServer(routes, server, undefined, false); + + await init(); + expect(httpServer.initialize).not.toHaveBeenCalled(); + }); + + it("registers custom paywall provider when provided", async () => { + const routes = { + "/api/*": { + accepts: { scheme: "exact", payTo: "0x123", price: "$0.01", network: "eip155:84532" }, + }, + } as const; + const server = createMockResourceServer(); + const paywall: PaywallProvider = { generateHtml: vi.fn() }; + + const { httpServer, init } = createHttpServer(routes, server, paywall); + + // Wait for initialization to complete to avoid warnings + await init(); + expect(httpServer.registerPaywallProvider).toHaveBeenCalledWith(paywall); + }); +}); + +describe("createRequestContext", () => { + it("extracts path and method from request", () => { + const req = createMockRequest({ url: "https://example.com/api/weather", method: "POST" }); + + const context = createRequestContext(req); + + expect(context.path).toBe("/api/weather"); + expect(context.method).toBe("POST"); + expect(context.adapter).toBeDefined(); + }); + + it("extracts x-payment header", () => { + const req = createMockRequest({ headers: { "X-Payment": "payment-data" } }); + + const context = createRequestContext(req); + + expect(context.paymentHeader).toBe("payment-data"); + }); + + it("extracts payment-signature header (v2)", () => { + const req = createMockRequest({ headers: { "Payment-Signature": "sig-data" } }); + + const context = createRequestContext(req); + + expect(context.paymentHeader).toBe("sig-data"); + }); + + it("prefers payment-signature over x-payment", () => { + const req = createMockRequest({ + headers: { "Payment-Signature": "sig-data", "X-Payment": "x-payment-data" }, + }); + + const context = createRequestContext(req); + + expect(context.paymentHeader).toBe("sig-data"); + }); + + it("returns undefined paymentHeader when no payment headers present", () => { + const req = createMockRequest(); + + const context = createRequestContext(req); + + expect(context.paymentHeader).toBeUndefined(); + }); +}); + +describe("handlePaymentError", () => { + it("returns HTML response when isHtml is true", () => { + const response = handlePaymentError({ + status: 402, + body: "Paywall", + headers: { "X-Custom": "value" }, + isHtml: true, + }); + + expect(response.status).toBe(402); + expect(response.headers.get("Content-Type")).toBe("text/html"); + expect(response.headers.get("X-Custom")).toBe("value"); + }); + + it("returns JSON response when isHtml is false", async () => { + const body = { error: "Payment required", accepts: [] }; + const response = handlePaymentError({ + status: 402, + body, + headers: {}, + isHtml: false, + }); + + expect(response.status).toBe(402); + expect(response.headers.get("Content-Type")).toBe("application/json"); + expect(await response.json()).toEqual(body); + }); + + it("handles empty body in JSON response", async () => { + const response = handlePaymentError({ + status: 402, + headers: {}, + }); + + expect(await response.json()).toEqual({}); + }); +}); + +describe("handleSettlement", () => { + let mockHttpServer: x402HTTPResourceServer; + const mockPaymentPayload = { + scheme: "exact", + network: "eip155:84532", + } as unknown as PaymentPayload; + const mockRequirements = { + scheme: "exact", + network: "eip155:84532", + } as unknown as PaymentRequirements; + + beforeEach(() => { + mockHttpServer = { + processSettlement: vi + .fn() + .mockResolvedValue({ success: true, headers: { "PAYMENT-RESPONSE": "settled" } }), + } as unknown as x402HTTPResourceServer; + }); + + it("returns original response when status >= 400 without settling", async () => { + const response = new NextResponse("Error", { status: 500 }); + + const result = await handleSettlement( + mockHttpServer, + response, + mockPaymentPayload, + mockRequirements, + ); + + expect(result.status).toBe(500); + expect(mockHttpServer.processSettlement).not.toHaveBeenCalled(); + }); + + it("returns original response when status is exactly 400", async () => { + const response = new NextResponse("Bad Request", { status: 400 }); + + const result = await handleSettlement( + mockHttpServer, + response, + mockPaymentPayload, + mockRequirements, + ); + + expect(result.status).toBe(400); + expect(mockHttpServer.processSettlement).not.toHaveBeenCalled(); + }); + + it("adds settlement headers on successful settlement", async () => { + const response = new NextResponse("OK", { status: 200 }); + + const result = await handleSettlement( + mockHttpServer, + response, + mockPaymentPayload, + mockRequirements, + ); + + expect(result.status).toBe(200); + expect(result.headers.get("PAYMENT-RESPONSE")).toBe("settled"); + expect(mockHttpServer.processSettlement).toHaveBeenCalledWith( + mockPaymentPayload, + mockRequirements, + undefined, + ); + }); + + it("returns 402 error response when settlement returns failure", async () => { + vi.mocked(mockHttpServer.processSettlement).mockResolvedValue({ + success: false, + errorReason: "Insufficient funds", + transaction: "", + network: "eip155:84532", + }); + const response = new NextResponse("OK", { status: 200 }); + + const result = await handleSettlement( + mockHttpServer, + response, + mockPaymentPayload, + mockRequirements, + ); + + expect(result.status).toBe(402); + const body = (await result.json()) as { error: string; details: string }; + expect(body.error).toBe("Settlement failed"); + expect(body.details).toBe("Insufficient funds"); + }); + + it("returns 402 error response when settlement throws", async () => { + vi.mocked(mockHttpServer.processSettlement).mockRejectedValue(new Error("Settlement rejected")); + const response = new NextResponse("OK", { status: 200 }); + + const result = await handleSettlement( + mockHttpServer, + response, + mockPaymentPayload, + mockRequirements, + ); + + expect(result.status).toBe(402); + const body = (await result.json()) as { error: string; details: string }; + expect(body.error).toBe("Settlement failed"); + expect(body.details).toBe("Settlement rejected"); + }); +}); diff --git a/typescript/packages/http/next/src/utils.ts b/typescript/packages/http/next/src/utils.ts new file mode 100644 index 0000000..1e5ec9c --- /dev/null +++ b/typescript/packages/http/next/src/utils.ts @@ -0,0 +1,179 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + HTTPRequestContext, + HTTPResponseInstructions, + PaywallProvider, + x402HTTPResourceServer, + x402ResourceServer, + RoutesConfig, +} from "@x402/core/server"; +import { PaymentPayload, PaymentRequirements } from "@x402/core/types"; +import { NextAdapter } from "./adapter"; + +/** + * Result of createHttpServer + */ +export interface HttpServerInstance { + httpServer: x402HTTPResourceServer; + init: () => Promise; +} + +/** + * Prepares an existing x402HTTPResourceServer with initialization logic + * + * @param httpServer - Pre-configured x402HTTPResourceServer instance + * @param paywall - Optional paywall provider for custom payment UI + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on start (defaults to true) + * @returns The HTTP server instance with initialization function + */ +export function prepareHttpServer( + httpServer: x402HTTPResourceServer, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +): HttpServerInstance { + // Register custom paywall provider if provided + if (paywall) { + httpServer.registerPaywallProvider(paywall); + } + + // Store initialization promise (not the result) + // httpServer.initialize() fetches facilitator support and validates routes + let initPromise: Promise | null = syncFacilitatorOnStart ? httpServer.initialize() : null; + + return { + httpServer, + async init() { + // Ensure initialization completes before processing + if (initPromise) { + await initPromise; + initPromise = null; // Clear after first await + } + }, + }; +} + +/** + * Creates and configures the x402 HTTP server with initialization logic + * + * @param routes - The route configuration for the server + * @param server - The x402 resource server instance + * @param paywall - Optional paywall provider for custom payment UI + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on start (defaults to true) + * @returns The HTTP server instance with initialization function + */ +export function createHttpServer( + routes: RoutesConfig, + server: x402ResourceServer, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +): HttpServerInstance { + // Create the x402 HTTP server instance with the resource server + const httpServer = new x402HTTPResourceServer(server, routes); + + return prepareHttpServer(httpServer, paywall, syncFacilitatorOnStart); +} + +/** + * Creates HTTP request context from a Next.js request + * + * @param request - The Next.js request object + * @returns The HTTP request context for x402 processing + */ +export function createRequestContext(request: NextRequest): HTTPRequestContext { + // Create adapter and context + const adapter = new NextAdapter(request); + return { + adapter, + path: request.nextUrl.pathname, + method: request.method, + paymentHeader: adapter.getHeader("payment-signature") || adapter.getHeader("x-payment"), + }; +} + +/** + * Handles payment error result by creating a 402 response + * + * @param response - The HTTP response instructions from payment verification + * @returns A Next.js response with the appropriate 402 status and headers + */ +export function handlePaymentError(response: HTTPResponseInstructions): NextResponse { + // Payment required but not provided or invalid + const headers = new Headers(response.headers); + if (response.isHtml) { + headers.set("Content-Type", "text/html"); + return new NextResponse(response.body as string, { + status: response.status, + headers, + }); + } + headers.set("Content-Type", "application/json"); + return new NextResponse(JSON.stringify(response.body || {}), { + status: response.status, + headers, + }); +} + +/** + * Handles settlement after a successful response + * + * @param httpServer - The x402 HTTP resource server instance + * @param response - The Next.js response from the protected route + * @param paymentPayload - The payment payload from the client + * @param paymentRequirements - The payment requirements for the route + * @param declaredExtensions - Optional declared extensions (for per-key enrichment) + * @returns The response with settlement headers or an error response if settlement fails + */ +export async function handleSettlement( + httpServer: x402HTTPResourceServer, + response: NextResponse, + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + declaredExtensions?: Record, +): Promise { + // If the response from the protected route is >= 400, do not settle payment + if (response.status >= 400) { + return response; + } + + try { + const result = await httpServer.processSettlement( + paymentPayload, + paymentRequirements, + declaredExtensions, + ); + + if (!result.success) { + // Settlement failed - do not return the protected resource + return new NextResponse( + JSON.stringify({ + error: "Settlement failed", + details: result.errorReason, + }), + { + status: 402, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + // Settlement succeeded - add headers and return original response + Object.entries(result.headers).forEach(([key, value]) => { + response.headers.set(key, value); + }); + + return response; + } catch (error) { + console.error("Settlement failed:", error); + // If settlement fails, return an error response + return new NextResponse( + JSON.stringify({ + error: "Settlement failed", + details: error instanceof Error ? error.message : "Unknown error", + }), + { + status: 402, + headers: { "Content-Type": "application/json" }, + }, + ); + } +} diff --git a/typescript/packages/http/next/tsconfig.json b/typescript/packages/http/next/tsconfig.json new file mode 100644 index 0000000..1b119d3 --- /dev/null +++ b/typescript/packages/http/next/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "allowJs": false, + "checkJs": false + }, + "include": ["src"] +} diff --git a/typescript/packages/http/next/tsup.config.ts b/typescript/packages/http/next/tsup.config.ts new file mode 100644 index 0000000..7ac9e3a --- /dev/null +++ b/typescript/packages/http/next/tsup.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from "tsup"; + +const baseConfig = { + entry: { + index: "src/index.ts", + }, + dts: { + resolve: true, + }, + sourcemap: true, + target: "node16", + external: ["next"], +}; + +export default defineConfig([ + { + ...baseConfig, + format: "esm", + outDir: "dist/esm", + clean: true, + }, + { + ...baseConfig, + format: "cjs", + outDir: "dist/cjs", + clean: false, + }, +]); diff --git a/typescript/packages/http/next/vitest.config.ts b/typescript/packages/http/next/vitest.config.ts new file mode 100644 index 0000000..156f8c9 --- /dev/null +++ b/typescript/packages/http/next/vitest.config.ts @@ -0,0 +1,10 @@ +import { loadEnv } from "vite"; +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ""), + }, + plugins: [tsconfigPaths({ projects: ["."] })], +})); diff --git a/typescript/packages/http/paywall/.prettierignore b/typescript/packages/http/paywall/.prettierignore new file mode 100644 index 0000000..5bd240b --- /dev/null +++ b/typescript/packages/http/paywall/.prettierignore @@ -0,0 +1,8 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +src/client +**/**/*.json +*.md \ No newline at end of file diff --git a/typescript/packages/http/paywall/.prettierrc b/typescript/packages/http/paywall/.prettierrc new file mode 100644 index 0000000..ffb416b --- /dev/null +++ b/typescript/packages/http/paywall/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/packages/http/paywall/CHANGELOG.md b/typescript/packages/http/paywall/CHANGELOG.md new file mode 100644 index 0000000..88bb904 --- /dev/null +++ b/typescript/packages/http/paywall/CHANGELOG.md @@ -0,0 +1,21 @@ +# @x402/paywall Changelog + +## 2.3.0 + +### Minor Changes + +- 51b8445: Bumped @x402/core dependency to 2.3.0 + +### Patch Changes + +- Updated dependencies [51b8445] +- Updated dependencies [51b8445] + - @x402/core@2.3.0 + +## 2.0.0 + +- Implements x402 2.0.0 for the TypeScript SDK. + +## 1.0.0 + +- Implements x402 1.0.0 for the TypeScript SDK. diff --git a/typescript/packages/http/paywall/README.md b/typescript/packages/http/paywall/README.md new file mode 100644 index 0000000..caafbcc --- /dev/null +++ b/typescript/packages/http/paywall/README.md @@ -0,0 +1,204 @@ +# @x402/paywall + +Modular paywall UI for the x402 payment protocol with support for EVM and Solana networks. + +## Features + +- Pre-built paywall UI out of the box +- Wallet connection (MetaMask, Coinbase Wallet, Phantom, etc.) +- USDC balance checking +- Multi-network support (EVM + Solana) +- Tree-shakeable - only bundle what you need +- Fully customizable via builder pattern + +## Installation + +```bash +pnpm add @x402/paywall +``` + +## Bundle Sizes + +Choose the import that matches your needs: + +| Import | Size | Networks | Use Case | +|--------|------|----------|----------| +| `@x402/paywall` | 3.5MB | EVM + Solana | Multi-network apps | +| `@x402/paywall/evm` | 3.4MB | EVM only | Base, Ethereum, Polygon, etc. | +| `@x402/paywall/svm` | 1.0MB | Solana only | Solana apps | + +## Usage + +### Option 1: EVM Only + +```typescript +import { createPaywall } from '@x402/paywall'; +import { evmPaywall } from '@x402/paywall/evm'; + +const paywall = createPaywall() + .withNetwork(evmPaywall) + .withConfig({ + appName: 'My App', + testnet: true + }) + .build(); + +// Use with Express +app.use(paymentMiddleware(routes, facilitators, schemes, undefined, paywall)); +``` + +### Option 2: Solana Only + +```typescript +import { createPaywall } from '@x402/paywall'; +import { svmPaywall } from '@x402/paywall/svm'; + +const paywall = createPaywall() + .withNetwork(svmPaywall) + .withConfig({ + appName: 'My Solana App', + testnet: true + }) + .build(); +``` + +### Option 3: Multi-Network + +```typescript +import { createPaywall } from '@x402/paywall'; +import { evmPaywall } from '@x402/paywall/evm'; +import { svmPaywall } from '@x402/paywall/svm'; + +const paywall = createPaywall() + .withNetwork(evmPaywall) // First-match priority + .withNetwork(svmPaywall) // Fallback option + .withConfig({ + appName: 'Multi-chain App', + testnet: true + }) + .build(); +``` + +## Configuration + +### PaywallConfig Options + +```typescript +interface PaywallConfig { + appName?: string; // App name shown in wallet connection + appLogo?: string; // App logo URL + currentUrl?: string; // URL of protected resource + testnet?: boolean; // Use testnet (default: true) +} +``` + +## How It Works + +### First-Match Selection + +When multiple networks are registered, the paywall uses **first-match selection**: + +1. Iterates through `paymentRequired.accepts` array +2. Finds the first payment requirement that has a registered handler +3. Uses that handler to generate the HTML + +**Example:** +```typescript +// Server returns multiple options +{ + "accepts": [ + { "network": "solana:5eykt...", ... }, // First + { "network": "eip155:8453", ... } // Second + ] +} + +// If both handlers registered, Solana is selected (it's first in accepts) +const paywall = createPaywall() + .withNetwork(evmPaywall) + .withNetwork(svmPaywall) + .build(); +``` + +### Supported Networks + +**EVM Networks** (via `evmPaywall`): +- CAIP-2: `eip155:*` (e.g., `eip155:8453` for Base, `eip155:84532` for Base Sepolia) + +**Solana Networks** (via `svmPaywall`): +- CAIP-2: `solana:*` (e.g., `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` for mainnet) + +## With HTTP Middleware + +### Express + +```typescript +import express from 'express'; +import { paymentMiddleware } from '@x402/express'; +import { createPaywall } from '@x402/paywall'; +import { evmPaywall } from '@x402/paywall/evm'; + +const app = express(); + +const paywall = createPaywall() + .withNetwork(evmPaywall) + .withConfig({ appName: 'My API' }) + .build(); + +app.use(paymentMiddleware( + { "/api/premium": { price: "$0.10", network: "eip155:84532", payTo: "0x..." } }, + facilitators, + schemes, + undefined, + paywall +)); +``` + +### Automatic Detection + +If you provide `paywallConfig` without a custom paywall, `@x402/core` automatically: +1. Tries to load `@x402/paywall` if installed +2. Falls back to basic HTML if not installed + +```typescript +// Simple usage - auto-detects @x402/paywall +app.use(paymentMiddleware(routes, facilitators, schemes, { + appName: 'My App', + testnet: true +})); +``` + +## Custom Network Handlers + +You can create custom handlers for new networks: + +```typescript +import { createPaywall, type PaywallNetworkHandler } from '@x402/paywall'; + +const suiPaywall: PaywallNetworkHandler = { + supports: (req) => req.network.startsWith('sui:'), + generateHtml: (req, paymentRequired, config) => { + return `...`; // Your custom Sui paywall + } +}; + +const paywall = createPaywall() + .withNetwork(evmPaywall) + .withNetwork(svmPaywall) + .withNetwork(suiPaywall) // Custom handler + .build(); +``` + +## Development + +### Build + +```bash +pnpm build:paywall # Generate HTML templates +pnpm build # Build TypeScript +``` + +### Test + +```bash +pnpm test # Run unit tests +``` \ No newline at end of file diff --git a/typescript/packages/http/paywall/eslint.config.js b/typescript/packages/http/paywall/eslint.config.js new file mode 100644 index 0000000..6bd7b54 --- /dev/null +++ b/typescript/packages/http/paywall/eslint.config.js @@ -0,0 +1,76 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: [ + "dist/**", + "node_modules/**", + "src/gen/**", + "src/dist/**", + "src/evm/gen/**", + "src/evm/dist/**", + "src/svm/gen/**", + "src/svm/dist/**", + ], + }, + { + files: ["**/*.ts", "**/*.tsx"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + window: "readonly", + document: "readonly", + console: "readonly", + fetch: "readonly", + Response: "readonly", + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + BufferEncoding: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error"], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": "off", + "jsdoc/require-param": "off", + "jsdoc/require-param-description": "off", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/typescript/packages/http/paywall/package.json b/typescript/packages/http/paywall/package.json new file mode 100644 index 0000000..31bda28 --- /dev/null +++ b/typescript/packages/http/paywall/package.json @@ -0,0 +1,114 @@ +{ + "name": "@x402/paywall", + "version": "2.3.0", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/index.d.ts", + "type": "module", + "scripts": { + "start": "tsx --env-file=.env index.ts", + "test": "vitest run", + "test:watch": "vitest", + "build": "tsup", + "build:paywall": "tsx src/evm/build.ts && tsx src/svm/build.ts", + "watch": "tsc --watch", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "keywords": [ + "x402", + "paywall", + "payment", + "http-402" + ], + "license": "Apache-2.0", + "author": "Coinbase Inc.", + "repository": "https://github.com/coinbase/x402", + "description": "x402 Payment Protocol Paywall UI", + "devDependencies": { + "@craftamap/esbuild-plugin-html": "^0.9.0", + "@eslint/js": "^9.24.0", + "@types/node": "^22.13.4", + "@types/react": "^19", + "@types/react-dom": "^19", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "@x402/evm": "workspace:~", + "@x402/svm": "workspace:~", + "buffer": "^6.0.3", + "esbuild": "^0.25.4", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tsup": "^8.4.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vite": "^6.2.6", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.5" + }, + "dependencies": { + "@scure/base": "^1.2.6", + "@solana-program/compute-budget": "^0.8.0", + "@solana-program/token": "^0.5.1", + "@solana-program/token-2022": "^0.4.2", + "@solana/kit": "^2.1.1", + "@solana/transaction-confirmation": "^2.1.1", + "@solana/wallet-standard-features": "^1.3.0", + "@tanstack/react-query": "^5.90.7", + "@wagmi/connectors": "^5.8.1", + "@wagmi/core": "^2.17.1", + "@wallet-standard/app": "^1.1.0", + "@wallet-standard/base": "^1.1.0", + "@wallet-standard/features": "^1.1.0", + "@x402/core": "workspace:~", + "viem": "^2.39.3", + "wagmi": "^2.17.1", + "zod": "^3.24.2" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + }, + "./evm": { + "import": { + "types": "./dist/esm/evm/index.d.ts", + "default": "./dist/esm/evm/index.js" + }, + "require": { + "types": "./dist/cjs/evm/index.d.cts", + "default": "./dist/cjs/evm/index.cjs" + } + }, + "./svm": { + "import": { + "types": "./dist/esm/svm/index.d.ts", + "default": "./dist/esm/svm/index.js" + }, + "require": { + "types": "./dist/cjs/svm/index.d.cts", + "default": "./dist/cjs/svm/index.cjs" + } + } + }, + "files": [ + "dist" + ] +} diff --git a/typescript/packages/x402/src/paywall/src/PaywallApp.tsx b/typescript/packages/http/paywall/src/PaywallApp.tsx similarity index 64% rename from typescript/packages/x402/src/paywall/src/PaywallApp.tsx rename to typescript/packages/http/paywall/src/PaywallApp.tsx index 21e56b8..2abc023 100644 --- a/typescript/packages/x402/src/paywall/src/PaywallApp.tsx +++ b/typescript/packages/http/paywall/src/PaywallApp.tsx @@ -1,10 +1,10 @@ "use client"; -import { useCallback, useMemo } from "react"; -import type { PaymentRequirements } from "../../types/verify"; -import { choosePaymentRequirement, isEvmNetwork, isSvmNetwork } from "./paywallUtils"; -import { EvmPaywall } from "./EvmPaywall"; -import { SolanaPaywall } from "./SolanaPaywall"; +import { useCallback } from "react"; +import type { PaymentRequired } from "@x402/core/types"; +import { isEvmNetwork, isSvmNetwork } from "./paywallUtils"; +import { EvmPaywall } from "./evm/EvmPaywall"; +import { SolanaPaywall } from "./svm/SolanaPaywall"; /** * Main Paywall App Component @@ -13,11 +13,7 @@ import { SolanaPaywall } from "./SolanaPaywall"; */ export function PaywallApp() { const x402 = window.x402; - const testnet = x402.testnet ?? true; - - const paymentRequirement = useMemo(() => { - return choosePaymentRequirement(x402.paymentRequirements, testnet); - }, [testnet, x402.paymentRequirements]); + const paymentRequired: PaymentRequired = x402.paymentRequired; const handleSuccessfulResponse = useCallback(async (response: Response) => { const contentType = response.headers.get("content-type"); @@ -30,7 +26,7 @@ export function PaywallApp() { } }, []); - if (!paymentRequirement) { + if (!paymentRequired || !paymentRequired.accepts || paymentRequired.accepts.length === 0) { return (
@@ -41,19 +37,22 @@ export function PaywallApp() { ); } - if (isEvmNetwork(paymentRequirement.network)) { + const firstRequirement = paymentRequired.accepts[0]; + const network = firstRequirement.network; + + if (isEvmNetwork(network)) { return ( ); } - if (isSvmNetwork(paymentRequirement.network)) { + if (isSvmNetwork(network)) { return ( ); diff --git a/typescript/packages/http/paywall/src/Providers.tsx b/typescript/packages/http/paywall/src/Providers.tsx new file mode 100644 index 0000000..1a4150d --- /dev/null +++ b/typescript/packages/http/paywall/src/Providers.tsx @@ -0,0 +1,62 @@ +import type { ReactNode } from "react"; +import { WagmiProvider, createConfig, http } from "wagmi"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { injected, coinbaseWallet } from "wagmi/connectors"; +import * as allChains from "viem/chains"; +import type { Chain } from "viem"; +import { isEvmNetwork } from "./paywallUtils"; + +type ProvidersProps = { + children: ReactNode; +}; + +// Create QueryClient for React Query +const queryClient = new QueryClient(); + +/** + * Providers component for the paywall + * Sets up Wagmi and React Query for wallet connectivity + * + * @param props - The component props + * @param props.children - The children of the Providers component + * @returns The Providers component + */ +export function Providers({ children }: ProvidersProps) { + const { paymentRequired } = window.x402; + + // Determine which chain to connect to + let targetChain: Chain = allChains.base; // Default to Base + + if (paymentRequired?.accepts?.[0]) { + const firstRequirement = paymentRequired.accepts[0]; + const network = firstRequirement.network; + + if (isEvmNetwork(network)) { + const chainId = parseInt(network.split(":")[1]); + const chain: Chain | undefined = Object.values(allChains).find(c => c.id === chainId); + if (chain) { + targetChain = chain; + } + } + } + + // Create Wagmi config + const config = createConfig({ + chains: [targetChain], + connectors: [ + injected(), + coinbaseWallet({ + appName: window.x402.appName || "x402 Paywall", + }), + ], + transports: { + [targetChain.id]: http(), + }, + }); + + return ( + + {children} + + ); +} diff --git a/typescript/packages/x402/src/paywall/baseTemplate.ts b/typescript/packages/http/paywall/src/baseTemplate.ts similarity index 100% rename from typescript/packages/x402/src/paywall/baseTemplate.ts rename to typescript/packages/http/paywall/src/baseTemplate.ts diff --git a/typescript/packages/x402/src/paywall/buffer-polyfill.ts b/typescript/packages/http/paywall/src/buffer-polyfill.ts similarity index 100% rename from typescript/packages/x402/src/paywall/buffer-polyfill.ts rename to typescript/packages/http/paywall/src/buffer-polyfill.ts diff --git a/typescript/packages/http/paywall/src/builder.test.ts b/typescript/packages/http/paywall/src/builder.test.ts new file mode 100644 index 0000000..a365b3b --- /dev/null +++ b/typescript/packages/http/paywall/src/builder.test.ts @@ -0,0 +1,228 @@ +import { describe, expect, it } from "vitest"; +import { createPaywall, PaywallBuilder } from "./builder"; +import type { PaymentRequired } from "./types"; +import { evmPaywall } from "./evm"; +import { svmPaywall } from "./svm"; + +const mockPaymentRequired: PaymentRequired = { + x402Version: 2, + error: "Payment required", + resource: { + url: "https://example.com/api/data", + description: "Test Resource", + mimeType: "application/json", + }, + accepts: [ + { + scheme: "exact", + network: "eip155:84532", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + amount: "100000", + payTo: "0x209693Bc6afc0C5328bA36FaF04C514EF312287C", + maxTimeoutSeconds: 60, + }, + ], +}; + +describe("PaywallBuilder", () => { + describe("createPaywall", () => { + it("creates a new PaywallBuilder instance", () => { + const builder = createPaywall(); + expect(builder).toBeInstanceOf(PaywallBuilder); + }); + }); + + describe("withConfig", () => { + it("sets configuration and returns builder for chaining", () => { + const builder = createPaywall(); + const result = builder.withConfig({ + appName: "Test App", + appLogo: "/test-logo.png", + }); + expect(result).toBe(builder); // Same instance (chainable) + }); + + it("merges multiple config calls", () => { + const paywall = createPaywall() + .withNetwork(evmPaywall) + .withConfig({ appName: "App 1" }) + .withConfig({ appLogo: "/logo-1.png" }) + .build(); + + const html = paywall.generateHtml(mockPaymentRequired); + expect(html).toContain("App 1"); + expect(html).toContain("/logo-1.png"); + }); + + it("later configs override earlier ones", () => { + const paywall = createPaywall() + .withNetwork(evmPaywall) + .withConfig({ appName: "First App" }) + .withConfig({ appName: "Second App" }) + .build(); + + const html = paywall.generateHtml(mockPaymentRequired); + expect(html).toContain("Second App"); + expect(html).not.toContain("First App"); + }); + }); + + describe("build", () => { + it("returns a PaywallProvider", () => { + const provider = createPaywall().build(); + expect(provider).toHaveProperty("generateHtml"); + expect(typeof provider.generateHtml).toBe("function"); + }); + + it("generates HTML with builder config", () => { + const paywall = createPaywall() + .withNetwork(evmPaywall) + .withConfig({ + appName: "Builder Test", + testnet: true, + }) + .build(); + + const html = paywall.generateHtml(mockPaymentRequired); + expect(html).toContain(""); + expect(html).toContain("Builder Test"); + }); + + it("runtime config overrides builder config", () => { + const paywall = createPaywall() + .withNetwork(evmPaywall) + .withConfig({ + appName: "Builder Config", + }) + .build(); + + const html = paywall.generateHtml(mockPaymentRequired, { + appName: "Runtime Config", + }); + + expect(html).toContain("Runtime Config"); + expect(html).not.toContain("Builder Config"); + }); + + it("merges builder config with runtime config", () => { + const paywall = createPaywall() + .withNetwork(evmPaywall) + .withConfig({ + appName: "Test App", + testnet: true, + }) + .build(); + + const html = paywall.generateHtml(mockPaymentRequired, { + appLogo: "/runtime-logo.png", + }); + + expect(html).toContain("Test App"); + expect(html).toContain("/runtime-logo.png"); + }); + }); + + describe("generateHtml", () => { + it("extracts amount from v2 payment requirements", () => { + const paywall = createPaywall().withNetwork(evmPaywall).build(); + const html = paywall.generateHtml(mockPaymentRequired); + + expect(html).toContain("window.x402"); + expect(html).toContain("0.1"); + }); + + it("uses resource URL as currentUrl when not provided", () => { + const paywall = createPaywall().withNetwork(evmPaywall).build(); + const html = paywall.generateHtml(mockPaymentRequired); + + expect(html).toContain("https://example.com/api/data"); + }); + + it("defaults to testnet when not specified", () => { + const paywall = createPaywall().withNetwork(evmPaywall).build(); + const html = paywall.generateHtml(mockPaymentRequired); + + expect(html).toContain("testnet: true"); + }); + }); + + describe("withNetwork", () => { + it("registers network handler and returns builder for chaining", () => { + const builder = createPaywall(); + const result = builder.withNetwork(evmPaywall); + expect(result).toBe(builder); // Same instance (chainable) + }); + + it("uses first-match selection from accepts array", () => { + const multiNetworkPaymentRequired: PaymentRequired = { + ...mockPaymentRequired, + accepts: [ + // Solana is first in accepts array + { + scheme: "exact", + network: "solana:5eykt", + asset: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + amount: "100000", + payTo: "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHEBg4", + maxTimeoutSeconds: 60, + }, + { + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + amount: "100000", + payTo: "0x209693Bc6afc0C5328bA36FaF04C514EF312287C", + maxTimeoutSeconds: 60, + }, + ], + }; + + const paywall = createPaywall().withNetwork(evmPaywall).withNetwork(svmPaywall).build(); + + const html = paywall.generateHtml(multiNetworkPaymentRequired); + + // Should match first requirement in accepts array (Solana) + expect(html).toMatch(/SVM Paywall/); + }); + + it("throws when no handler matches", () => { + const customNetworkRequired: PaymentRequired = { + ...mockPaymentRequired, + accepts: [ + { + scheme: "exact", + network: "unknown:network", + asset: "0x123", + amount: "100000", + payTo: "0x456", + maxTimeoutSeconds: 60, + }, + ], + }; + + const paywall = createPaywall().withNetwork(evmPaywall).withNetwork(svmPaywall).build(); + + expect(() => paywall.generateHtml(customNetworkRequired)).toThrow( + "No paywall handler supports networks: unknown:network", + ); + }); + + it("only uses network handler when registered", () => { + const evmOnlyPaywall = createPaywall().withNetwork(evmPaywall).build(); + + const html = evmOnlyPaywall.generateHtml(mockPaymentRequired); + + expect(html).toContain(""); + }); + + it("can chain multiple networks", () => { + const paywall = createPaywall() + .withNetwork(evmPaywall) + .withNetwork(svmPaywall) + .withConfig({ appName: "Multi-chain App" }) + .build(); + + expect(paywall).toHaveProperty("generateHtml"); + }); + }); +}); diff --git a/typescript/packages/http/paywall/src/builder.ts b/typescript/packages/http/paywall/src/builder.ts new file mode 100644 index 0000000..2cbb421 --- /dev/null +++ b/typescript/packages/http/paywall/src/builder.ts @@ -0,0 +1,80 @@ +import type { + PaywallConfig, + PaywallProvider, + PaymentRequired, + PaywallNetworkHandler, +} from "./types"; + +/** + * Builder for creating configured paywall providers + */ +export class PaywallBuilder { + private config: PaywallConfig = {}; + private handlers: PaywallNetworkHandler[] = []; + + /** + * Register a network-specific paywall handler + * + * @param handler - Network handler to register + * @returns This builder instance for chaining + */ + withNetwork(handler: PaywallNetworkHandler): this { + this.handlers.push(handler); + return this; + } + + /** + * Set configuration options for the paywall + * + * @param config - Paywall configuration options + * @returns This builder instance for chaining + */ + withConfig(config: PaywallConfig): this { + this.config = { ...this.config, ...config }; + return this; + } + + /** + * Build the paywall provider + * + * @returns A configured PaywallProvider instance + */ + build(): PaywallProvider { + const builderConfig = this.config; + const handlers = this.handlers; + + return { + generateHtml: (paymentRequired: PaymentRequired, runtimeConfig?: PaywallConfig): string => { + // Merge builder config with runtime config (runtime takes precedence) + const finalConfig = { ...builderConfig, ...runtimeConfig }; + + if (handlers.length === 0) { + throw new Error( + "No paywall handlers registered. Use .withNetwork(evmPaywall) or .withNetwork(svmPaywall)", + ); + } + + for (const requirement of paymentRequired.accepts) { + const handler = handlers.find(h => h.supports(requirement)); + if (handler) { + return handler.generateHtml(requirement, paymentRequired, finalConfig); + } + } + + const networks = paymentRequired.accepts.map(r => r.network).join(", "); + throw new Error( + `No paywall handler supports networks: ${networks}. Register appropriate handlers with .withNetwork()`, + ); + }, + }; + } +} + +/** + * Create a new paywall builder + * + * @returns A new PaywallBuilder instance + */ +export function createPaywall(): PaywallBuilder { + return new PaywallBuilder(); +} diff --git a/typescript/packages/x402/src/paywall/src/EvmPaywall.tsx b/typescript/packages/http/paywall/src/evm/EvmPaywall.tsx similarity index 58% rename from typescript/packages/x402/src/paywall/src/EvmPaywall.tsx rename to typescript/packages/http/paywall/src/evm/EvmPaywall.tsx index 3988f54..d3be221 100644 --- a/typescript/packages/x402/src/paywall/src/EvmPaywall.tsx +++ b/typescript/packages/http/paywall/src/evm/EvmPaywall.tsx @@ -1,28 +1,19 @@ -import { FundButton, getOnrampBuyUrl } from "@coinbase/onchainkit/fund"; -import { Avatar, Name } from "@coinbase/onchainkit/identity"; -import { - ConnectWallet, - Wallet, - WalletDropdown, - WalletDropdownDisconnect, -} from "@coinbase/onchainkit/wallet"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { createPublicClient, formatUnits, http, publicActions } from "viem"; -import { base, baseSepolia } from "viem/chains"; -import { useAccount, useSwitchChain, useWalletClient } from "wagmi"; +import { createPublicClient, formatUnits, http, publicActions, type Chain } from "viem"; +import * as allChains from "viem/chains"; +import { useAccount, useSwitchChain, useWalletClient, useConnect, useDisconnect } from "wagmi"; -import type { PaymentRequirements } from "../../types/verify"; -import { exact } from "../../schemes"; -import { getUSDCBalance } from "../../shared/evm"; -import type { Network } from "../../types/shared"; +import { registerExactEvmScheme } from "@x402/evm/exact/client"; +import { x402Client } from "@x402/core/client"; +import type { PaymentRequired } from "@x402/core/types"; +import { getUSDCBalance } from "./utils"; import { Spinner } from "./Spinner"; -import { useOnrampSessionToken } from "./useOnrampSessionToken"; -import { ensureValidAmount } from "./utils"; -import { getNetworkDisplayName, isTestnetNetwork } from "./paywallUtils"; +import { getNetworkDisplayName, isTestnetNetwork } from "../paywallUtils"; +import { wagmiToClientSigner } from "./browserAdapter"; type EvmPaywallProps = { - paymentRequirement: PaymentRequirements; + paymentRequired: PaymentRequired; onSuccessfulResponse: (response: Response) => Promise; }; @@ -30,34 +21,44 @@ type EvmPaywallProps = { * Paywall experience for EVM networks. * * @param props - Component props. - * @param props.paymentRequirement - Payment requirement evaluated for the protected resource. + * @param props.paymentRequired - Payment required response with accepts array. * @param props.onSuccessfulResponse - Callback fired once the 402 fetch succeeds. * @returns JSX element. */ -export function EvmPaywall({ paymentRequirement, onSuccessfulResponse }: EvmPaywallProps) { +export function EvmPaywall({ paymentRequired, onSuccessfulResponse }: EvmPaywallProps) { const { address, isConnected, chainId: connectedChainId } = useAccount(); const { switchChainAsync } = useSwitchChain(); const { data: wagmiWalletClient } = useWalletClient(); - const { sessionToken } = useOnrampSessionToken(address); + const { connectors, connect } = useConnect(); + const { disconnect } = useDisconnect(); const [status, setStatus] = useState(""); const [isCorrectChain, setIsCorrectChain] = useState(null); const [isPaying, setIsPaying] = useState(false); const [formattedUsdcBalance, setFormattedUsdcBalance] = useState(""); const [hideBalance, setHideBalance] = useState(true); + const [selectedConnectorId, setSelectedConnectorId] = useState(""); const x402 = window.x402; - const amount = - typeof x402.amount === "number" - ? x402.amount - : Number(paymentRequirement.maxAmountRequired ?? 0) / 1_000_000; - - const network = paymentRequirement.network as Network; - const paymentChain = network === "base-sepolia" ? baseSepolia : base; - const chainId = paymentChain.id; + const amount = x402.amount; + + const firstRequirement = paymentRequired.accepts[0]; + if (!firstRequirement) { + throw new Error("No payment requirements in paymentRequired.accepts"); + } + + const network = firstRequirement.network; const chainName = getNetworkDisplayName(network); const testnet = isTestnetNetwork(network); - const showOnramp = Boolean(!testnet && isConnected && x402.sessionTokenEndpoint); + + const chainId = parseInt(network.split(":")[1]); + + // Find the chain from viem's chain definitions + const paymentChain: Chain | undefined = Object.values(allChains).find(c => c.id === chainId); + + if (!paymentChain) { + throw new Error(`Unsupported chain ID: ${chainId}`); + } const publicClient = useMemo( () => @@ -113,16 +114,12 @@ export function EvmPaywall({ paymentRequirement, onSuccessfulResponse }: EvmPayw } }, [chainId, connectedChainId, isConnected, chainName]); - const onrampBuyUrl = useMemo(() => { - if (!sessionToken) { - return undefined; + // Auto-select if only one connector is available + useEffect(() => { + if (!selectedConnectorId && connectors.length === 1) { + setSelectedConnectorId(connectors[0].id); } - return getOnrampBuyUrl({ - presetFiatAmount: 2, - fiatCurrency: "USD", - sessionToken, - }); - }, [sessionToken]); + }, [connectors, selectedConnectorId]); const handlePayment = useCallback(async () => { if (!address || !x402) { @@ -148,51 +145,28 @@ export function EvmPaywall({ paymentRequirement, onSuccessfulResponse }: EvmPayw } setStatus("Creating payment signature..."); - const validPaymentRequirements = ensureValidAmount(paymentRequirement); - const initialPayment = await exact.evm.createPayment( - walletClient, - 1, - validPaymentRequirements, - ); - const paymentHeader: string = exact.evm.encodePayment(initialPayment); + // Create client and register EVM schemes (handles v1 and v2) + const signer = wagmiToClientSigner(walletClient); + const client = new x402Client(); + registerExactEvmScheme(client, { signer }); + + // Create payment payload - client automatically handles version + const paymentPayload = await client.createPaymentPayload(paymentRequired); + + // Encode as base64 JSON for v2 header + const paymentHeader = btoa(JSON.stringify(paymentPayload)); setStatus("Requesting content with payment..."); const response = await fetch(x402.currentUrl, { headers: { - "X-PAYMENT": paymentHeader, - "Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE", + "PAYMENT-SIGNATURE": paymentHeader, + "Access-Control-Expose-Headers": "PAYMENT-RESPONSE", }, }); if (response.ok) { await onSuccessfulResponse(response); - } else if (response.status === 402) { - const errorData = await response.json().catch(() => ({})); - if (errorData && typeof errorData.x402Version === "number") { - const retryPayment = await exact.evm.createPayment( - walletClient, - errorData.x402Version, - validPaymentRequirements, - ); - - retryPayment.x402Version = errorData.x402Version; - const retryHeader = exact.evm.encodePayment(retryPayment); - const retryResponse = await fetch(x402.currentUrl, { - headers: { - "X-PAYMENT": retryHeader, - "Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE", - }, - }); - if (retryResponse.ok) { - await onSuccessfulResponse(retryResponse); - return; - } else { - throw new Error(`Payment retry failed: ${retryResponse.statusText}`); - } - } else { - throw new Error(`Payment failed: ${response.statusText}`); - } } else { throw new Error(`Request failed: ${response.status} ${response.statusText}`); } @@ -204,7 +178,7 @@ export function EvmPaywall({ paymentRequirement, onSuccessfulResponse }: EvmPayw }, [ address, x402, - paymentRequirement, + paymentRequired, handleSwitchChain, wagmiWalletClient, publicClient, @@ -221,8 +195,8 @@ export function EvmPaywall({ paymentRequirement, onSuccessfulResponse }: EvmPayw

Payment Required

- {paymentRequirement.description && `${paymentRequirement.description}.`} To access this - content, please pay ${amount} {chainName} USDC. + {paymentRequired.resource?.description && `${paymentRequired.resource.description}.`} To + access this content, please pay ${amount} {chainName} USDC.

{testnet && (

@@ -235,15 +209,42 @@ export function EvmPaywall({ paymentRequirement, onSuccessfulResponse }: EvmPayw

- - - - - - - - - + {!isConnected ? ( +
+ + +
+ ) : ( +
+ +
+ )} {isConnected && (
@@ -273,16 +274,8 @@ export function EvmPaywall({ paymentRequirement, onSuccessfulResponse }: EvmPayw
- {isCorrectChain ? ( -
- {showOnramp && ( - - )} +
+ {isCorrectChain ? ( -
- ) : ( - - )} + ) : ( + + )} +
)} {status &&
{status}
} diff --git a/typescript/packages/http/paywall/src/evm/Spinner.tsx b/typescript/packages/http/paywall/src/evm/Spinner.tsx new file mode 100644 index 0000000..bd948ed --- /dev/null +++ b/typescript/packages/http/paywall/src/evm/Spinner.tsx @@ -0,0 +1,14 @@ +/** + * Simple Spinner component for loading states + * + * @param props - The component props + * @param props.className - Optional CSS classes to apply to the spinner + * @returns The Spinner component + */ +export function Spinner({ className = "" }: { className?: string }) { + return ( +
+
+
+ ); +} diff --git a/typescript/packages/http/paywall/src/evm/browserAdapter.ts b/typescript/packages/http/paywall/src/evm/browserAdapter.ts new file mode 100644 index 0000000..e5ef558 --- /dev/null +++ b/typescript/packages/http/paywall/src/evm/browserAdapter.ts @@ -0,0 +1,28 @@ +import type { ClientEvmSigner } from "@x402/evm"; +import type { Account, WalletClient } from "viem"; + +/** + * Converts a wagmi/viem WalletClient to a ClientEvmSigner for x402Client + * + * @param walletClient - The wagmi wallet client from useWalletClient() + * @returns ClientEvmSigner compatible with ExactEvmClient + */ +export function wagmiToClientSigner(walletClient: WalletClient): ClientEvmSigner { + if (!walletClient.account) { + throw new Error("Wallet client must have an account"); + } + + return { + address: walletClient.account.address, + signTypedData: async message => { + const signature = await walletClient.signTypedData({ + account: walletClient.account as Account, + domain: message.domain, + types: message.types, + primaryType: message.primaryType, + message: message.message, + }); + return signature; + }, + }; +} diff --git a/typescript/packages/http/paywall/src/evm/build.ts b/typescript/packages/http/paywall/src/evm/build.ts new file mode 100644 index 0000000..0d7d924 --- /dev/null +++ b/typescript/packages/http/paywall/src/evm/build.ts @@ -0,0 +1,130 @@ +import esbuild from "esbuild"; +import { htmlPlugin } from "@craftamap/esbuild-plugin-html"; +import fs from "fs"; +import path from "path"; +import { getBaseTemplate } from "../baseTemplate"; + +// EVM-specific build - only bundles EVM dependencies +const DIST_DIR = "src/evm/dist"; +const OUTPUT_HTML = path.join(DIST_DIR, "evm-paywall.html"); +const OUTPUT_TS = path.join("src/evm/gen", "template.ts"); + +// Cross-language template output paths (relative to package root where build runs) +const PYTHON_DIR = path.join("..", "..", "..", "..", "python", "x402", "http", "paywall"); +const GO_DIR = path.join("..", "..", "..", "..", "go", "http"); +const OUTPUT_PY = path.join(PYTHON_DIR, "evm_paywall_template.py"); +const OUTPUT_GO = path.join(GO_DIR, "evm_paywall_template.go"); + +const options: esbuild.BuildOptions = { + entryPoints: ["src/evm/entry.tsx", "src/styles.css"], + bundle: true, + metafile: true, + outdir: DIST_DIR, + treeShaking: true, + minify: true, + format: "iife", + sourcemap: false, + platform: "browser", + target: "es2020", + jsx: "transform", + define: { + "process.env.NODE_ENV": '"development"', + global: "globalThis", + Buffer: "globalThis.Buffer", + }, + mainFields: ["browser", "module", "main"], + conditions: ["browser"], + plugins: [ + htmlPlugin({ + files: [ + { + entryPoints: ["src/evm/entry.tsx", "src/styles.css"], + filename: "evm-paywall.html", + title: "Payment Required", + scriptLoading: "module", + inline: { + css: true, + js: true, + }, + htmlTemplate: getBaseTemplate(), + }, + ], + }), + ], + inject: ["./src/buffer-polyfill.ts"], + external: ["crypto"], +}; + +/** + * Builds the EVM paywall HTML template with bundled JS and CSS. + * Also generates Python and Go template files for cross-language support. + */ +async function build() { + try { + if (!fs.existsSync(DIST_DIR)) { + fs.mkdirSync(DIST_DIR, { recursive: true }); + } + + const genDir = path.dirname(OUTPUT_TS); + if (!fs.existsSync(genDir)) { + fs.mkdirSync(genDir, { recursive: true }); + } + + await esbuild.build(options); + console.log("[EVM] Build completed successfully!"); + + if (fs.existsSync(OUTPUT_HTML)) { + const html = fs.readFileSync(OUTPUT_HTML, "utf8"); + + const tsContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT +/** + * The pre-built EVM paywall template with inlined CSS and JS + */ +export const EVM_PAYWALL_TEMPLATE = ${JSON.stringify(html)}; +`; + + // Generate Python template file + const pyContent = `# THIS FILE IS AUTO-GENERATED - DO NOT EDIT +EVM_PAYWALL_TEMPLATE = ${JSON.stringify(html)} +`; + + // Generate Go template file + const goContent = `// THIS FILE IS AUTO-GENERATED - DO NOT EDIT +package http + +// EVMPaywallTemplate is the pre-built EVM paywall template with inlined CSS and JS +const EVMPaywallTemplate = ${JSON.stringify(html)} +`; + + fs.writeFileSync(OUTPUT_TS, tsContent); + console.log(`[EVM] Generated template.ts (${(html.length / 1024 / 1024).toFixed(2)} MB)`); + + // Write the Python template file + if (fs.existsSync(PYTHON_DIR)) { + fs.writeFileSync(OUTPUT_PY, pyContent); + console.log( + `[EVM] Generated Python evm_paywall_template.py (${(html.length / 1024 / 1024).toFixed(2)} MB)`, + ); + } else { + console.warn(`[EVM] Python directory not found: ${PYTHON_DIR}`); + } + + // Write the Go template file + if (fs.existsSync(GO_DIR)) { + fs.writeFileSync(OUTPUT_GO, goContent); + console.log( + `[EVM] Generated Go evm_paywall_template.go (${(html.length / 1024 / 1024).toFixed(2)} MB)`, + ); + } else { + console.warn(`[EVM] Go directory not found: ${GO_DIR}`); + } + } else { + throw new Error(`EVM bundled HTML not found at ${OUTPUT_HTML}`); + } + } catch (error) { + console.error("[EVM] Build failed:", error); + process.exit(1); + } +} + +build(); diff --git a/typescript/packages/http/paywall/src/evm/entry.tsx b/typescript/packages/http/paywall/src/evm/entry.tsx new file mode 100644 index 0000000..fc35224 --- /dev/null +++ b/typescript/packages/http/paywall/src/evm/entry.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { EvmPaywall } from "./EvmPaywall"; +import { Providers } from "../Providers"; +import type {} from "../window"; + +// EVM-specific paywall entry point +window.addEventListener("load", () => { + const rootElement = document.getElementById("root"); + if (!rootElement) { + console.error("Root element not found"); + return; + } + + const x402 = window.x402; + const paymentRequired = x402.paymentRequired; + + if (!paymentRequired?.accepts?.[0]) { + console.error("No payment requirements found"); + return; + } + + const root = createRoot(rootElement); + root.render( + + { + const contentType = response.headers.get("content-type"); + if (contentType && contentType.includes("text/html")) { + document.documentElement.innerHTML = await response.text(); + } else { + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + window.location.href = url; + } + }} + /> + , + ); +}); diff --git a/typescript/packages/http/paywall/src/evm/gen/template.ts b/typescript/packages/http/paywall/src/evm/gen/template.ts new file mode 100644 index 0000000..8d9a00e --- /dev/null +++ b/typescript/packages/http/paywall/src/evm/gen/template.ts @@ -0,0 +1,6 @@ +// THIS FILE IS AUTO-GENERATED - DO NOT EDIT +/** + * The pre-built EVM paywall template with inlined CSS and JS + */ +export const EVM_PAYWALL_TEMPLATE = + '\n \n \n Payment Required\n \n
\n \n \n '; diff --git a/typescript/packages/http/paywall/src/evm/index.ts b/typescript/packages/http/paywall/src/evm/index.ts new file mode 100644 index 0000000..88e24eb --- /dev/null +++ b/typescript/packages/http/paywall/src/evm/index.ts @@ -0,0 +1,51 @@ +import type { + PaywallNetworkHandler, + PaymentRequirements, + PaymentRequired, + PaywallConfig, +} from "../types"; +import { getEvmPaywallHtml } from "./paywall"; + +/** + * EVM paywall handler that supports EVM-based networks (CAIP-2 format only) + */ +export const evmPaywall: PaywallNetworkHandler = { + /** + * Check if this handler supports the given payment requirement + * + * @param requirement - Payment requirement to check + * @returns True if this handler can process this requirement + */ + supports(requirement: PaymentRequirements): boolean { + return requirement.network.startsWith("eip155:"); + }, + + /** + * Generate EVM-specific paywall HTML + * + * @param requirement - The selected payment requirement + * @param paymentRequired - Full payment required response + * @param config - Paywall configuration + * @returns HTML string for the paywall page + */ + generateHtml( + requirement: PaymentRequirements, + paymentRequired: PaymentRequired, + config: PaywallConfig, + ): string { + const amount = requirement.amount + ? parseFloat(requirement.amount) / 1000000 + : requirement.maxAmountRequired + ? parseFloat(requirement.maxAmountRequired) / 1000000 + : 0; + + return getEvmPaywallHtml({ + amount, + paymentRequired, + currentUrl: paymentRequired.resource?.url || config.currentUrl || "", + testnet: config.testnet ?? true, + appName: config.appName, + appLogo: config.appLogo, + }); + }, +}; diff --git a/typescript/packages/x402/src/paywall/index.ts b/typescript/packages/http/paywall/src/evm/paywall.ts similarity index 50% rename from typescript/packages/x402/src/paywall/index.ts rename to typescript/packages/http/paywall/src/evm/paywall.ts index 83150ba..c8b66ff 100644 --- a/typescript/packages/x402/src/paywall/index.ts +++ b/typescript/packages/http/paywall/src/evm/paywall.ts @@ -1,17 +1,5 @@ -import { PAYWALL_TEMPLATE } from "./gen/template"; -import { config } from "../types/shared/evm/config"; -import { PaymentRequirements } from "../types/verify"; - -interface PaywallOptions { - amount: number; - paymentRequirements: PaymentRequirements[]; - currentUrl: string; - testnet: boolean; - cdpClientKey?: string; - appName?: string; - appLogo?: string; - sessionTokenEndpoint?: string; -} +import type { PaymentRequired } from "../types"; +import { getEvmTemplate } from "./template-loader"; /** * Escapes a string for safe injection into JavaScript string literals @@ -30,52 +18,74 @@ function escapeString(str: string): string { } /** - * Generates an HTML paywall page that allows users to pay for content access + * Gets the EVM chain config + * + * @returns The EVM chain config + */ +function getChainConfig() { + return { + base: { + usdcAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + usdcName: "USDC", + }, + "base-sepolia": { + usdcAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + usdcName: "USDC", + }, + }; +} + +interface EvmPaywallOptions { + amount: number; + paymentRequired: PaymentRequired; + currentUrl: string; + testnet: boolean; + appName?: string; + appLogo?: string; +} + +/** + * Generates EVM-specific paywall HTML * * @param options - The options for generating the paywall * @param options.amount - The amount to be paid in USD - * @param options.paymentRequirements - The payment requirements for the content + * @param options.paymentRequired - The payment required response with accepts array * @param options.currentUrl - The URL of the content being accessed * @param options.testnet - Whether to use testnet or mainnet - * @param options.cdpClientKey - CDP client API key for OnchainKit * @param options.appName - The name of the application to display in the wallet connection modal * @param options.appLogo - The logo of the application to display in the wallet connection modal - * @param options.sessionTokenEndpoint - The API endpoint for generating session tokens for Onramp authentication - * @returns An HTML string containing the paywall page + * @returns HTML string for the paywall page */ -export function getPaywallHtml({ - amount, - testnet, - paymentRequirements, - currentUrl, - cdpClientKey, - appName, - appLogo, - sessionTokenEndpoint, -}: PaywallOptions): string { +export function getEvmPaywallHtml(options: EvmPaywallOptions): string { + const EVM_PAYWALL_TEMPLATE = getEvmTemplate(); + + if (!EVM_PAYWALL_TEMPLATE) { + return `

EVM Paywall (run pnpm build:paywall to generate full template)

`; + } + + const { amount, testnet, paymentRequired, currentUrl, appName, appLogo } = options; + const logOnTestnet = testnet - ? "console.log('Payment requirements initialized:', window.x402);" + ? "console.log('EVM Payment required initialized:', window.x402);" : ""; - // Create the configuration script to inject with proper escaping + const config = getChainConfig(); + const configScript = ` `; - // Inject the configuration script into the head - return PAYWALL_TEMPLATE.replace("", `${configScript}\n`); + return EVM_PAYWALL_TEMPLATE.replace("", `${configScript}\n`); } diff --git a/typescript/packages/http/paywall/src/evm/template-loader.ts b/typescript/packages/http/paywall/src/evm/template-loader.ts new file mode 100644 index 0000000..c2afca1 --- /dev/null +++ b/typescript/packages/http/paywall/src/evm/template-loader.ts @@ -0,0 +1,16 @@ +let cachedTemplate: string | null = null; + +export function getEvmTemplate(): string | null { + if (cachedTemplate !== null) { + return cachedTemplate; + } + + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const template = require("./gen/template"); + cachedTemplate = template.EVM_PAYWALL_TEMPLATE; + return cachedTemplate; + } catch { + return null; + } +} diff --git a/typescript/packages/http/paywall/src/evm/utils.ts b/typescript/packages/http/paywall/src/evm/utils.ts new file mode 100644 index 0000000..1a04f67 --- /dev/null +++ b/typescript/packages/http/paywall/src/evm/utils.ts @@ -0,0 +1,55 @@ +import type { Address, Client, Chain, Transport, Account } from "viem"; + +/** + * USDC contract addresses by chain ID + */ +const USDC_ADDRESSES: Record = { + 1: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // Ethereum Mainnet + 8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base Mainnet + 84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Base Sepolia +}; + +/** + * ERC20 balanceOf ABI + */ +const ERC20_BALANCE_ABI = [ + { + name: "balanceOf", + type: "function", + stateMutability: "view", + inputs: [{ name: "account", type: "address" }], + outputs: [{ name: "balance", type: "uint256" }], + }, +] as const; + +/** + * Gets the USDC balance for a specific address on the current chain. + * + * @param client - Viem client instance connected to the blockchain + * @param address - Address to check the USDC balance for + * @returns USDC balance as bigint (0 if USDC not supported on chain or error) + */ +export async function getUSDCBalance< + TTransport extends Transport, + TChain extends Chain, + TAccount extends Account | undefined = undefined, +>(client: Client, address: Address): Promise { + const chainId = client.chain?.id; + if (!chainId) return 0n; + + const usdcAddress = USDC_ADDRESSES[chainId]; + if (!usdcAddress) return 0n; + + try { + const balance = await client.readContract({ + address: usdcAddress, + abi: ERC20_BALANCE_ABI, + functionName: "balanceOf", + args: [address], + }); + return balance as bigint; + } catch (error) { + console.error("Failed to fetch USDC balance:", error); + return 0n; + } +} diff --git a/typescript/packages/http/paywall/src/index.test.ts b/typescript/packages/http/paywall/src/index.test.ts new file mode 100644 index 0000000..00ba681 --- /dev/null +++ b/typescript/packages/http/paywall/src/index.test.ts @@ -0,0 +1,12 @@ +import { describe, it, expect } from "vitest"; + +describe("@x402/paywall", () => { + it("should be defined", () => { + expect(true).toBe(true); + }); + + // TODO: Add actual tests for paywall functionality + it.todo("should handle payment required responses"); + it.todo("should render paywall UI"); + it.todo("should process payments"); +}); diff --git a/typescript/packages/http/paywall/src/index.ts b/typescript/packages/http/paywall/src/index.ts new file mode 100644 index 0000000..29443fe --- /dev/null +++ b/typescript/packages/http/paywall/src/index.ts @@ -0,0 +1,17 @@ +/** + * @module @x402/paywall - x402 Payment Protocol Paywall Extension + * This module provides paywall functionality for the x402 payment protocol. + */ + +export { createPaywall, PaywallBuilder } from "./builder"; +export type { + PaywallProvider, + PaywallConfig, + PaymentRequired, + PaywallNetworkHandler, + PaymentRequirements, +} from "./types"; + +// Re-export network handlers for convenience +export { evmPaywall } from "./evm"; +export { svmPaywall } from "./svm"; diff --git a/typescript/packages/x402/src/paywall/index.tsx b/typescript/packages/http/paywall/src/index.tsx similarity index 77% rename from typescript/packages/x402/src/paywall/index.tsx rename to typescript/packages/http/paywall/src/index.tsx index 9fe844b..3a91b72 100644 --- a/typescript/packages/x402/src/paywall/index.tsx +++ b/typescript/packages/http/paywall/src/index.tsx @@ -1,6 +1,7 @@ +import React from "react"; import { createRoot } from "react-dom/client"; -import { Providers } from "./src/Providers"; -import { PaywallApp } from "./src/PaywallApp"; +import { Providers } from "./Providers"; +import { PaywallApp } from "./PaywallApp"; // Initialize the app when the window loads window.addEventListener("load", () => { diff --git a/typescript/packages/http/paywall/src/network-handlers.test.ts b/typescript/packages/http/paywall/src/network-handlers.test.ts new file mode 100644 index 0000000..cb4e506 --- /dev/null +++ b/typescript/packages/http/paywall/src/network-handlers.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from "vitest"; +import { evmPaywall } from "./evm"; +import { svmPaywall } from "./svm"; +import type { PaymentRequired, PaymentRequirements } from "./types"; + +const evmRequirement: PaymentRequirements = { + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + amount: "100000", + payTo: "0x209693Bc6afc0C5328bA36FaF04C514EF312287C", + maxTimeoutSeconds: 60, +}; + +const svmRequirement: PaymentRequirements = { + scheme: "exact", + network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + asset: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + amount: "100000", + payTo: "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHEBg4", + maxTimeoutSeconds: 60, +}; + +const mockPaymentRequired: PaymentRequired = { + x402Version: 2, + resource: { + url: "https://example.com/api/data", + description: "Test", + mimeType: "application/json", + }, + accepts: [evmRequirement], +}; + +describe("Network Handlers", () => { + describe("evmPaywall", () => { + it("supports CAIP-2 EVM networks", () => { + expect(evmPaywall.supports({ ...evmRequirement, network: "eip155:8453" })).toBe(true); + expect(evmPaywall.supports({ ...evmRequirement, network: "eip155:84532" })).toBe(true); + expect(evmPaywall.supports({ ...evmRequirement, network: "eip155:1" })).toBe(true); + expect(evmPaywall.supports({ ...evmRequirement, network: "eip155:137" })).toBe(true); + }); + + it("rejects non-EVM networks", () => { + expect(evmPaywall.supports({ ...evmRequirement, network: "solana:5eykt" })).toBe(false); + expect(evmPaywall.supports({ ...evmRequirement, network: "base" })).toBe(false); + expect(evmPaywall.supports({ ...evmRequirement, network: "unknown" })).toBe(false); + }); + + it("generates HTML for EVM networks", () => { + const html = evmPaywall.generateHtml(evmRequirement, mockPaymentRequired, { + appName: "Test App", + testnet: true, + }); + + expect(html).toContain(""); + expect(html).toMatch(/Test App|EVM Paywall/); + }); + }); + + describe("svmPaywall", () => { + it("supports CAIP-2 Solana networks", () => { + expect( + svmPaywall.supports({ + ...svmRequirement, + network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + }), + ).toBe(true); + expect( + svmPaywall.supports({ + ...svmRequirement, + network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + }), + ).toBe(true); + }); + + it("rejects non-Solana networks", () => { + expect(svmPaywall.supports({ ...svmRequirement, network: "eip155:8453" })).toBe(false); + expect(svmPaywall.supports({ ...svmRequirement, network: "base" })).toBe(false); + expect(svmPaywall.supports({ ...svmRequirement, network: "unknown" })).toBe(false); + }); + + it("generates HTML for Solana networks", () => { + const html = svmPaywall.generateHtml(svmRequirement, mockPaymentRequired, { + appName: "Solana Test", + testnet: true, + }); + + expect(html).toContain(""); + expect(html).toMatch(/Solana Test|SVM Paywall/); + }); + }); +}); diff --git a/typescript/packages/http/paywall/src/paywallUtils.test.ts b/typescript/packages/http/paywall/src/paywallUtils.test.ts new file mode 100644 index 0000000..063de44 --- /dev/null +++ b/typescript/packages/http/paywall/src/paywallUtils.test.ts @@ -0,0 +1,158 @@ +import { describe, expect, it } from "vitest"; +import type { PaymentRequirements } from "@x402/core/types"; +import { + choosePaymentRequirement, + getNetworkDisplayName, + isEvmNetwork, + isSvmNetwork, + normalizePaymentRequirements, + isTestnetNetwork, +} from "./paywallUtils"; + +const baseRequirement: PaymentRequirements = { + scheme: "exact", + network: "eip155:8453", + maxAmountRequired: "1000", + resource: "https://example.com/protected", + description: "Base resource", + mimeType: "application/json", + payTo: "0x0000000000000000000000000000000000000001", + maxTimeoutSeconds: 60, + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + extra: { + feePayer: "0x0000000000000000000000000000000000000003", + }, +}; + +const baseSepoliaRequirement: PaymentRequirements = { + ...baseRequirement, + network: "eip155:84532", + description: "Base Sepolia resource", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", +}; + +const solanaRequirement: PaymentRequirements = { + scheme: "exact", + network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + maxAmountRequired: "1000", + resource: "https://example.com/solana", + description: "Solana resource", + mimeType: "application/json", + payTo: "2Zt8RZ8kW1nWcJ6YyqHq9kTjY8QpM2R2t1xXUQ1e1VQa", + maxTimeoutSeconds: 60, + asset: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + extra: { + feePayer: "3d9yxXikBVYjgvPbJF4dPSt31Z87Nb5fV9jXYzQ3QAtc", + }, +}; + +describe("paywallUtils", () => { + describe("normalizePaymentRequirements", () => { + it("normalizes single payment requirement into an array", () => { + const normalized = normalizePaymentRequirements(baseRequirement); + expect(normalized).toHaveLength(1); + expect(normalized[0]).toBe(baseRequirement); + }); + + it("returns array as-is when already an array", () => { + const requirements = [baseRequirement, solanaRequirement]; + const normalized = normalizePaymentRequirements(requirements); + expect(normalized).toBe(requirements); + expect(normalized).toHaveLength(2); + }); + }); + + describe("choosePaymentRequirement", () => { + it("selects base payment on mainnet preference", () => { + const selected = choosePaymentRequirement([solanaRequirement, baseRequirement], false); + expect(selected.network).toBe("eip155:8453"); + }); + + it("selects base sepolia payment on testnet preference", () => { + const selected = choosePaymentRequirement([solanaRequirement, baseSepoliaRequirement], true); + expect(selected.network).toBe("eip155:84532"); + }); + + it("falls back to solana when no evm networks exist", () => { + const selected = choosePaymentRequirement([solanaRequirement], false); + expect(selected.network).toBe("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"); + }); + + it("returns first requirement when no preferred networks match", () => { + const customRequirement = { ...baseRequirement, network: "eip155:137" }; + const selected = choosePaymentRequirement([customRequirement], true); + expect(selected).toBe(customRequirement); + }); + }); + + describe("getNetworkDisplayName", () => { + it("returns display names for CAIP-2 EVM networks using viem", () => { + expect(getNetworkDisplayName("eip155:8453")).toBe("Base"); + expect(getNetworkDisplayName("eip155:84532")).toBe("Base Sepolia"); + expect(getNetworkDisplayName("eip155:1")).toBe("Ethereum"); + expect(getNetworkDisplayName("eip155:137")).toBe("Polygon"); + }); + + it("returns display names for CAIP-2 Solana networks", () => { + expect(getNetworkDisplayName("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp")).toBe( + "Solana Mainnet", + ); + expect(getNetworkDisplayName("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1")).toBe( + "Solana Devnet", + ); + }); + + it("returns fallback for unknown chain IDs", () => { + expect(getNetworkDisplayName("eip155:999999")).toBe("Chain 999999"); + }); + + it("returns network as-is for unknown formats", () => { + expect(getNetworkDisplayName("unknown")).toBe("unknown"); + }); + }); + + describe("isEvmNetwork", () => { + it("identifies CAIP-2 EVM networks", () => { + expect(isEvmNetwork("eip155:8453")).toBe(true); + expect(isEvmNetwork("eip155:84532")).toBe(true); + expect(isEvmNetwork("eip155:1")).toBe(true); + expect(isEvmNetwork("eip155:137")).toBe(true); + }); + + it("rejects non-EVM networks", () => { + expect(isEvmNetwork("solana:5eykt")).toBe(false); + expect(isEvmNetwork("base")).toBe(false); + expect(isEvmNetwork("unknown")).toBe(false); + }); + }); + + describe("isSvmNetwork", () => { + it("identifies CAIP-2 Solana networks", () => { + expect(isSvmNetwork("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp")).toBe(true); + expect(isSvmNetwork("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1")).toBe(true); + }); + + it("rejects non-Solana networks", () => { + expect(isSvmNetwork("eip155:8453")).toBe(false); + expect(isSvmNetwork("base")).toBe(false); + expect(isSvmNetwork("unknown")).toBe(false); + }); + }); + + describe("isTestnetNetwork", () => { + it("identifies EVM testnets using viem metadata", () => { + expect(isTestnetNetwork("eip155:84532")).toBe(true); + expect(isTestnetNetwork("eip155:80002")).toBe(true); + }); + + it("identifies Solana testnets", () => { + expect(isTestnetNetwork("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1")).toBe(true); + }); + + it("rejects mainnets", () => { + expect(isTestnetNetwork("eip155:8453")).toBe(false); + expect(isTestnetNetwork("eip155:1")).toBe(false); + expect(isTestnetNetwork("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp")).toBe(false); + }); + }); +}); diff --git a/typescript/packages/http/paywall/src/paywallUtils.ts b/typescript/packages/http/paywall/src/paywallUtils.ts new file mode 100644 index 0000000..f480cbd --- /dev/null +++ b/typescript/packages/http/paywall/src/paywallUtils.ts @@ -0,0 +1,143 @@ +import type { PaymentRequirements } from "@x402/core/types"; +import * as allChains from "viem/chains"; + +// Chain configuration constants + +// EVM Chain IDs (CAIP-2 format: eip155:chainId) +// Only chains we explicitly reference in code +export const EVM_CHAIN_IDS = { + BASE_MAINNET: "8453", + BASE_SEPOLIA: "84532", +} as const; + +// Solana Network References (CAIP-2 format: solana:genesisHash) +export const SOLANA_NETWORK_REFS = { + MAINNET: "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + DEVNET: "EtWTRABZaYq6iMfeYKouRu166VU2xqa1", +} as const; + +/** + * Normalizes the payment requirements into an array. + * + * @param paymentRequirements - A single requirement or a list of requirements. + * @returns An array of payment requirements. + */ +export function normalizePaymentRequirements( + paymentRequirements: PaymentRequirements | PaymentRequirements[], +): PaymentRequirements[] { + if (Array.isArray(paymentRequirements)) { + return paymentRequirements; + } + return [paymentRequirements]; +} + +/** + * Returns the preferred networks to attempt first when selecting a payment requirement. + * + * @param testnet - Whether the paywall is operating in testnet mode. + * @returns Ordered list of preferred networks (CAIP-2 format). + */ +export function getPreferredNetworks(testnet: boolean): string[] { + if (testnet) { + return [`eip155:${EVM_CHAIN_IDS.BASE_SEPOLIA}`, `solana:${SOLANA_NETWORK_REFS.DEVNET}`]; + } + return [`eip155:${EVM_CHAIN_IDS.BASE_MAINNET}`, `solana:${SOLANA_NETWORK_REFS.MAINNET}`]; +} + +/** + * Selects the most appropriate payment requirement for the user. + * + * @param paymentRequirements - All available payment requirements. + * @param testnet - Whether the paywall is operating in testnet mode. + * @returns The selected payment requirement. + */ +export function choosePaymentRequirement( + paymentRequirements: PaymentRequirements | PaymentRequirements[], + testnet: boolean, +): PaymentRequirements { + const normalized = normalizePaymentRequirements(paymentRequirements); + const preferredNetworks = getPreferredNetworks(testnet); + + // Try to find a requirement matching preferred networks + for (const preferredNetwork of preferredNetworks) { + const match = normalized.find(req => req.network === preferredNetwork); + if (match) { + return match; + } + } + + // Fall back to first requirement + return normalized[0]; +} + +/** + * Determines if the provided network is an EVM network. + * + * @param network - The network to check (CAIP-2 format: eip155:chainId). + * @returns True if the network is EVM based. + */ +export function isEvmNetwork(network: string): boolean { + return network.startsWith("eip155:"); +} + +/** + * Determines if the provided network is an SVM network. + * + * @param network - The network to check (CAIP-2 format: solana:reference). + * @returns True if the network is SVM based. + */ +export function isSvmNetwork(network: string): boolean { + return network.startsWith("solana:"); +} + +/** + * Provides a human-readable display name for a network. + * Uses viem/chains for EVM chain metadata (based on ethereum-lists/chains). + * See: https://github.com/ethereum-lists/chains + * + * @param network - The network identifier (CAIP-2 format). + * @returns A display name suitable for UI use. + */ +export function getNetworkDisplayName(network: string): string { + if (network.startsWith("eip155:")) { + const chainId = parseInt(network.split(":")[1]); + + // Find matching chain in viem's chain definitions + const chain = Object.values(allChains).find(c => c.id === chainId); + + if (chain) { + return chain.name; + } + + return `Chain ${chainId}`; + } + + if (network.startsWith("solana:")) { + const ref = network.split(":")[1]; + return ref === SOLANA_NETWORK_REFS.DEVNET ? "Solana Devnet" : "Solana Mainnet"; + } + + return network; +} + +/** + * Indicates whether the provided network is a testnet. + * Uses viem's testnet property for EVM chains. + * + * @param network - The network to evaluate (CAIP-2 format). + * @returns True if the network is a recognized testnet. + */ +export function isTestnetNetwork(network: string): boolean { + if (network.startsWith("eip155:")) { + const chainId = parseInt(network.split(":")[1]); + const chain = Object.values(allChains).find(c => c.id === chainId); + return chain?.testnet ?? false; + } + + if (network.startsWith("solana:")) { + const ref = network.split(":")[1]; + return ref === SOLANA_NETWORK_REFS.DEVNET; + } + + return false; +} diff --git a/typescript/packages/x402/src/paywall/styles.css b/typescript/packages/http/paywall/src/styles.css similarity index 89% rename from typescript/packages/x402/src/paywall/styles.css rename to typescript/packages/http/paywall/src/styles.css index b60fd9d..ed9bb00 100644 --- a/typescript/packages/x402/src/paywall/styles.css +++ b/typescript/packages/http/paywall/src/styles.css @@ -1,4 +1,3 @@ -@import "@coinbase/onchainkit/styles.css"; /* Reset */ *, *::before, @@ -57,27 +56,6 @@ h6 { --button-error-hover-color: #dc2626; } -.ock-font-family { - font-family: "Inter", system-ui, -apple-system, sans-serif; -} - -.ock-bg-secondary, .ock-bg-default { - background-color: var(--details-background-color); - transition: background-color 150ms; -} - -.ock-bg-secondary:hover { - background-color: var(--details-background-color-hover); -} - -.opacity-80 { - opacity: 0.8; -} - -[data-testid="ockWalletDropdown"] { - z-index: 10; -} - body { min-height: 100vh; background-color: var(--background-color); @@ -233,6 +211,7 @@ body { flex-basis: 50%; flex-direction: row; gap: 0.5rem; + margin-top: 1rem; } .balance-button { @@ -245,3 +224,24 @@ body { justify-content: flex-end; align-items: center; } + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.spinner { + display: inline-flex; + align-items: center; + justify-content: center; +} + +.spinner > div { + animation: spin 1s linear infinite; + border: 2px solid #e5e7eb; + border-top-color: #3b82f6; + border-radius: 50%; + width: 1rem; + height: 1rem; +} \ No newline at end of file diff --git a/typescript/packages/x402/src/paywall/src/SolanaPaywall.tsx b/typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx similarity index 77% rename from typescript/packages/x402/src/paywall/src/SolanaPaywall.tsx rename to typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx index c4d1cd3..309e65c 100644 --- a/typescript/packages/x402/src/paywall/src/SolanaPaywall.tsx +++ b/typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx @@ -2,12 +2,12 @@ import { useCallback, useEffect, useRef, useState } from "react"; import type { WalletAccount } from "@wallet-standard/base"; import type { WalletWithSolanaFeatures } from "@solana/wallet-standard-features"; -import type { PaymentRequirements } from "../../types/verify"; -import { exact } from "../../schemes"; +import { registerExactSvmScheme } from "@x402/svm/exact/client"; +import { x402Client } from "@x402/core/client"; +import type { PaymentRequired } from "@x402/core/types"; import { Spinner } from "./Spinner"; -import { ensureValidAmount } from "./utils"; -import { getNetworkDisplayName } from "./paywallUtils"; +import { getNetworkDisplayName, SOLANA_NETWORK_REFS } from "../paywallUtils"; import { getStandardConnectFeature, getStandardDisconnectFeature } from "./solana/features"; import { useSolanaBalance } from "./solana/useSolanaBalance"; import { useSolanaSigner } from "./solana/useSolanaSigner"; @@ -17,7 +17,7 @@ import { useSilentWalletConnection } from "./solana/useSilentWalletConnection"; import type { WalletOption } from "./solana/types"; type SolanaPaywallProps = { - paymentRequirement: PaymentRequirements; + paymentRequired: PaymentRequired; onSuccessfulResponse: (response: Response) => Promise; }; @@ -25,11 +25,11 @@ type SolanaPaywallProps = { * Paywall experience for Solana networks. * * @param props - Component props. - * @param props.paymentRequirement - Payment requirement enforced for Solana requests. + * @param props.paymentRequired - Payment required response with accepts array. * @param props.onSuccessfulResponse - Callback invoked on successful 402 response. * @returns JSX element. */ -export function SolanaPaywall({ paymentRequirement, onSuccessfulResponse }: SolanaPaywallProps) { +export function SolanaPaywall({ paymentRequired, onSuccessfulResponse }: SolanaPaywallProps) { const [status, setStatus] = useState(""); const [isPaying, setIsPaying] = useState(false); const walletOptions = useSolanaWalletOptions(); @@ -39,24 +39,27 @@ export function SolanaPaywall({ paymentRequirement, onSuccessfulResponse }: Sola const [hideBalance, setHideBalance] = useState(true); const attemptedSilentConnectWalletsRef = useRef>(new Set()); + const x402 = window.x402; + const amount = x402.amount; + + const firstRequirement = paymentRequired.accepts[0]; + if (!firstRequirement) { + throw new Error("No payment requirements in paymentRequired.accepts"); + } + + const network = firstRequirement.network; + const chainName = getNetworkDisplayName(network); + + const isMainnet = network.includes(SOLANA_NETWORK_REFS.MAINNET); + const targetChain = isMainnet ? ("solana:mainnet" as const) : ("solana:devnet" as const); + const { usdcBalance, formattedBalance, isFetchingBalance, refreshBalance, resetBalance } = useSolanaBalance({ activeAccount, - paymentRequirement, + paymentRequired, onStatus: setStatus, }); - const x402 = window.x402; - const amount = - typeof x402.amount === "number" - ? x402.amount - : Number(paymentRequirement.maxAmountRequired ?? 0) / 1_000_000; - - const network = paymentRequirement.network; - const chainName = getNetworkDisplayName(network); - const targetChain = - network === "solana" ? ("solana:mainnet" as const) : ("solana:devnet" as const); - const walletSigner = useSolanaSigner({ activeWallet, activeAccount, @@ -175,57 +178,28 @@ export function SolanaPaywall({ paymentRequirement, onSuccessfulResponse }: Sola } } - setStatus("Creating payment transaction..."); - const validPaymentRequirements = ensureValidAmount(paymentRequirement); + setStatus("Creating payment signature..."); - const createHeader = async (version: number) => - exact.svm.createPaymentHeader(walletSigner, version, validPaymentRequirements); + const client = new x402Client(); + registerExactSvmScheme(client, { signer: walletSigner }); - const paymentHeader = await createHeader(1); + const paymentPayload = await client.createPaymentPayload(paymentRequired); + + const paymentHeader = btoa(JSON.stringify(paymentPayload)); setStatus("Requesting content with payment..."); const response = await fetch(x402.currentUrl, { headers: { - "X-PAYMENT": paymentHeader, - "Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE", + "PAYMENT-SIGNATURE": paymentHeader, + "Access-Control-Expose-Headers": "PAYMENT-RESPONSE", }, }); if (response.ok) { await onSuccessfulResponse(response); - return; - } - - if (response.status === 402) { - const errorData = await response.json().catch(() => ({})); - if (errorData && typeof errorData.x402Version === "number") { - const retryPayment = await exact.svm.createPaymentHeader( - walletSigner, - errorData.x402Version, - validPaymentRequirements, - ); - - const retryResponse = await fetch(x402.currentUrl, { - headers: { - "X-PAYMENT": retryPayment, - "Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE", - }, - }); - - if (retryResponse.ok) { - await onSuccessfulResponse(retryResponse); - return; - } - - throw new Error( - `Payment retry failed: ${retryResponse.status} ${retryResponse.statusText}`, - ); - } - - throw new Error(`Payment failed: ${response.statusText}`); + } else { + throw new Error(`Request failed: ${response.status} ${response.statusText}`); } - - throw new Error(`Payment failed: ${response.status} ${response.statusText}`); } catch (error) { setStatus(error instanceof Error ? error.message : "Payment failed."); } finally { @@ -238,7 +212,7 @@ export function SolanaPaywall({ paymentRequirement, onSuccessfulResponse }: Sola usdcBalance, refreshBalance, chainName, - paymentRequirement, + paymentRequired, onSuccessfulResponse, ]); @@ -247,10 +221,10 @@ export function SolanaPaywall({ paymentRequirement, onSuccessfulResponse }: Sola

Payment Required

- {paymentRequirement.description && `${paymentRequirement.description}.`} To access this - content, please pay ${amount} {chainName} USDC. + {paymentRequired.resource?.description && `${paymentRequired.resource.description}.`} To + access this content, please pay ${amount} {chainName} USDC.

- {network === "solana-devnet" && ( + {String(network).includes("devnet") && (

Need Solana Devnet USDC?{" "} @@ -306,7 +280,9 @@ export function SolanaPaywall({ paymentRequirement, onSuccessfulResponse }: Sola