@payai/x402
Version:
PayAI-distributed wrapper for @x402/core v2
440 lines (434 loc) • 15.9 kB
JavaScript
;
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/client/index.ts
var client_exports = {};
__export(client_exports, {
x402Client: () => x402Client,
x402HTTPClient: () => x402HTTPClient
});
module.exports = __toCommonJS(client_exports);
// src/index.ts
var x402Version = 2;
// 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");
}
// src/client/x402Client.ts
var x402Client = class _x402Client {
/**
* Creates a new x402Client instance.
*
* @param paymentRequirementsSelector - Function to select payment requirements from available options
*/
constructor(paymentRequirementsSelector) {
this.registeredClientSchemes = /* @__PURE__ */ new Map();
this.policies = [];
this.beforePaymentCreationHooks = [];
this.afterPaymentCreationHooks = [];
this.onPaymentCreationFailureHooks = [];
this.paymentRequirementsSelector = paymentRequirementsSelector || ((x402Version2, accepts) => accepts[0]);
}
/**
* Creates a new x402Client instance from a configuration object.
*
* @param config - The client configuration including schemes, policies, and payment requirements selector
* @returns A configured x402Client instance
*/
static fromConfig(config) {
const client = new _x402Client(config.paymentRequirementsSelector);
config.schemes.forEach((scheme) => {
if (scheme.x402Version === 1) {
client.registerV1(scheme.network, scheme.client);
} else {
client.register(scheme.network, scheme.client);
}
});
config.policies?.forEach((policy) => {
client.registerPolicy(policy);
});
return client;
}
/**
* Registers a scheme client for the current x402 version.
*
* @param network - The network to register the client for
* @param client - The scheme network client to register
* @returns The x402Client instance for chaining
*/
register(network, client) {
return this._registerScheme(x402Version, network, client);
}
/**
* Registers a scheme client for x402 version 1.
*
* @param network - The v1 network identifier (e.g., 'base-sepolia', 'solana-devnet')
* @param client - The scheme network client to register
* @returns The x402Client instance for chaining
*/
registerV1(network, client) {
return this._registerScheme(1, network, client);
}
/**
* Registers a policy to filter or transform payment requirements.
*
* Policies are applied in order after filtering by registered schemes
* and before the selector chooses the final payment requirement.
*
* @param policy - Function to filter/transform payment requirements
* @returns The x402Client instance for chaining
*
* @example
* ```typescript
* // Prefer cheaper options
* client.registerPolicy((version, reqs) =>
* reqs.filter(r => BigInt(r.value) < BigInt('1000000'))
* );
*
* // Prefer specific networks
* client.registerPolicy((version, reqs) =>
* reqs.filter(r => r.network.startsWith('eip155:'))
* );
* ```
*/
registerPolicy(policy) {
this.policies.push(policy);
return this;
}
/**
* Register a hook to execute before payment payload creation.
* Can abort creation by returning { abort: true, reason: string }
*
* @param hook - The hook function to register
* @returns The x402Client instance for chaining
*/
onBeforePaymentCreation(hook) {
this.beforePaymentCreationHooks.push(hook);
return this;
}
/**
* Register a hook to execute after successful payment payload creation.
*
* @param hook - The hook function to register
* @returns The x402Client instance for chaining
*/
onAfterPaymentCreation(hook) {
this.afterPaymentCreationHooks.push(hook);
return this;
}
/**
* Register a hook to execute when payment payload creation fails.
* Can recover from failure by returning { recovered: true, payload: PaymentPayload }
*
* @param hook - The hook function to register
* @returns The x402Client instance for chaining
*/
onPaymentCreationFailure(hook) {
this.onPaymentCreationFailureHooks.push(hook);
return this;
}
/**
* Creates a payment payload based on a PaymentRequired response.
*
* Automatically extracts x402Version, resource, and extensions from the PaymentRequired
* response and constructs a complete PaymentPayload with the accepted requirements.
*
* @param paymentRequired - The PaymentRequired response from the server
* @returns Promise resolving to the complete payment payload
*/
async createPaymentPayload(paymentRequired) {
const clientSchemesByNetwork = this.registeredClientSchemes.get(paymentRequired.x402Version);
if (!clientSchemesByNetwork) {
throw new Error(`No client registered for x402 version: ${paymentRequired.x402Version}`);
}
const requirements = this.selectPaymentRequirements(paymentRequired.x402Version, paymentRequired.accepts);
const context = {
paymentRequired,
selectedRequirements: requirements
};
for (const hook of this.beforePaymentCreationHooks) {
const result = await hook(context);
if (result && "abort" in result && result.abort) {
throw new Error(`Payment creation aborted: ${result.reason}`);
}
}
try {
const schemeNetworkClient = findByNetworkAndScheme(clientSchemesByNetwork, requirements.scheme, requirements.network);
if (!schemeNetworkClient) {
throw new Error(`No client registered for scheme: ${requirements.scheme} and network: ${requirements.network}`);
}
const partialPayload = await schemeNetworkClient.createPaymentPayload(paymentRequired.x402Version, requirements);
let paymentPayload;
if (partialPayload.x402Version == 1) {
paymentPayload = partialPayload;
} else {
paymentPayload = {
...partialPayload,
extensions: paymentRequired.extensions,
resource: paymentRequired.resource,
accepted: requirements
};
}
const createdContext = {
...context,
paymentPayload
};
for (const hook of this.afterPaymentCreationHooks) {
await hook(createdContext);
}
return paymentPayload;
} catch (error) {
const failureContext = {
...context,
error
};
for (const hook of this.onPaymentCreationFailureHooks) {
const result = await hook(failureContext);
if (result && "recovered" in result && result.recovered) {
return result.payload;
}
}
throw error;
}
}
/**
* Selects appropriate payment requirements based on registered clients and policies.
*
* Selection process:
* 1. Filter by registered schemes (network + scheme support)
* 2. Apply all registered policies in order
* 3. Use selector to choose final requirement
*
* @param x402Version - The x402 protocol version
* @param paymentRequirements - Array of available payment requirements
* @returns The selected payment requirements
*/
selectPaymentRequirements(x402Version2, paymentRequirements) {
const clientSchemesByNetwork = this.registeredClientSchemes.get(x402Version2);
if (!clientSchemesByNetwork) {
throw new Error(`No client registered for x402 version: ${x402Version2}`);
}
const supportedPaymentRequirements = paymentRequirements.filter((requirement) => {
let clientSchemes = findSchemesByNetwork(clientSchemesByNetwork, requirement.network);
if (!clientSchemes) {
return false;
}
return clientSchemes.has(requirement.scheme);
});
if (supportedPaymentRequirements.length === 0) {
throw new Error(`No network/scheme registered for x402 version: ${x402Version2} which comply with the payment requirements. ${JSON.stringify({
x402Version: x402Version2,
paymentRequirements,
x402Versions: Array.from(this.registeredClientSchemes.keys()),
networks: Array.from(clientSchemesByNetwork.keys()),
schemes: Array.from(clientSchemesByNetwork.values()).map((schemes) => Array.from(schemes.keys())).flat()
})}`);
}
let filteredRequirements = supportedPaymentRequirements;
for (const policy of this.policies) {
filteredRequirements = policy(x402Version2, filteredRequirements);
if (filteredRequirements.length === 0) {
throw new Error(`All payment requirements were filtered out by policies for x402 version: ${x402Version2}`);
}
}
return this.paymentRequirementsSelector(x402Version2, filteredRequirements);
}
/**
* Internal method to register a scheme client.
*
* @param x402Version - The x402 protocol version
* @param network - The network to register the client for
* @param client - The scheme network client to register
* @returns The x402Client instance for chaining
*/
_registerScheme(x402Version2, network, client) {
if (!this.registeredClientSchemes.has(x402Version2)) {
this.registeredClientSchemes.set(x402Version2, /* @__PURE__ */ new Map());
}
const clientSchemesByNetwork = this.registeredClientSchemes.get(x402Version2);
if (!clientSchemesByNetwork.has(network)) {
clientSchemesByNetwork.set(network, /* @__PURE__ */ new Map());
}
const clientByScheme = clientSchemesByNetwork.get(network);
if (!clientByScheme.has(client.scheme)) {
clientByScheme.set(client.scheme, client);
}
return this;
}
};
// src/http/index.ts
function encodePaymentSignatureHeader(paymentPayload) {
return safeBase64Encode(JSON.stringify(paymentPayload));
}
function decodePaymentRequiredHeader(paymentRequiredHeader) {
if (!Base64EncodedRegex.test(paymentRequiredHeader)) {
throw new Error("Invalid payment required header");
}
return JSON.parse(safeBase64Decode(paymentRequiredHeader));
}
function decodePaymentResponseHeader(paymentResponseHeader) {
if (!Base64EncodedRegex.test(paymentResponseHeader)) {
throw new Error("Invalid payment response header");
}
return JSON.parse(safeBase64Decode(paymentResponseHeader));
}
// src/http/x402HTTPClient.ts
var x402HTTPClient = class {
/**
* Creates a new x402HTTPClient instance.
*
* @param client - The underlying x402Client for payment logic
*/
constructor(client) {
this.client = client;
this.paymentRequiredHooks = [];
}
/**
* Register a hook to handle 402 responses before payment.
* Hooks run in order; first to return headers wins.
*
* @param hook - The hook function to register
* @returns This instance for chaining
*/
onPaymentRequired(hook) {
this.paymentRequiredHooks.push(hook);
return this;
}
/**
* Run hooks and return headers if any hook provides them.
*
* @param paymentRequired - The payment required response from the server
* @returns Headers to use for retry, or null to proceed to payment
*/
async handlePaymentRequired(paymentRequired) {
for (const hook of this.paymentRequiredHooks) {
const result = await hook({ paymentRequired });
if (result?.headers) {
return result.headers;
}
}
return null;
}
/**
* Encodes a payment payload into appropriate HTTP headers based on version.
*
* @param paymentPayload - The payment payload to encode
* @returns HTTP headers containing the encoded payment signature
*/
encodePaymentSignatureHeader(paymentPayload) {
switch (paymentPayload.x402Version) {
case 2:
return {
"PAYMENT-SIGNATURE": encodePaymentSignatureHeader(paymentPayload)
};
case 1:
return {
"X-PAYMENT": encodePaymentSignatureHeader(paymentPayload)
};
default:
throw new Error(
`Unsupported x402 version: ${paymentPayload.x402Version}`
);
}
}
/**
* Extracts payment required information from HTTP response.
*
* @param getHeader - Function to retrieve header value by name (case-insensitive)
* @param body - Optional response body for v1 compatibility
* @returns The payment required object
*/
getPaymentRequiredResponse(getHeader, body) {
const paymentRequired = getHeader("PAYMENT-REQUIRED");
if (paymentRequired) {
return decodePaymentRequiredHeader(paymentRequired);
}
if (body && body instanceof Object && "x402Version" in body && body.x402Version === 1) {
return body;
}
throw new Error("Invalid payment required response");
}
/**
* Extracts payment settlement response from HTTP headers.
*
* @param getHeader - Function to retrieve header value by name (case-insensitive)
* @returns The settlement response object
*/
getPaymentSettleResponse(getHeader) {
const paymentResponse = getHeader("PAYMENT-RESPONSE");
if (paymentResponse) {
return decodePaymentResponseHeader(paymentResponse);
}
const xPaymentResponse = getHeader("X-PAYMENT-RESPONSE");
if (xPaymentResponse) {
return decodePaymentResponseHeader(xPaymentResponse);
}
throw new Error("Payment response header not found");
}
/**
* Creates a payment payload for the given payment requirements.
* Delegates to the underlying x402Client.
*
* @param paymentRequired - The payment required response from the server
* @returns Promise resolving to the payment payload
*/
async createPaymentPayload(paymentRequired) {
return this.client.createPaymentPayload(paymentRequired);
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
x402Client,
x402HTTPClient
});
//# sourceMappingURL=index.js.map