UNPKG

@payai/x402

Version:

PayAI-distributed wrapper for @x402/core v2

1,446 lines (1,437 loc) 60.1 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/server/index.ts var server_exports = {}; __export(server_exports, { FacilitatorResponseError: () => FacilitatorResponseError, HTTPFacilitatorClient: () => HTTPFacilitatorClient, RouteConfigurationError: () => RouteConfigurationError, getFacilitatorResponseError: () => getFacilitatorResponseError, x402HTTPResourceServer: () => x402HTTPResourceServer, x402ResourceServer: () => x402ResourceServer }); module.exports = __toCommonJS(server_exports); // src/types/facilitator.ts var VerifyError = class extends Error { /** * 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, response) { 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; } }; var SettleError = class extends Error { /** * 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, response) { 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; } }; var FacilitatorResponseError = class extends Error { /** * Creates a FacilitatorResponseError for malformed facilitator responses. * * @param message - The boundary error message */ constructor(message) { super(message); this.name = "FacilitatorResponseError"; } }; function getFacilitatorResponseError(error) { let current = error; while (current instanceof Error) { if (current instanceof FacilitatorResponseError) { return current; } current = current.cause; } return null; } // src/utils/index.ts var findSchemesByNetwork = (map, network) => { let implementationsByScheme = map.get(network); if (!implementationsByScheme) { for (const [registeredNetworkPattern, implementations] of map.entries()) { const pattern = registeredNetworkPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*"); const regex = new RegExp(`^${pattern}$`); if (regex.test(network)) { implementationsByScheme = implementations; break; } } } return implementationsByScheme; }; var findByNetworkAndScheme = (map, scheme, network) => { return findSchemesByNetwork(map, network)?.get(scheme); }; var Base64EncodedRegex = /^[A-Za-z0-9+/]*={0,2}$/; function safeBase64Encode(data) { 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"); } function safeBase64Decode(data) { 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"); } function deepEqual(obj1, obj2) { const normalize = (obj) => { if (obj === null || obj === void 0) return JSON.stringify(obj); if (typeof obj !== "object") return JSON.stringify(obj); if (Array.isArray(obj)) { return JSON.stringify( obj.map( (item) => typeof item === "object" && item !== null ? JSON.parse(normalize(item)) : item ) ); } const sorted = {}; Object.keys(obj).sort().forEach((key) => { const value = obj[key]; sorted[key] = typeof value === "object" && value !== null ? JSON.parse(normalize(value)) : value; }); return JSON.stringify(sorted); }; try { return normalize(obj1) === normalize(obj2); } catch { return JSON.stringify(obj1) === JSON.stringify(obj2); } } // src/schemas/index.ts var import_zod = require("zod"); var import_zod2 = require("zod"); var NonEmptyString = import_zod.z.string().min(1); var Any = import_zod.z.record(import_zod.z.unknown()); var OptionalAny = import_zod.z.record(import_zod.z.unknown()).optional().nullable(); var NetworkSchemaV1 = NonEmptyString; var NetworkSchemaV2 = import_zod.z.string().min(3).refine((val) => val.includes(":"), { message: "Network must be in CAIP-2 format (e.g., 'eip155:84532')" }); var NetworkSchema = import_zod.z.union([NetworkSchemaV1, NetworkSchemaV2]); var ResourceInfoSchema = import_zod.z.object({ url: NonEmptyString, description: import_zod.z.string().optional(), mimeType: import_zod.z.string().optional() }); var PaymentRequirementsV1Schema = import_zod.z.object({ scheme: NonEmptyString, network: NetworkSchemaV1, maxAmountRequired: NonEmptyString, resource: NonEmptyString, // URL string in V1 description: import_zod.z.string(), mimeType: import_zod.z.string().optional(), outputSchema: Any.optional().nullable(), payTo: NonEmptyString, maxTimeoutSeconds: import_zod.z.number().positive(), asset: NonEmptyString, extra: OptionalAny }); var PaymentRequiredV1Schema = import_zod.z.object({ x402Version: import_zod.z.literal(1), error: import_zod.z.string().optional(), accepts: import_zod.z.array(PaymentRequirementsV1Schema).min(1) }); var PaymentPayloadV1Schema = import_zod.z.object({ x402Version: import_zod.z.literal(1), scheme: NonEmptyString, network: NetworkSchemaV1, payload: Any }); var PaymentRequirementsV2Schema = import_zod.z.object({ scheme: NonEmptyString, network: NetworkSchemaV2, amount: NonEmptyString, asset: NonEmptyString, payTo: NonEmptyString, maxTimeoutSeconds: import_zod.z.number().positive(), extra: OptionalAny }); var PaymentRequiredV2Schema = import_zod.z.object({ x402Version: import_zod.z.literal(2), error: import_zod.z.string().optional(), resource: ResourceInfoSchema, accepts: import_zod.z.array(PaymentRequirementsV2Schema).min(1), extensions: OptionalAny }); var PaymentPayloadV2Schema = import_zod.z.object({ x402Version: import_zod.z.literal(2), resource: ResourceInfoSchema.optional(), accepted: PaymentRequirementsV2Schema, payload: Any, extensions: OptionalAny }); var PaymentRequirementsSchema = import_zod.z.union([ PaymentRequirementsV1Schema, PaymentRequirementsV2Schema ]); var PaymentRequiredSchema = import_zod.z.discriminatedUnion("x402Version", [ PaymentRequiredV1Schema, PaymentRequiredV2Schema ]); var PaymentPayloadSchema = import_zod.z.discriminatedUnion("x402Version", [ PaymentPayloadV1Schema, PaymentPayloadV2Schema ]); // src/http/httpFacilitatorClient.ts var DEFAULT_FACILITATOR_URL = "https://facilitator.payai.network"; var GET_SUPPORTED_RETRIES = 3; var GET_SUPPORTED_RETRY_DELAY_MS = 1e3; var verifyResponseSchema = import_zod2.z.object({ isValid: import_zod2.z.boolean(), invalidReason: import_zod2.z.string().optional(), invalidMessage: import_zod2.z.string().optional(), payer: import_zod2.z.string().optional(), extensions: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional() }); var settleResponseSchema = import_zod2.z.object({ success: import_zod2.z.boolean(), errorReason: import_zod2.z.string().optional(), errorMessage: import_zod2.z.string().optional(), payer: import_zod2.z.string().optional(), transaction: import_zod2.z.string(), network: import_zod2.z.custom((value) => typeof value === "string"), extensions: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional() }); var supportedKindSchema = import_zod2.z.object({ x402Version: import_zod2.z.number(), scheme: import_zod2.z.string(), network: import_zod2.z.custom( (value) => typeof value === "string" ), extra: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional() }); var supportedResponseSchema = import_zod2.z.object({ kinds: import_zod2.z.array(supportedKindSchema), extensions: import_zod2.z.array(import_zod2.z.string()).default([]), signers: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.array(import_zod2.z.string())).default({}) }); function responseExcerpt(text, limit = 200) { const compact = text.trim().replace(/\s+/g, " "); if (!compact) { return "<empty response>"; } if (compact.length <= limit) { return compact; } return `${compact.slice(0, limit - 3)}...`; } async function parseSuccessResponse(response, schema, operation) { const text = await response.text(); let data; try { data = JSON.parse(text); } catch { throw new FacilitatorResponseError( `Facilitator ${operation} returned invalid JSON: ${responseExcerpt(text)}` ); } const parsed = schema.safeParse(data); if (!parsed.success) { throw new FacilitatorResponseError( `Facilitator ${operation} returned invalid data: ${responseExcerpt(text)}` ); } return parsed.data; } var HTTPFacilitatorClient = class { /** * Creates a new HTTPFacilitatorClient instance. * * @param config - Configuration options for the facilitator client */ constructor(config) { 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, paymentRequirements) { let headers = { "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) }) }); if (!response.ok) { const text = await response.text(); let data; try { data = JSON.parse(text); } catch { throw new Error(`Facilitator verify failed (${response.status}): ${responseExcerpt(text)}`); } if (typeof data === "object" && data !== null && "isValid" in data) { throw new VerifyError(response.status, data); } throw new Error( `Facilitator verify failed (${response.status}): ${responseExcerpt(JSON.stringify(data))}` ); } return parseSuccessResponse(response, verifyResponseSchema, "verify"); } /** * Settle a payment with the facilitator * * @param paymentPayload - The payment to settle * @param paymentRequirements - The requirements for settlement * @returns Settlement response */ async settle(paymentPayload, paymentRequirements) { let headers = { "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) }) }); if (!response.ok) { const text = await response.text(); let data; try { data = JSON.parse(text); } catch { throw new Error(`Facilitator settle failed (${response.status}): ${responseExcerpt(text)}`); } if (typeof data === "object" && data !== null && "success" in data) { throw new SettleError(response.status, data); } throw new Error( `Facilitator settle failed (${response.status}): ${responseExcerpt(JSON.stringify(data))}` ); } return parseSuccessResponse(response, settleResponseSchema, "settle"); } /** * 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() { let headers = { "Content-Type": "application/json" }; if (this._createAuthHeaders) { const authHeaders = await this.createAuthHeaders("supported"); headers = { ...headers, ...authHeaders.headers }; } let lastError = null; for (let attempt = 0; attempt < GET_SUPPORTED_RETRIES; attempt++) { const response = await fetch(`${this.url}/supported`, { method: "GET", headers }); if (response.ok) { return parseSuccessResponse(response, supportedResponseSchema, "supported"); } const errorText = await response.text().catch(() => response.statusText); lastError = new Error( `Facilitator getSupported failed (${response.status}): ${responseExcerpt(errorText)}` ); 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) { if (this._createAuthHeaders) { const authHeaders = await this._createAuthHeaders(); 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 */ toJsonSafe(obj) { return JSON.parse( JSON.stringify(obj, (_, value) => typeof value === "bigint" ? value.toString() : value) ); } }; // src/index.ts var x402Version = 2; // src/server/x402ResourceServer.ts var x402ResourceServer = class { /** * Creates a new x402ResourceServer instance. * * @param facilitatorClients - Optional facilitator client(s) for payment processing */ constructor(facilitatorClients) { this.registeredServerSchemes = /* @__PURE__ */ new Map(); this.supportedResponsesMap = /* @__PURE__ */ new Map(); this.facilitatorClientsMap = /* @__PURE__ */ new Map(); this.registeredExtensions = /* @__PURE__ */ new Map(); this.beforeVerifyHooks = []; this.afterVerifyHooks = []; this.onVerifyFailureHooks = []; this.beforeSettleHooks = []; this.afterSettleHooks = []; this.onSettleFailureHooks = []; if (!facilitatorClients) { this.facilitatorClients = [new HTTPFacilitatorClient()]; } else if (Array.isArray(facilitatorClients)) { this.facilitatorClients = facilitatorClients.length > 0 ? facilitatorClients : [new HTTPFacilitatorClient()]; } else { 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, server) { if (!this.registeredServerSchemes.has(network)) { this.registeredServerSchemes.set(network, /* @__PURE__ */ 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, scheme) { 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) { 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) { return this.registeredExtensions.has(key); } /** * Get all registered extensions. * * @returns Array of registered extensions */ getExtensions() { 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, transportContext) { const enriched = {}; 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) { 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) { 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) { 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) { 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) { 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) { 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() { this.supportedResponsesMap.clear(); this.facilitatorClientsMap.clear(); let lastError; for (const facilitatorClient of this.facilitatorClients) { try { const supported = await facilitatorClient.getSupported(); for (const kind of supported.kinds) { const x402Version2 = kind.x402Version; if (!this.supportedResponsesMap.has(x402Version2)) { this.supportedResponsesMap.set(x402Version2, /* @__PURE__ */ new Map()); } const responseVersionMap = this.supportedResponsesMap.get(x402Version2); if (!this.facilitatorClientsMap.has(x402Version2)) { this.facilitatorClientsMap.set(x402Version2, /* @__PURE__ */ new Map()); } const clientVersionMap = this.facilitatorClientsMap.get(x402Version2); if (!responseVersionMap.has(kind.network)) { responseVersionMap.set(kind.network, /* @__PURE__ */ new Map()); } const responseNetworkMap = responseVersionMap.get(kind.network); if (!clientVersionMap.has(kind.network)) { clientVersionMap.set(kind.network, /* @__PURE__ */ new Map()); } const clientNetworkMap = clientVersionMap.get(kind.network); if (!responseNetworkMap.has(kind.scheme)) { responseNetworkMap.set(kind.scheme, supported); clientNetworkMap.set(kind.scheme, facilitatorClient); } } } catch (error) { lastError = error; console.warn(`Failed to fetch supported kinds from facilitator: ${error}`); } } if (this.supportedResponsesMap.size === 0) { throw lastError ? new Error( "Failed to initialize: no supported payment kinds loaded from any facilitator.", { cause: lastError } ) : new Error( "Failed to initialize: no supported payment kinds loaded from any facilitator." ); } } /** * 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(x402Version2, network, scheme) { const versionMap = this.supportedResponsesMap.get(x402Version2); if (!versionMap) return void 0; const supportedResponse = findByNetworkAndScheme(versionMap, scheme, network); if (!supportedResponse) return void 0; return supportedResponse.kinds.find( (kind) => kind.x402Version === x402Version2 && 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(x402Version2, network, scheme) { const versionMap = this.supportedResponsesMap.get(x402Version2); 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) { const requirements = []; const scheme = resourceConfig.scheme; const SchemeNetworkServer = findByNetworkAndScheme( this.registeredServerSchemes, scheme, resourceConfig.network ); if (!SchemeNetworkServer) { console.warn( `No server implementation registered for scheme: ${scheme}, network: ${resourceConfig.network}` ); return requirements; } 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.` ); } const facilitatorExtensions = this.getFacilitatorExtensions( x402Version, resourceConfig.network, SchemeNetworkServer.scheme ); const parsedPrice = await SchemeNetworkServer.parsePrice( resourceConfig.price, resourceConfig.network ); const baseRequirements = { 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 } }; 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, context) { const allRequirements = []; for (const option of paymentOptions) { 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 = { scheme: option.scheme, payTo: resolvedPayTo, price: resolvedPrice, network: option.network, maxTimeoutSeconds: option.maxTimeoutSeconds, extra: option.extra }; 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) * @param transportContext - Optional transport-specific context (e.g., HTTP request, MCP tool context) * @returns Payment required response object */ async createPaymentRequiredResponse(requirements, resourceInfo, error, extensions, transportContext) { let response = { x402Version: 2, error, resource: resourceInfo, accepts: requirements }; if (extensions && Object.keys(extensions).length > 0) { response.extensions = extensions; } if (extensions) { for (const [key, declaration] of Object.entries(extensions)) { const extension = this.registeredExtensions.get(key); if (extension?.enrichPaymentRequiredResponse) { try { const context = { requirements, resourceInfo, error, paymentRequiredResponse: response, transportContext }; const extensionData = await extension.enrichPaymentRequiredResponse( declaration, context ); if (extensionData !== void 0) { if (!response.extensions) { response.extensions = {}; } response.extensions[key] = extensionData; } } catch (error2) { console.error( `Error in enrichPaymentRequiredResponse hook for extension ${key}:`, error2 ); } } } } 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, requirements) { const context = { paymentPayload, requirements }; 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 { const facilitatorClient = this.getFacilitatorClient( paymentPayload.x402Version, requirements.network, requirements.scheme ); let verifyResult; if (!facilitatorClient) { let lastError; for (const client of this.facilitatorClients) { try { verifyResult = await client.verify(paymentPayload, requirements); break; } catch (error) { lastError = error; } } if (!verifyResult) { throw lastError || new Error( `No facilitator supports ${requirements.scheme} on ${requirements.network} for v${paymentPayload.x402Version}` ); } } else { verifyResult = await facilitatorClient.verify(paymentPayload, requirements); } const resultContext = { ...context, result: verifyResult }; for (const hook of this.afterVerifyHooks) { await hook(resultContext); } return verifyResult; } catch (error) { const failureContext = { ...context, error }; 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) * @param transportContext - Optional transport-specific context (e.g., HTTP request/response, MCP tool context) * @returns Settlement response */ async settlePayment(paymentPayload, requirements, declaredExtensions, transportContext) { const context = { paymentPayload, requirements }; 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) { if (error instanceof SettleError) { throw error; } throw new SettleError(400, { success: false, errorReason: "before_settle_hook_error", errorMessage: error instanceof Error ? error.message : "", transaction: "", network: requirements.network }); } } try { const facilitatorClient = this.getFacilitatorClient( paymentPayload.x402Version, requirements.network, requirements.scheme ); let settleResult; if (!facilitatorClient) { let lastError; for (const client of this.facilitatorClients) { try { settleResult = await client.settle(paymentPayload, requirements); break; } catch (error) { lastError = error; } } if (!settleResult) { throw lastError || new Error( `No facilitator supports ${requirements.scheme} on ${requirements.network} for v${paymentPayload.x402Version}` ); } } else { settleResult = await facilitatorClient.settle(paymentPayload, requirements); } const resultContext = { ...context, result: settleResult, transportContext }; for (const hook of this.afterSettleHooks) { await hook(resultContext); } 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 !== void 0) { 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 = { ...context, error }; 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, paymentPayload) { switch (paymentPayload.x402Version) { case 2: return availableRequirements.find( (paymentRequirements) => deepEqual(paymentRequirements, paymentPayload.accepted) ); case 1: return availableRequirements.find( (req) => req.scheme === paymentPayload.accepted.scheme && req.network === paymentPayload.accepted.network ); default: throw new Error( `Unsupported x402 version: ${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, resourceConfig, resourceInfo, extensions) { const requirements = await this.buildPaymentRequirements(resourceConfig); if (!paymentPayload) { return { success: false, requiresPayment: await this.createPaymentRequiredResponse( requirements, resourceInfo, "Payment required", extensions ) }; } const matchingRequirements = this.findMatchingRequirements(requirements, paymentPayload); if (!matchingRequirements) { return { success: false, requiresPayment: await this.createPaymentRequiredResponse( requirements, resourceInfo, "No matching payment requirements found", extensions ) }; } const verificationResult = await this.verifyPayment(paymentPayload, matchingRequirements); if (!verificationResult.isValid) { return { success: false, error: verificationResult.invalidReason, verificationResult }; } 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 */ getFacilitatorClient(x402Version2, network, scheme) { const versionMap = this.facilitatorClientsMap.get(x402Version2); if (!versionMap) return void 0; return findByNetworkAndScheme(versionMap, scheme, network); } }; // src/http/index.ts function decodePaymentSignatureHeader(paymentSignatureHeader) { if (!Base64EncodedRegex.test(paymentSignatureHeader)) { throw new Error("Invalid payment signature header"); } return JSON.parse(safeBase64Decode(paymentSignatureHeader)); } function encodePaymentRequiredHeader(paymentRequired) { return safeBase64Encode(JSON.stringify(paymentRequired)); } function encodePaymentResponseHeader(paymentResponse) { return safeBase64Encode(JSON.stringify(paymentResponse)); } // src/http/x402HTTPResourceServer.ts var RouteConfigurationError = class extends Error { /** * Creates a new RouteConfigurationError with the given validation errors. * * @param errors - The validation errors that caused this exception. */ constructor(errors) { const message = `x402 Route Configuration Errors: ${errors.map((e) => ` - ${e.message}`).join("\n")}`; super(message); this.name = "RouteConfigurationError"; this.errors = errors; } }; var x402HTTPResourceServer = class { /** * Creates a new x402HTTPResourceServer instance. * * @param ResourceServer - The core x402ResourceServer instance to use * @param routes - Route configuration for payment-protected endpoints */ constructor(ResourceServer, routes) { this.compiledRoutes = []; this.protectedRequestHooks = []; this.ResourceServer = ResourceServer; this.routesConfig = routes; const normalizedRoutes = typeof routes === "object" && !("accepts" in routes) ? routes : { "*": routes }; 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() { return this.ResourceServer; } /** * Get the routes configuration. * * @returns The routes configuration */ get routes() { 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() { await this.ResourceServer.initialize(); 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) { 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) { 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, paywallConfig) { const { adapter, path, method } = context; const routeConfig = this.getRouteConfig(path, method); if (!routeConfig) { return { type: "no-payment-required" }; } 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 } } }; } } const paymentOptions = this.normalizePaymentOptions(routeConfig); const paymentPayload = this.extractPayment(adapter); const resourceInfo = { url: routeConfig.resource || context.adapter.getUrl(), description: routeConfig.description || "", mimeType: routeConfig.mimeType || "" }; let requirements = await this.ResourceServer.buildPaymentRequirementsFromOptions( paymentOptions, context ); let extensions = routeConfig.extensions; if (extensions) { extensions = this.ResourceServer.enrichExtensions(extensions, context); } const transportContext = { request: context }; const paymentRequired = await this.ResourceServer.createPaymentRequiredResponse( requirements, resourceInfo, !paymentPayload ? "Payment required" : void 0, extensions, transportContext ); if (!paymentPayload) { const unpaidBody = routeConfig.unpaidResponseBody ? await routeConfig.unpaidResponseBody(context) : void 0; return { type: "payment-error", response: this.createHTTPResponse( paymentRequired, this.isWebBrowser(adapter), paywallConfig, routeConfig.customPaywallHtml, unpaidBody ) }; } 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, transportContext ); 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, transportContext ); return { type: "payment-error", response: this.createHTTPResponse(errorResponse, false, paywallConfig) }; } return { type: "payment-verified", paymentPayload, paymentRequirements: matchingRequirements, declaredExtensions: routeConfig.extensions }; } catch (error) { if (error instanceof FacilitatorResponseError) { throw error; } const errorResponse = await this.ResourceServer.createPaymentRequiredResponse( requirements, resourceInfo, error instanceof Error ? error.message : "Payment verification failed", routeConfig.extensions, transportContext ); 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) * @param transportContext - Optional HTTP transport context * @returns ProcessSettleResultResponse - SettleResponse with headers if success or errorReason if failure */ async processSettlement(paymentPayload, requirements, declaredExtensions, transportContext) { try { const settleResponse = await this.ResourceServer.settlePayment( paymentPayload, requirements, declaredExtensions, transportContext ); if (!settleResponse.success) { const failure = { ...settleResponse, success: false, errorReason: settleResponse.errorReason || "Settlement failed", errorMessage: settleResponse.errorMessage || settleResponse.errorReason || "Settlement failed", headers: this.createSettlementHeaders(settleResponse) }; const response = await this.buildSettlementFailureResponse(failure, transportContext); return { ...failure, response }; } return { ...settleResponse, success: true, headers: this.createSettlementHeaders(settleResponse), requirements }; } catch (error) { if (error instanceof FacilitatorResponseError) { throw error; } if (error instanceof SettleError) { const errorReason2 = error.errorReason || error.message; const settleResponse2 = { success: false, errorReason: errorReason2, errorMessage: error.errorMessage || errorReason2, payer: error.payer, network: error.network, transaction: error.transaction }; const failure2 = { ...settleResponse2, success: false, errorReason: errorReason2, headers: this.createSettlementHeaders(settleResponse2) }; const response2 = await this.buildSettlementFailureResponse(failure2, transportContext); return { ...failure2, response: response2 }; } const errorReason = error instanceof Error ? error.message : "Settlement failed"; const settleResponse = { success: false, errorReason, errorMessage: errorReason, network: requirements.network, transaction: "" }; const failure = { ...settleResponse, success: false, errorReason, headers: this.createSettlementHeaders(settleResponse) }; const response = await this.buildSettlementFailureResponse(failure, transportContext); return { ...failure, response }; } } /** * 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) { const routeConfig = this.getRouteConfig(context.path, context.method); return routeConfig !== void 0; } /** * Build HTTPResponseInstructions for settlement failure. * Uses settlementFailedResponseBody hook if configured, otherwise defaults to empty body. * * @param failure - Settlement failure result with headers * @param transportContext - Optional HTTP transport context for the request * @returns HTTP response instructions for the 402 settlement failure response */ async buildSettlementFailureResponse(failure, transportContext) { const settlementHeaders = failure.headers; const routeConfig = transportContext ? this.getRouteConfig(transportContext.request.path, transportContext.request.method) : void 0; const custo