UNPKG

@payai/x402

Version:

PayAI-distributed wrapper for @x402/core v2

1,456 lines (1,448 loc) 50 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, { HTTPFacilitatorClient: () => HTTPFacilitatorClient, RouteConfigurationError: () => RouteConfigurationError, 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; } }; // 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/http/httpFacilitatorClient.ts var DEFAULT_FACILITATOR_URL = "https://x402.org/facilitator"; 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) }) }); const data = await response.json(); if (typeof data === "object" && data !== null && "isValid" in data) { const verifyResponse = data; 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, 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) }) }); const data = await response.json(); if (typeof data === "object" && data !== null && "success" in data) { const settleResponse = data; 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 * * @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 }; } const response = await fetch(`${this.url}/supported`, { method: "GET", headers }); if (!response.ok) { const errorText = await response.text().catch(() => response.statusText); throw new Error(`Facilitator getSupported failed (${response.status}): ${errorText}`); } return await response.json(); } /** * 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(); 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) { 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(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 } }; 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 }; 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, resourceInfo, error, extensions) { 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 }; 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) * @returns Settlement response */ async settlePayment(paymentPayload, requirements, declaredExtensions) { 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) { 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 }; 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 paymentRequired = await this.ResourceServer.createPaymentRequiredResponse( requirements, resourceInfo, !paymentPayload ? "Payment required" : void 0, extensions ); 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 ); 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) }; } 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, requirements, declaredExtensions) { 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, 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) { const routeConfig = this.getRouteConfig(context.path, context.method); return routeConfig !== void 0; } /** * 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 */ normalizePaymentOptions(routeConfig) { 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) */ validateRouteConfiguration() { const errors = []; const normalizedRoutes = typeof this.routesConfig === "object" && !("accepts" in this.routesConfig) ? Object.entries(this.routesConfig) : [["*", this.routesConfig]]; for (const [pattern, config] of normalizedRoutes) { const paymentOptions = this.normalizePaymentOptions(config); for (const option of paymentOptions) { 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}"` }); continue; } 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 */ getRouteConfig(path, method) { 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 */ extractPayment(adapter) { 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 */ isWebBrowser(adapter) { 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 */ createHTTPResponse(paymentRequired, isWebBrowser, paywallConfig, customHtml, unpaidResponse) { 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); 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 */ createHTTPPaymentRequiredResponse(paymentRequired) { return { headers: { "PAYMENT-REQUIRED": encodePaymentRequiredHeader(paymentRequired) } }; } /** * Create settlement response headers * * @param settleResponse - Settlement response * @returns Headers to add to response */ createSettlementHeaders(settleResponse) { 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 */ parseRoutePattern(pattern) { const [verb, path] = pattern.includes(" ") ? pattern.split(/\s+/) : ["*", pattern]; const regex = new RegExp( `^${path.replace(/[$()+.?^{|}]/g, "\\$&").replace(/\*/g, ".*?").replace(/\[([^\]]+)\]/g, "[^/]+").replace(/\//g, "\\/")}$`, "i" ); return { verb: verb.toUpperCase(), regex }; } /** * Normalize path for matching * * @param path - Raw path from request * @returns Normalized path */ normalizePath(path) { const pathWithoutQuery = path.split(/[?#]/)[0]; let decodedOrRawPath; 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 */ generatePaywallHTML(paymentRequired, paywallConfig, customHtml) { if (customHtml) { return customHtml; } if (this.paywallProvider) { return this.paywallProvider.generateHtml(paymentRequired, paywallConfig); } try { const paywall = require("@x402/paywall"); const displayAmount2 = this.getDisplayAmount(paymentRequired); const resource2 = paymentRequired.resource; return paywall.getPaywallHtml({ amount: displayAmount2, paymentRequired, currentUrl: resource2?.url || paywallConfig?.currentUrl || "", testnet: paywallConfig?.testnet ?? true, appName: paywallConfig?.appName, appLogo: paywallConfig?.appLogo, sessionTokenEndpoint: paywallConfig?.sessionTokenEndpoint }); } catch { } const resource = paymentRequired.resource; const displayAmount = this.getDisplayAmount(paymentRequired); return ` <!DOCTYPE html> <html> <head> <title>Payment Required</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <div style="max-width: 600px; margin: 50px auto; padding: 20px; font-family: system-ui, -apple-system, sans-serif;"> ${paywallConfig?.appLogo ? `<img src="${paywallConfig.appLogo}" alt="${paywallConfig.appName || "App"}" style="max-width: 200px; margin-bottom: 20px;">` : ""} <h1>Payment Required</h1> ${resource ? `<p><strong>Resource:</strong> ${resource.description || resource.url}</p>` : ""} <p><strong>Amount:</strong> $${displayAmount.toFixed(2)} USDC</p> <div id="payment-widget" data-requirements='${JSON.stringify(paymentRequired)}' data-app-name="${paywallConfig?.appName || ""}" data-testnet="${paywallConfig?.testnet || false}"> <!-- Install @x402/paywall for full wallet integration --> <p style="margin-top: 2rem; padding: 1rem; background: #fef3c7; border-radius: 0.5rem;"> <strong>Note:</strong> Install <code>@x402/paywall</code> for full wallet connection and payment UI. </p> </div> </div> </body> </html> `; } /** * Extract display amount from payment requirements. * * @param paymentRequired - The payment required object * @returns The display amount in decimal format */ getDisplayAmount(paymentRequired) { const accepts = paymentRequired.accepts; if (accepts && accepts.length > 0) { const firstReq = accepts[0]; if ("amount" in firstReq) { return parseFloat(firstReq.amount) / 1e6; } } return 0; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { HTTPFacilitatorClient, RouteConfigurationError, x402HTTPResourceServer, x402ResourceServer }); //# sourceMappingURL