UNPKG

@payai/x402

Version:

PayAI-distributed wrapper for @x402/core v2

618 lines (610 loc) 23 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/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.registeredExtensions = /* @__PURE__ */ new Map(); 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; } /** * Registers a client extension that can enrich payment payloads. * * Extensions are invoked after the scheme creates the base payload and the * payload is wrapped with extensions/resource/accepted data. If the extension's * key is present in `paymentRequired.extensions`, the extension's * `enrichPaymentPayload` hook is called to modify the payload. * * @param extension - The client extension to register * @returns The x402Client instance for chaining */ registerExtension(extension) { this.registeredExtensions.set(extension.key, extension); 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, { extensions: paymentRequired.extensions } ); let paymentPayload; if (partialPayload.x402Version == 1) { paymentPayload = partialPayload; } else { const mergedExtensions = this.mergeExtensions( paymentRequired.extensions, partialPayload.extensions ); paymentPayload = { x402Version: partialPayload.x402Version, payload: partialPayload.payload, extensions: mergedExtensions, resource: paymentRequired.resource, accepted: requirements }; } paymentPayload = await this.enrichPaymentPayloadWithExtensions(paymentPayload, paymentRequired); 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; } } /** * Merges server-declared extensions with scheme-provided extensions. * Scheme extensions overlay on top of server extensions at each key, * preserving server-provided schema while overlaying scheme-provided info. * * @param serverExtensions - Extensions declared by the server in the 402 response * @param schemeExtensions - Extensions provided by the scheme client (e.g. EIP-2612) * @returns The merged extensions object, or undefined if both inputs are undefined */ mergeExtensions(serverExtensions, schemeExtensions) { if (!schemeExtensions) return serverExtensions; if (!serverExtensions) return schemeExtensions; const merged = { ...serverExtensions }; for (const [key, schemeValue] of Object.entries(schemeExtensions)) { const serverValue = merged[key]; if (serverValue && typeof serverValue === "object" && schemeValue && typeof schemeValue === "object") { merged[key] = { ...serverValue, ...schemeValue }; } else { merged[key] = schemeValue; } } return merged; } /** * Enriches a payment payload by calling registered extension hooks. * For each extension key present in the PaymentRequired response, * invokes the corresponding extension's enrichPaymentPayload callback. * * @param paymentPayload - The payment payload to enrich with extension data * @param paymentRequired - The PaymentRequired response containing extension declarations * @returns The enriched payment payload with extension data applied */ async enrichPaymentPayloadWithExtensions(paymentPayload, paymentRequired) { if (!paymentRequired.extensions || this.registeredExtensions.size === 0) { return paymentPayload; } let enriched = paymentPayload; for (const [key, extension] of this.registeredExtensions) { if (key in paymentRequired.extensions && extension.enrichPaymentPayload) { enriched = await extension.enrichPaymentPayload(enriched, paymentRequired); } } return enriched; } /** * 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/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 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({}) }); // 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