@payai/x402
Version:
PayAI-distributed wrapper for @x402/core v2
1,446 lines (1,437 loc) • 60.1 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, {
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