UNPKG

run402-mcp

Version:

MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.

567 lines • 22.6 kB
import { Run402Error } from "../errors.js"; export const DEFAULT_PAYMENT_MAX_USD_MICROS = 100_000; export const X402_COMMERCE_RESULT_SCHEMA_VERSION = "x402-commerce-result.v1"; export const X402_PAYMENT_POLICY_ERROR_CODES = [ "MERCHANT_RECEIPT_REQUIRED", "MERCHANT_RECEIPT_UNAVAILABLE", ]; export const X402_GATEWAY_AVAILABILITY_ERROR_CODE = "MERCHANT_EVIDENCE_UNAVAILABLE"; export const X402_MUTATION_STATES = [ "not_started", "committed", "unknown", ]; export const X402_RECOVERY_ACTIONS = [ "retry", "reconcile_payment", ]; export const X402_EVIDENCE_STATUSES = [ "verified", "absent", "invalid", "untrusted", "unavailable", ]; export function payFetchResultToJson(result, body) { const payment = result.payment; return { schema_version: X402_COMMERCE_RESULT_SCHEMA_VERSION, http_status: result.response.status, body, payment: payment ? { payment_id: payment.paymentId, amount_usd_micros: payment.amountUsdMicros, asset: payment.asset, network: payment.network, payer: payment.payer, pay_to: payment.payTo, transaction: payment.transaction, resource_url: payment.resourceUrl, settlement: payment.settlement, funds_moved: payment.fundsMoved, deduplicated: payment.deduplicated, delivery: payment.delivery, offer: { status: payment.offer.status, resource_url: payment.offer.resourceUrl, valid_until: payment.offer.validUntil, }, merchant_receipt: { status: payment.merchantReceipt.status, claim: payment.merchantReceipt.claim, issued_at: payment.merchantReceipt.issuedAt, }, signer_relationship: { kind: payment.signerRelationship.kind, merchant_root: payment.signerRelationship.merchantRoot, signer: payment.signerRelationship.signer, authorization_expires_at: payment.signerRelationship.authorizationExpiresAt, }, policy: { require_receipt: payment.policy.requireReceipt, status: payment.policy.status, }, evidence: { offer: payment.evidence.offer, merchant_receipt: payment.evidence.merchantReceipt, signer_authorization: payment.evidence.signerAuthorization, }, } : null, outcome: payment ? "paid" : "not_paid", replay: result.replay, next_actions: (result.nextActions ?? []).map((action) => ({ type: action.type, why: action.why, ...(action.request ? { request: action.request } : {}), ...(action.reusePayer ? { reuse_payer: true } : {}), ...(action.reuseIdempotencyKey ? { reuse_idempotency_key: true } : {}), })), }; } /** Structured local failure from {@link Pay.fetch}. */ export class PaymentBuyerError extends Run402Error { kind = "payment_buyer_error"; code; fundsMoved; paymentId; intentState; delivery; deduplicated; settledAt; cause; constructor(init) { const mutationState = init.fundsMoved === false ? "not_started" : init.fundsMoved === true ? "completed" : "ambiguous"; const body = init.body ?? { error: init.code, code: init.code, message: init.message, category: "payment", source: "sdk", retryable: init.retryable ?? false, safe_to_retry: init.safeToRetry ?? init.fundsMoved === false, mutation_state: mutationState, details: { funds_moved: init.fundsMoved, ...(init.details ?? {}), }, next_actions: init.nextActions, }; super(init.message, init.status ?? null, body, "paying an x402-priced URL"); this.code = init.code; this.fundsMoved = init.fundsMoved; this.paymentId = stringOrNull(body.payment_id); this.intentState = stringOrNull(body.intent_state); this.delivery = paymentDelivery(body.delivery); this.deduplicated = booleanOrNull(body.deduplicated); this.settledAt = stringOrNull(body.settled_at); if (init.cause !== undefined) this.cause = init.cause; } toJSON() { return { ...super.toJSON(), fundsMoved: this.fundsMoved, paymentId: this.paymentId, intentState: this.intentState, delivery: this.delivery, deduplicated: this.deduplicated, settledAt: this.settledAt, }; } } /** * A receipt policy failed after a paid response was received. The original * response and commerce result remain available for reconciliation. */ export class PaymentPolicyError extends PaymentBuyerError { response; result; mutationState; safeToRetry; nextActions; constructor(init) { super({ code: "MERCHANT_RECEIPT_UNAVAILABLE", message: init.message, fundsMoved: init.fundsMoved, safeToRetry: init.safeToRetry, details: { merchant_receipt_status: init.result.payment?.merchantReceipt.status ?? "unavailable", }, nextActions: init.nextActions.map((action) => ({ type: action.type, why: action.why, ...(action.request ? { request: action.request } : {}), ...(action.reusePayer ? { reuse_payer: action.reusePayer } : {}), ...(action.reuseIdempotencyKey ? { reuse_idempotency_key: action.reuseIdempotencyKey } : {}), })), body: { error: "MERCHANT_RECEIPT_UNAVAILABLE", code: "MERCHANT_RECEIPT_UNAVAILABLE", message: init.message, category: "payment_policy", source: "sdk", retryable: init.safeToRetry, safe_to_retry: init.safeToRetry, mutation_state: init.mutationState, funds_moved: init.fundsMoved, details: { merchant_receipt_status: init.result.payment?.merchantReceipt.status ?? "unavailable", }, next_actions: init.nextActions, }, }); this.response = init.response; this.result = init.result; this.mutationState = init.mutationState; this.safeToRetry = init.safeToRetry; this.nextActions = init.nextActions; } } export function isPaymentBuyerError(error) { return Boolean(error && typeof error === "object" && error.isRun402Error === true && error.kind === "payment_buyer_error"); } /** * Duck-typed Response check: the buyer's transport may hand back npm * undici's Response (vault-owned-dispatcher), which is not an instance * of the GLOBAL Response class even though it satisfies the same contract. */ function isResponseLike(value) { return (typeof value === "object" && value !== null && typeof value.status === "number" && typeof value.headers === "object"); } export function isPaymentPolicyError(error) { return Boolean(isPaymentBuyerError(error) && error.code === "MERCHANT_RECEIPT_UNAVAILABLE" && isResponseLike(error.response) && typeof error.result === "object"); } /** Arbitrary-URL buyer namespace. Node supplies the x402 executor; isomorphic callers may inject one. */ export class Pay { client; executor; constructor(client, executor) { this.client = client; this.executor = executor; } async fetch(url, init, options = {}) { const normalizedUrl = normalizePayUrl(url); const maxUsdMicros = normalizeMax(options.maxUsdMicros); const nextInit = withIdempotencyKey(init, options.idempotencyKey); if (this.executor) { return this.executor(normalizedUrl, nextInit, { maxUsdMicros, requireReceipt: options.requireReceipt === true, ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}), }); } const response = await this.client.fetch(normalizedUrl, nextInit); if (response.status === 402) { if (options.requireReceipt) { throw merchantReceiptRequiredError(); } throw walletUnavailableError(); } return { response, payment: null, outcome: "not_required", replay: await responseSignalsReplay(response, options.idempotencyKey !== undefined), ...payResponseMetadata(response), }; } } export function merchantReceiptRequiredError(details = {}) { return new PaymentBuyerError({ code: "MERCHANT_RECEIPT_REQUIRED", message: "No eligible payment requirement carried a valid wallet-rooted merchant offer.", fundsMoved: false, safeToRetry: true, details, nextActions: [], body: { error: "MERCHANT_RECEIPT_REQUIRED", code: "MERCHANT_RECEIPT_REQUIRED", message: "No eligible payment requirement carried a valid wallet-rooted merchant offer.", category: "payment_policy", source: "sdk", retryable: false, safe_to_retry: true, mutation_state: "not_started", funds_moved: false, details, next_actions: [], }, }); } export function paymentExceedsMaxError(challengedAmountUsdMicros, maxUsdMicros) { return new PaymentBuyerError({ code: "PAYMENT_EXCEEDS_MAX", message: `The endpoint requested ${challengedAmountUsdMicros} usd_micros, above this call's ` + `${maxUsdMicros} ceiling. Raise it explicitly with { maxUsdMicros: ${challengedAmountUsdMicros} } ` + `or CLI --max-usd ${(challengedAmountUsdMicros / 1_000_000).toFixed(6)}.`, fundsMoved: false, details: { challenged_amount_usd_micros: challengedAmountUsdMicros, max_usd_micros: maxUsdMicros, }, nextActions: [ { type: "edit_request", field: "maxUsdMicros", value: challengedAmountUsdMicros, why: "Raise the ceiling only after confirming this exact price is intended.", }, ], }); } export function walletUnavailableError(details = {}) { return new PaymentBuyerError({ code: "PAYMENT_WALLET_UNFUNDED", message: "No funded x402 wallet is available for this payment.", fundsMoved: false, details, nextActions: [ { type: "fund_wallet", why: "Fund the configured Run402 wallet with USDC on a challenge network, then retry.", }, { type: "run_command", command: "run402 wallets fund", why: "Open the canonical wallet funding flow.", }, { type: "run_command", command: "run402 init", why: "Create a wallet and use the testnet faucet when no wallet is configured.", }, ], }); } /** * The paid stack is an OPTIONAL peer set, so a consumer can legitimately be * unable to pay while holding a perfectly funded wallet. Reporting that as * PAYMENT_WALLET_UNFUNDED sends the caller to fund an already-funded wallet — * an unattended agent then loops between `init` (reports funded) and the * payment (reports unfunded). The remedy is an install, so say so. */ export function paidStackUnavailableError(missingPackages, details = {}) { const packages = [...missingPackages].sort(); return new PaymentBuyerError({ code: "PAYMENT_DEPENDENCY_MISSING", message: `This payment needs optional peer dependencies that are not installed: ${packages.join(", ")}. ` + `The wallet is not the problem — install them and retry.`, fundsMoved: false, details: { missing_packages: packages, ...details }, nextActions: [ { type: "run_command", command: `npm install ${packages.join(" ")}`, why: "Install the paid-request dependencies this environment is missing, then retry.", }, ], }); } export async function responseSignalsReplay(response, inspect) { const declared = booleanHeader(response.headers.get("x-run402-payment-deduplicated")); if (declared !== null) return declared; if (!inspect || !response.headers.get("content-type")?.includes("application/json")) return false; const declaredLength = Number(response.headers.get("content-length") ?? 0); if (Number.isFinite(declaredLength) && declaredLength > 1_000_000) return false; try { const body = await response.clone().json(); return body?.deduplicated === true; } catch { return false; } } /** Parse platform-owned payment response headers without consuming the response body. */ export function payResponseMetadata(response) { const funds = response.headers.get("x-run402-payment-funds-moved"); const delivery = response.headers.get("x-run402-payment-delivery"); return { paymentId: nonEmpty(response.headers.get("x-run402-payment-id")), deduplicated: booleanHeader(response.headers.get("x-run402-payment-deduplicated")), fundsMoved: funds === "true" ? true : funds === "false" ? false : funds === "unknown" ? "unknown" : null, delivery: delivery === "first" || delivery === "replay" || delivery === "none" ? delivery : null, settledAt: nonEmpty(response.headers.get("x-run402-payment-settled-at")), intentState: nonEmpty(response.headers.get("x-run402-payment-intent-state")), }; } export const RUN402_PENDING_CLASSIFIER_VERSION = 1; const RESERVED_RUN402_LABELS = new Set([ "api", "admin", "assets", "cdn", "dashboard", "docs", "help", "mail", "sites", "static", "status", "support", "www", ]); /** Exact DNS-label classifier for Run402-owned tenant and deployment hosts. */ export function isTrustedRun402PaymentUrl(input, options = {}) { let url; try { url = input instanceof URL ? input : new URL(input); } catch { return false; } if (options.allowTestLocalhost && url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1")) return true; if (url.protocol !== "https:" || url.username || url.password || url.port) return false; const labels = url.hostname.toLowerCase().split("."); if (labels.length === 3 && labels[1] === "run402" && (labels[2] === "com" || labels[2] === "app")) { return labels[0].length > 0 && !RESERVED_RUN402_LABELS.has(labels[0]); } return labels.length === 4 && labels[1] === "sites" && labels[2] === "run402" && (labels[3] === "com" || labels[3] === "app") && labels[0].length > 0; } /** * Trust a pending result only at the complete signed-response boundary. The * caller supplies `paymentBearing` and `redirectsDisabled` from its executor; * shape alone is intentionally insufficient for arbitrary/custom domains. */ export function isTrustedRun402PendingResponse(input) { if (!input.paymentBearing || !input.redirectsDisabled || input.response.redirected) return false; if (input.response.status !== 409 || input.envelope?.code !== "PAYMENT_INTENT_PENDING") return false; if (input.response.headers.get("x-run402-payment-intent-state") !== "pending") return false; if (!input.response.url) return false; try { const requestUrl = new URL(input.requestUrl); const responseUrl = new URL(input.response.url); return requestUrl.origin === responseUrl.origin && isTrustedRun402PaymentUrl(requestUrl, { allowTestLocalhost: input.allowTestLocalhost }); } catch { return false; } } export async function readPaymentErrorEnvelope(response) { if (!response.headers.get("content-type")?.includes("application/json")) return null; const length = Number(response.headers.get("content-length") ?? 0); if (Number.isFinite(length) && length > 1_000_000) return null; try { const value = await response.clone().json(); return value && typeof value === "object" && !Array.isArray(value) ? value : null; } catch { return null; } } export function gatewayPaymentBuyerError(response, envelope) { const code = envelope.code; if (!isPaymentBuyerErrorCode(code)) return null; const metadata = payResponseMetadata(response); const declaredFunds = envelope.funds_moved; const fundsMoved = declaredFunds === true || declaredFunds === false || declaredFunds === "unknown" ? declaredFunds : metadata.fundsMoved ?? "unknown"; const body = { ...envelope, ...(envelope.payment_id === undefined && metadata.paymentId !== null ? { payment_id: metadata.paymentId } : {}), ...(envelope.deduplicated === undefined && metadata.deduplicated !== null ? { deduplicated: metadata.deduplicated } : {}), ...(envelope.delivery === undefined && metadata.delivery !== null ? { delivery: metadata.delivery } : {}), ...(envelope.settled_at === undefined && metadata.settledAt !== null ? { settled_at: metadata.settledAt } : {}), ...(envelope.intent_state === undefined && metadata.intentState !== null ? { intent_state: metadata.intentState } : {}), ...(envelope.funds_moved === undefined && metadata.fundsMoved !== null ? { funds_moved: metadata.fundsMoved } : {}), }; return new PaymentBuyerError({ code, message: typeof envelope.message === "string" ? envelope.message : code, fundsMoved, details: isRecord(envelope.details) ? envelope.details : {}, nextActions: Array.isArray(envelope.next_actions) ? envelope.next_actions : [], retryable: typeof envelope.retryable === "boolean" ? envelope.retryable : false, safeToRetry: typeof envelope.safe_to_retry === "boolean" ? envelope.safe_to_retry : false, status: response.status, body, }); } function isPaymentBuyerErrorCode(value) { return typeof value === "string" && new Set([ "PAYMENT_EXCEEDS_MAX", "PAYMENT_WALLET_UNFUNDED", "PAYMENT_NETWORK_UNSUPPORTED", "PAYMENT_INTENT_PENDING", "PAYMENT_DESTINATION_DRAINING", "PAYMENT_INTENT_DESTINATION_CHANGED", "PAYMENT_INTENT_FENCE_EXPIRED", "PAYMENT_AUTHORIZATION_LIFETIME_EXCEEDED", "PAYMENT_CALLER_IDENTITY_NOT_ACTIVE", "IDEMPOTENCY_KEY_REUSED", "INVALID_IDEMPOTENCY_KEY", "IDEMPOTENCY_KEY_PAYER_REQUIRED", "PAYMENT_SETTLEMENT_FAILED", "MERCHANT_RECEIPT_REQUIRED", "MERCHANT_RECEIPT_UNAVAILABLE", ]).has(value); } function nonEmpty(value) { const trimmed = value?.trim(); return trimmed ? trimmed : null; } function booleanHeader(value) { return value === "true" ? true : value === "false" ? false : null; } function stringOrNull(value) { return typeof value === "string" && value.length > 0 ? value : null; } function booleanOrNull(value) { return typeof value === "boolean" ? value : null; } function paymentDelivery(value) { return value === "first" || value === "replay" || value === "none" ? value : null; } function isRecord(value) { return Boolean(value && typeof value === "object" && !Array.isArray(value)); } function normalizePayUrl(input) { let url; try { url = input instanceof URL ? input : new URL(input); } catch (cause) { throw new PaymentBuyerError({ code: "PAYMENT_SETTLEMENT_FAILED", message: "pay.fetch requires an absolute HTTP(S) URL.", fundsMoved: false, details: { field: "url" }, nextActions: [{ type: "edit_request", field: "url", why: "Pass the complete priced endpoint URL." }], cause, }); } if (url.protocol !== "https:" && url.protocol !== "http:") { throw new PaymentBuyerError({ code: "PAYMENT_SETTLEMENT_FAILED", message: "pay.fetch supports only HTTP(S) URLs.", fundsMoved: false, details: { field: "url", protocol: url.protocol }, nextActions: [{ type: "edit_request", field: "url", why: "Use an https:// or http:// endpoint." }], }); } return url.toString(); } function normalizeMax(value) { const normalized = value ?? DEFAULT_PAYMENT_MAX_USD_MICROS; if (!Number.isSafeInteger(normalized) || normalized < 0) { throw new PaymentBuyerError({ code: "PAYMENT_SETTLEMENT_FAILED", message: "maxUsdMicros must be a non-negative safe integer.", fundsMoved: false, details: { field: "maxUsdMicros", value: normalized }, nextActions: [{ type: "edit_request", field: "maxUsdMicros", why: "Pass an integer number of usd_micros." }], }); } return normalized; } function withIdempotencyKey(init, key) { if (key === undefined) return init; if (key.trim() === "") { throw new PaymentBuyerError({ code: "PAYMENT_SETTLEMENT_FAILED", message: "idempotencyKey must not be empty.", fundsMoved: false, details: { field: "idempotencyKey" }, nextActions: [{ type: "edit_request", field: "idempotencyKey", why: "Use a stable non-empty key for this paid intent." }], }); } const headers = new Headers(init?.headers); headers.set("Idempotency-Key", key); return { ...init, headers }; } //# sourceMappingURL=pay.js.map