@payai/x402
Version:
PayAI-distributed wrapper for @x402/core v2
1,456 lines (1,448 loc) • 50 kB
JavaScript
"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