@payai/x402
Version:
PayAI-distributed wrapper for @x402/core v2
660 lines (659 loc) • 22.1 kB
JavaScript
import {
HTTPFacilitatorClient,
RouteConfigurationError,
x402HTTPResourceServer
} from "../chunk-CWIHZ5RE.mjs";
import {
x402Version
} from "../chunk-VE37GDG2.mjs";
import {
SettleError,
VerifyError
} from "../chunk-L2RJI3BI.mjs";
import {
deepEqual,
findByNetworkAndScheme
} from "../chunk-TDLQZ6MP.mjs";
import "../chunk-BJTO5JO5.mjs";
// 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);
}
};
export {
HTTPFacilitatorClient,
RouteConfigurationError,
x402HTTPResourceServer,
x402ResourceServer
};
//# sourceMappingURL=index.mjs.map