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.

2,042 lines • 93.4 kB
/**
 * Node-only x402-wrapped fetch. Reads the wallet file, checks on-chain
 * USDC balances through independent RPC providers, and returns a fetch wrapper
 * that auto-signs 402 responses only when the requested chain has confirmed
 * funds.
 *
 * Balance reads are pre-payment, read-only operations. They may be retried and
 * failed over safely; a payment payload has not been created or submitted yet.
 * An exhausted RPC check is never represented as a zero balance.
 *
 * The viem / @x402/* / mppx imports live behind `./_paid-stack.ts` so the
 * SDK's direct surface to those packages is auditable from one file and the
 * packages can be declared as optional peer dependencies in package.json.
 *
 * Never calls `process.exit` — the SDK leaves exit-code decisions to the
 * CLI edge.
 */
import { readWallet } from "../../core-dist/wallet.js";
import { getApiBase } from "../../core-dist/config.js";
import { isTerminalRoomInviteRefusal } from "../namespaces/rooms.js";
import { AsyncLocalStorage } from "node:async_hooks";
import { createHash } from "node:crypto";
import { LocalError, PaymentAttemptError, Run402Error, } from "../errors.js";
import { PaidStackUnavailable, loadMppStack, loadX402Stack } from "./_paid-stack.js";
import { createLightningFetch, createLightningPayExecutor } from "./lightning-paid-fetch.js";
import { DEFAULT_PAYMENT_MAX_USD_MICROS, PaymentBuyerError, PaymentPolicyError, gatewayPaymentBuyerError, isTrustedRun402PaymentUrl, isTrustedRun402PendingResponse, payResponseMetadata, merchantReceiptRequiredError, paymentExceedsMaxError, readPaymentErrorEnvelope, responseSignalsReplay, paidStackUnavailableError, walletUnavailableError, } from "../namespaces/pay.js";
import { attemptIdFromRequest, createFilePaymentAttemptStore, createPaymentAttemptId, hasPaymentAuthorization, requestSummary, withPaymentAttemptHeader, } from "./payment-attempts.js";
import { sdkFetch } from "./http-dispatcher.js";
import { paymentOffers, switchRailFor } from "./payment-offers.js";
import { resolveWalletAccount } from "./signers/index.js";
import { PATH_USD, TEMPO_CHAINS, TEMPO_FEE_HEADROOM_USD_MICROS, tempoChainById } from "./tempo.js";
import { createMppPayExecutor } from "./mpp-pay-executor.js";
/**
 * A machine-readable x402 balance-preflight failure.
 *
 * `safeToRetry` is true only for read-only RPC failures. A confirmed balance
 * miss is not retryable without changing wallet funds or payment requirements.
 * Both states are emitted before payment payload creation, so
 * `mutationState` is always `not_started`.
 */
/**
 * The largest amount a first-party (api.run402.com) challenge may ask for:
 * the team tier. Every gateway price is at or under it, so a challenge above
 * it is not a price, it is a compromised or misconfigured host — refuse to
 * sign rather than trust a number the server chose. Arbitrary-URL purchases
 * through `pay.fetch` are bounded separately by the caller's `maxUsdMicros`.
 */
export const FIRST_PARTY_MAX_PAYMENT_USD_MICROS = 20_000_000;
function originOf(url) {
    if (!url)
        return null;
    try {
        return new URL(url).origin;
    }
    catch {
        return null;
    }
}
function challengeOrigin(required) {
    const resource = required?.resource;
    return typeof resource?.url === "string" ? originOf(resource.url) : null;
}
/**
 * Refuse a first-party challenge whose every accepted requirement exceeds the
 * first-party cap. Requirements at or under the cap are left alone; the
 * selector picks among them as usual.
 */
export function assertFirstPartyPaymentWithinCap(required, firstPartyOrigin) {
    if (!firstPartyOrigin || challengeOrigin(required) !== firstPartyOrigin)
        return;
    const accepts = required.accepts;
    if (!Array.isArray(accepts) || accepts.length === 0)
        return;
    const amounts = accepts
        .map((entry) => Number(entry?.amount))
        .filter((amount) => Number.isFinite(amount));
    if (amounts.length === 0)
        return;
    const smallest = Math.min(...amounts);
    if (smallest <= FIRST_PARTY_MAX_PAYMENT_USD_MICROS)
        return;
    throw new PaymentBuyerCapError(smallest);
}
/** A first-party challenge above the largest known price. Never retried blindly. */
export class PaymentBuyerCapError extends Run402Error {
    kind = "local_error";
    code = "X402_AMOUNT_EXCEEDS_FIRST_PARTY_CAP";
    constructor(requestedUsdMicros) {
        super("The run402 API asked for a payment above the largest known tier price; refusing to sign it.", null, {
            error: "X402_AMOUNT_EXCEEDS_FIRST_PARTY_CAP",
            code: "X402_AMOUNT_EXCEEDS_FIRST_PARTY_CAP",
            category: "payment",
            source: "sdk",
            retryable: false,
            safe_to_retry: false,
            mutation_state: "not_started",
            details: {
                phase: "payment_signing",
                payment_started: false,
                requested_usd_micros: requestedUsdMicros,
                cap_usd_micros: FIRST_PARTY_MAX_PAYMENT_USD_MICROS,
            },
            next_actions: [{
                    type: "verify_api_base",
                    why: "Check RUN402_API_BASE points at the real gateway; a genuine price never exceeds the team tier.",
                }],
        }, "signing an x402 payment");
    }
}
/**
 * A gateway 402 that names the caller's allowance (`allowance.allowance_usd_micros`,
 * `allowance.price_usd_micros`, `allowance.shortfall_usd_micros`) turns a bare
 * "insufficient USDC" into the actual situation: the organization's allowance
 * was checked first and fell short by a known amount, and a voucher or top-up
 * closes the gap without any on-chain funds. Only an insufficient-funds verdict
 * is reshaped; an RPC failure stays an RPC failure.
 */
function withAllowanceShortfall(error, challengeBody) {
    const block = challengeBody?.allowance;
    if (error.code !== "X402_INSUFFICIENT_FUNDS" || !block || typeof block !== "object")
        return error;
    const a = block;
    const allowance = Number(a.allowance_usd_micros);
    const price = Number(a.price_usd_micros);
    const shortfall = Number(a.shortfall_usd_micros);
    if (![allowance, price, shortfall].every(Number.isFinite))
        return error;
    const priorDetails = error.body?.details ?? {};
    return new X402BalanceError("X402_INSUFFICIENT_FUNDS", `The organization's allowance ($${(allowance / 1_000_000).toFixed(2)}) does not cover this ($${(price / 1_000_000).toFixed(2)}), and the wallet holds no USDC for the $${(shortfall / 1_000_000).toFixed(2)} shortfall.`, {
        ...priorDetails,
        allowance: { allowance_usd_micros: allowance, price_usd_micros: price, shortfall_usd_micros: shortfall },
    }, error.cause, [
        { type: "redeem_voucher", cli: "run402 redeem <code>", why: "A promo code adds to the organization's allowance, which settles a tier with no on-chain payment." },
        { type: "top_up", why: "Top up the organization's allowance (Lightning or card), then retry." },
        { type: "fund_wallet", why: "Or fund the wallet with USDC on an accepted network for the shortfall, then retry." },
    ]);
}
export class X402BalanceError extends Run402Error {
    kind = "local_error";
    code;
    cause;
    constructor(code, message, details, cause, nextActions, context = "checking x402 USDC balance") {
        const rpcFailure = code !== "X402_INSUFFICIENT_FUNDS";
        super(message, null, {
            error: code,
            message,
            code,
            category: rpcFailure ? "network" : "payment_required",
            source: "sdk",
            retryable: rpcFailure,
            safe_to_retry: rpcFailure,
            mutation_state: "not_started",
            details: {
                phase: "balance_preflight",
                payment_started: false,
                ...details,
            },
            next_actions: nextActions ?? (rpcFailure
                ? [
                    {
                        type: "retry",
                        why: "Retry the identical request; no payment payload was created.",
                    },
                ]
                : [
                    {
                        type: "fund_wallet",
                        why: "Fund the configured wallet on an accepted network, then retry.",
                    },
                ]),
        }, context);
        this.code = code;
        if (cause !== undefined)
            this.cause = cause;
    }
}
const USDC_ABI = [
    {
        name: "balanceOf",
        type: "function",
        stateMutability: "view",
        inputs: [{ name: "account", type: "address" }],
        outputs: [{ name: "", type: "uint256" }],
    },
];
const USDC_MAINNET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
// Independent public providers. Do not add credential-bearing URLs here.
const BASE_RPC_URLS = [
    "https://mainnet.base.org",
    "https://base-rpc.publicnode.com",
    "https://1rpc.io/base",
];
const BASE_SEPOLIA_RPC_URLS = [
    "https://sepolia.base.org",
    "https://base-sepolia-rpc.publicnode.com",
    "https://base-sepolia.drpc.org",
];
const DEFAULT_ATTEMPTS_PER_PROVIDER = 2;
const DEFAULT_BASE_DELAY_MS = 100;
const MAX_PENDING_POLICY_ERRORS = 32;
let warnedMissingDeps = false;
let missingPaidStackPackages = null;
let policyErrorSequence = 0;
const policyErrors = new Map();
let stackLoaders = {
    x402: loadX402Stack,
    mpp: loadMppStack,
};
/** @internal Test seam; not re-exported from `@run402/sdk/node`. */
export function _setPaidStackLoadersForTest(loaders) {
    stackLoaders = {
        x402: loaders?.x402 ?? loadX402Stack,
        mpp: loaders?.mpp ?? loadMppStack,
    };
}
class AttemptJournalWriteError extends Error {
    cause;
    constructor(cause) {
        super("Could not persist the x402 payment attempt journal");
        this.cause = cause;
        this.name = "AttemptJournalWriteError";
    }
}
function classifyRpcFailure(err) {
    const record = err && typeof err === "object" ? err : null;
    const code = typeof record?.code === "string" ? record.code.toLowerCase() : "";
    const status = typeof record?.status === "number" ? record.status : null;
    const message = err instanceof Error ? err.message.toLowerCase() : "";
    if (code.includes("timeout") ||
        code === "etimedout" ||
        code === "abort_err" ||
        /timed? out|timeout|aborted/.test(message)) {
        return "timeout";
    }
    if (status === 429 || code.includes("rate") || /rate.?limit|too many requests|\b429\b/.test(message)) {
        return "rate_limited";
    }
    if (code === "econnreset" ||
        code === "econnrefused" ||
        code === "enotfound" ||
        code === "eai_again" ||
        /network|connection|socket|dns|fetch failed/.test(message)) {
        return "network";
    }
    return "rpc_error";
}
function exhaustedRpcCode(failures) {
    if (failures.length > 0 && failures.every((failure) => failure.reason === "timeout")) {
        return "X402_RPC_TIMEOUT";
    }
    if (failures.length > 0 && failures.every((failure) => failure.reason === "rate_limited")) {
        return "X402_RPC_RATE_LIMITED";
    }
    return "X402_RPC_UNAVAILABLE";
}
function delayForAttempt(baseDelayMs, retryIndex, random) {
    const exponential = baseDelayMs * 2 ** retryIndex;
    return Math.round(exponential + exponential * 0.25 * random());
}
/** @internal Exported only for deterministic source-level tests; not re-exported by the package. */
export async function checkBalanceAcrossProviders(clients, tokenAddress, walletAddress, network, options = {}) {
    const attemptsPerProvider = options.attemptsPerProvider ?? DEFAULT_ATTEMPTS_PER_PROVIDER;
    const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
    const sleep = options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
    const random = options.random ?? Math.random;
    const failures = [];
    let lastCause;
    for (let providerIndex = 0; providerIndex < clients.length; providerIndex += 1) {
        for (let attempt = 1; attempt <= attemptsPerProvider; attempt += 1) {
            if (attempt > 1) {
                await sleep(delayForAttempt(baseDelayMs, attempt - 2, random));
            }
            try {
                return await clients[providerIndex].readContract({
                    address: tokenAddress,
                    abi: USDC_ABI,
                    functionName: "balanceOf",
                    args: [walletAddress],
                });
            }
            catch (err) {
                lastCause = err;
                failures.push({
                    provider_index: providerIndex,
                    attempt,
                    reason: classifyRpcFailure(err),
                });
            }
        }
    }
    const code = exhaustedRpcCode(failures);
    throw new X402BalanceError(code, `Unable to confirm the ${options.balanceLabel ?? "x402 USDC"} balance on ${network}; no zero balance was assumed.`, {
        network,
        balance_status: "unknown",
        providers_exhausted: true,
        providers_attempted: clients.length,
        attempts: failures,
    }, lastCause, undefined, options.balanceLabel ? `checking ${options.balanceLabel} balance` : undefined);
}
function bigintAmount(value) {
    if (typeof value !== "string" || !/^\d+$/.test(value))
        return null;
    try {
        return BigInt(value);
    }
    catch {
        return null;
    }
}
/** @internal Exported only for deterministic source-level tests; not re-exported by the package. */
export function filterAffordableRequirements(requirements, balances, 
/**
 * Payer address per network. Surfaced in the insufficient-funds error so the
 * `fund_wallet` next_action names WHERE to send money — "fund the wallet" is
 * not executable without an address. Optional: omitting it only omits the
 * `payers` detail.
 */
payers = {}) {
    const recognized = requirements.filter((requirement) => typeof requirement.network === "string" && requirement.network in balances);
    const known = recognized.filter((requirement) => typeof requirement.network === "string" &&
        balances[requirement.network]?.status === "known" &&
        bigintAmount(requirement.amount) !== null);
    const affordable = known.filter((requirement) => {
        const state = balances[requirement.network];
        return state.balance >= bigintAmount(requirement.amount);
    });
    if (affordable.length > 0)
        return affordable;
    const unknown = recognized
        .map((requirement) => (typeof requirement.network === "string" ? balances[requirement.network] : undefined))
        .find((state) => state?.status === "unknown");
    if (unknown)
        throw unknown.error;
    // Only call this confirmed-insufficient when every recognized requirement
    // had a valid amount and a successful authoritative balance read.
    if (recognized.length > 0 && known.length === recognized.length) {
        const balanceDetails = Object.fromEntries(Object.entries(balances)
            .filter((entry) => entry[1].status === "known")
            .map(([network, state]) => [network, state.balance.toString()]));
        const payerDetails = Object.fromEntries(Object.keys(balanceDetails)
            .filter((network) => payers[network])
            .map((network) => [network, payers[network]]));
        throw new X402BalanceError("X402_INSUFFICIENT_FUNDS", "The configured wallet has insufficient confirmed USDC for the accepted x402 payment requirements.", {
            balances: balanceDetails,
            requirements: recognized.map((requirement) => ({
                network: requirement.network,
                amount: requirement.amount,
            })),
            ...(Object.keys(payerDetails).length > 0 ? { payers: payerDetails } : {}),
        });
    }
    // Preserve the x402 library's invalid/unsupported-requirement behavior; it
    // would be unfaithful to label malformed input as a confirmed balance miss.
    return [];
}
function wrapPolicyError(error) {
    policyErrorSequence += 1;
    const token = `run402-x402-preflight-${policyErrorSequence}`;
    // @x402/fetch currently preserves the thrown message, so the outer wrapper
    // removes this entry immediately. Keep the relay bounded anyway: if a future
    // release rewrites messages, no process-lifetime sentinel leak can grow.
    if (policyErrors.size >= MAX_PENDING_POLICY_ERRORS) {
        const oldest = policyErrors.keys().next().value;
        if (oldest)
            policyErrors.delete(oldest);
    }
    policyErrors.set(token, error);
    return new Error(`${token}: ${error.message}`);
}
function unwrapPolicyError(error) {
    const message = error instanceof Error ? error.message : "";
    const token = /run402-x402-preflight-\d+/.exec(message)?.[0];
    if (!token)
        return null;
    const structured = policyErrors.get(token) ?? null;
    policyErrors.delete(token);
    return structured;
}
function createRpcClients(stack, chain, urls) {
    return urls.map((url) => stack.createPublicClient({ chain, transport: stack.http(url) }));
}
async function balanceState(clients, tokenAddress, walletAddress, network) {
    try {
        return {
            status: "known",
            balance: await checkBalanceAcrossProviders(clients, tokenAddress, walletAddress, network),
        };
    }
    catch (err) {
        if (err instanceof X402BalanceError)
            return { status: "unknown", error: err };
        throw err;
    }
}
/**
 * MPP on Tempo: keep the Tempo challenges the wallet can cover, in the order
 * the server offered them (spec `mpp-tempo-client-surface`). Run402 offers
 * both Tempo chains on every SKU, mainnet first on paid SKUs and Moderato
 * first on the free tier; mppx pays the first candidate it is handed, so
 * dropping the chains this wallet cannot pay lets a faucet-funded wallet pay
 * Moderato and a mainnet-funded wallet pay mainnet with no chain setting.
 *
 * "Cover" is the amount plus the sender's Tempo fee headroom. When nothing is
 * covered this raises the same balance-preflight errors as x402: an RPC
 * failure when a read failed, else `X402_INSUFFICIENT_FUNDS` naming every
 * offered chain, before anything is signed.
 */
export async function orderTempoCandidates(candidates, address, readBalance) {
    const offers = candidates.flatMap((candidate) => {
        const { method, intent, request } = candidate.challenge;
        if (method !== "tempo" || intent !== "charge")
            return [];
        const req = (request ?? {});
        const chain = tempoChainById(req.methodDetails?.chainId);
        const amount = bigintAmount(req.amount);
        return chain && amount !== null ? [{ candidate, chain, amount }] : [];
    });
    if (offers.length === 0)
        return [...candidates];
    const chains = [...new Set(offers.map((offer) => offer.chain))];
    const states = new Map(await Promise.all(chains.map(async (chain) => {
        try {
            return [chain, { status: "known", balance: await readBalance(chain) }];
        }
        catch (err) {
            const error = err instanceof X402BalanceError
                ? err
                : new X402BalanceError("X402_RPC_UNAVAILABLE", `Could not read the wallet's pathUSD balance on ${chain.name}.`, { network: chain.network }, err, undefined, "checking MPP pathUSD balance");
            return [chain, { status: "unknown", error }];
        }
    })));
    const covered = new Set(offers
        .filter(({ chain, amount }) => {
        const state = states.get(chain);
        return state?.status === "known" && state.balance >= amount + TEMPO_FEE_HEADROOM_USD_MICROS;
    })
        .map((offer) => offer.candidate));
    if (covered.size > 0) {
        const tempo = new Set(offers.map((offer) => offer.candidate));
        return candidates.filter((candidate) => !tempo.has(candidate) || covered.has(candidate));
    }
    const unknown = offers.map(({ chain }) => states.get(chain)).find((state) => state?.status === "unknown");
    if (unknown)
        throw unknown.error;
    const first = offers[0].chain;
    const moderatoOffered = offers.some(({ chain }) => chain.testnet);
    const nextActions = first.testnet
        ? [{ type: "fund_wallet", command: "run402 wallets fund", why: `Request Moderato test pathUSD for ${address}, then retry.` }]
        : [
            { type: "fund_wallet", why: `Fund ${address} with pathUSD on ${first.name} (${first.network}), then retry.` },
            ...(moderatoOffered
                ? [{ type: "fund_wallet", command: "run402 wallets fund", why: "Or request Moderato test pathUSD, which this purchase also accepts, then retry." }]
                : []),
        ];
    throw new X402BalanceError("X402_INSUFFICIENT_FUNDS", "The configured wallet has insufficient confirmed pathUSD on every Tempo chain this payment accepts.", {
        token: "pathUSD",
        asset: PATH_USD,
        fee_headroom_usd_micros: Number(TEMPO_FEE_HEADROOM_USD_MICROS),
        requirements: offers.map(({ chain, amount }) => ({ network: chain.network, amount: amount.toString() })),
        balances: Object.fromEntries(chains.map((chain) => [chain.network, states.get(chain).balance.toString()])),
        payers: Object.fromEntries(chains.map((chain) => [chain.network, address])),
    }, undefined, nextActions, "checking MPP pathUSD balance");
}
/** Reads pathUSD on one Tempo chain through its RPC, with the x402 retry policy. */
async function tempoBalanceReader(address) {
    const { createPublicClient, defineChain, http } = await import("viem");
    return (chain) => {
        const client = createPublicClient({
            chain: defineChain({
                id: chain.id,
                name: chain.name,
                nativeCurrency: { name: "pathUSD", symbol: "pathUSD", decimals: 6 },
                rpcUrls: { default: { http: [chain.rpcUrl] } },
            }),
            transport: http(chain.rpcUrl),
        });
        return checkBalanceAcrossProviders([client], PATH_USD, address, chain.network, { balanceLabel: "MPP pathUSD" });
    };
}
/**
 * True when `response` genuinely came back from `configuredApiBase`'s
 * origin — never redirected, and never merely a matching string (an actual
 * `fetch` return carries the real settled URL in `.url`). Deliberately NOT
 * `isTrustedRun402PaymentUrl`'s tenant-hostname classifier: that function
 * exists for `r.pay.fetch`'s arbitrary-URL buyer (a DIFFERENT trust
 * question — "is this some run402-hosted tenant's own paid resource?") and
 * excludes `api.run402.com` by name (a reserved label). This is the
 * SIMPLER, narrower question the default fetch actually needs: "is this
 * response from the one origin this SDK instance is configured to talk
 * to?" — true for `api.run402.com` in production and for whatever
 * `RUN402_API_BASE`/`apiBase` names in a test or staging environment.
 */
function isConfiguredApiOrigin(configuredApiBase, response) {
    if (!response.url || response.redirected)
        return false;
    try {
        return new URL(configuredApiBase).origin === new URL(response.url).origin;
    }
    catch {
        return false;
    }
}
export async function setupPaidFetch(options = {}) {
    validatePaymentSource(options);
    const configuredApiBase = options.apiBase ?? getApiBase();
    // Malformed or missing selected local state degrades to an unwrapped 402,
    // but it never falls back to a different wallet source.
    const resolvedWallet = options.paymentSigner ? null : await resolveWallet(options);
    const localWallet = resolvedWallet?.localWallet ?? null;
    if (!localWallet && !options.paymentSigner)
        return null;
    const rail = options.railOverride ?? localWallet?.rail;
    try {
        if (rail === "lightning" && localWallet?.lightning?.nwc) {
            // The Lightning wallet: pay the seller's Lightning challenge from the
            // agent's budgeted wallet on Run402's Hub; fall back to the x402 buyer
            // built from the same wallet when no Lightning challenge is offered.
            const lightning = localWallet.lightning;
            const fallback = async () => setupPaidFetch({ ...options, railOverride: "x402" });
            const fetchFn = createLightningFetch({
                pairingUri: lightning.nwc,
                baseFetch: sdkFetch,
                fallback,
            });
            // `pay.fetch` on this rail: the same buyer, reporting the `lnbtc`
            // payment it made; a 402 with no Lightning entry is the x402 buyer's.
            const pay = async (url, init, payOptions) => {
                const executor = createLightningPayExecutor({ pairingUri: lightning.nwc, baseFetch: sdkFetch, fallback: async () => null });
                const result = await executor(url, init, payOptions);
                if (result.payment || result.response.status !== 402)
                    return result;
                await result.response.body?.cancel().catch(() => undefined);
                const x402 = await fallback();
                if (!x402?.pay)
                    return result;
                return x402.pay(url, init, payOptions);
            };
            return withPayer(fetchFn, {
                source: resolvedWallet.source,
                rail: "lightning",
                payers: [{ address: lightning.lightning_address ?? lightning.wallet_id }],
            }, undefined, pay);
        }
        if (rail === "mpp" && localWallet) {
            const stack = await stackLoaders.mpp();
            const account = await resolveWalletAccount(localWallet, { privateKeyToAccount: stack.privateKeyToAccount });
            const readBalance = await tempoBalanceReader(localWallet.address);
            const mppx = stack.Mppx.create({
                polyfill: false,
                methods: [stack.tempo({ account })],
                // Pay the first offered Tempo chain this wallet can cover.
                orderChallenges: (candidates) => orderTempoCandidates(candidates, localWallet.address, readBalance),
            });
            // `pay.fetch` on this rail: the same selection, with the spend guard
            // applied to the chosen challenge before mppx signs it.
            const pay = createMppPayExecutor({
                prepare: (response, request) => mppx.preparePayment(response, { request }),
                payer: localWallet.address,
                baseFetch: sdkFetch,
                fingerprint: paymentRequestFingerprint,
                unsupported: (offers) => paymentNetworkUnsupportedError(offerNetworks(offers), TEMPO_CHAINS.map((chain) => chain.network), {}, offers, "mpp"),
            });
            return withPayer(mppx.fetch, {
                source: resolvedWallet.source,
                rail: "mpp",
                payers: [{ address: localWallet.address }],
            }, undefined, pay);
        }
        // Default: x402 on Base + Base Sepolia. Each chain has its own independent
        // provider list; one degraded chain does not erase a confirmed balance on
        // the other chain.
        const stack = await stackLoaders.x402();
        const mainnetClients = createRpcClients(stack, stack.base, BASE_RPC_URLS);
        const sepoliaClients = createRpcClients(stack, stack.baseSepolia, BASE_SEPOLIA_RPC_URLS);
        const [mainnetSigner, sepoliaSigner] = options.paymentSigner
            ? await Promise.all([
                options.paymentSigner.getSigner({ network: "eip155:8453", publicClient: mainnetClients[0] }),
                options.paymentSigner.getSigner({ network: "eip155:84532", publicClient: sepoliaClients[0] }),
            ])
            : await localWalletSigners(stack, localWallet, mainnetClients[0], sepoliaClients[0]);
        if (!mainnetSigner && !sepoliaSigner)
            return null;
        const balances = {};
        const refreshBalances = async () => {
            const [mainnet, sepolia] = await Promise.all([
                mainnetSigner
                    ? balanceState(mainnetClients, USDC_MAINNET, mainnetSigner.address, "eip155:8453")
                    : null,
                sepoliaSigner
                    ? balanceState(sepoliaClients, USDC_SEPOLIA, sepoliaSigner.address, "eip155:84532")
                    : null,
            ]);
            if (mainnet)
                balances["eip155:8453"] = mainnet;
            else
                delete balances["eip155:8453"];
            if (sepolia)
                balances["eip155:84532"] = sepolia;
            else
                delete balances["eip155:84532"];
        };
        await refreshBalances();
        const client = new stack.x402Client();
        // Spend controls are run402's, not the library's. `@x402/core` >= 2.22 ships
        // a client-side default of $1 per payment; inherited silently, it refused to
        // sign a $5 hobby or $20 team purchase from every fresh install (the
        // 2026-09-20 launch-voucher rehearsal found it). The first-party cap below
        // is explicit and derived from the price list; arbitrary-URL purchases are
        // bounded by `pay.fetch`'s own `maxUsdMicros`. Asset selection is already
        // run402's (known USDC per network), so nothing the library's default
        // allow-list protected is left open.
        if (typeof client.setSpendControls === "function")
            client.setSpendControls(false);
        const firstPartyOrigin = originOf(options.apiBase);
        // Initialization may happen on an unpaid request (including the faucet),
        // before funds arrive. Select requirements from a fresh balance snapshot
        // whenever a new payment payload is created, in both automatic and buyer
        // flows. Keep the selected signers and any replayable proof unchanged.
        const createPaymentPayload = client.createPaymentPayload.bind(client);
        client.createPaymentPayload = async (required) => {
            assertFirstPartyPaymentWithinCap(required, firstPartyOrigin);
            await refreshBalances();
            return createPaymentPayload(required);
        };
        if (mainnetSigner) {
            client.register("eip155:8453", new stack.ExactEvmScheme(stack.toClientEvmSigner(mainnetSigner, mainnetClients[0])));
        }
        if (sepoliaSigner) {
            client.register("eip155:84532", new stack.ExactEvmScheme(stack.toClientEvmSigner(sepoliaSigner, sepoliaClients[0])));
        }
        client.registerPolicy((_version, requirements) => {
            try {
                return filterAffordableRequirements(requirements, balances, {
                    ...(mainnetSigner ? { "eip155:8453": mainnetSigner.address } : {}),
                    ...(sepoliaSigner ? { "eip155:84532": sepoliaSigner.address } : {}),
                });
            }
            catch (err) {
                // @x402/fetch currently wraps payment-creation errors in a plain Error.
                // A per-call token lets the outer wrapper restore the original typed
                // error without putting wallet or RPC details into the sentinel.
                if (err instanceof X402BalanceError)
                    throw wrapPolicyError(err);
                throw err;
            }
        });
        const trackedFetch = createTrackedX402Fetch((baseFetch, rawClient) => {
            const wrapped = stack.wrapFetchWithPayment(baseFetch, rawClient);
            return async (input, init) => {
                try {
                    return await wrapped(input, init);
                }
                catch (err) {
                    throw unwrapPolicyError(err) ?? err;
                }
            };
        }, client, {
            // Live-proof defect B: recognize this SDK's own gateway's terminal,
            // never-settled refusal codes (see isTerminalRoomInviteRefusal) so
            // the caller gets the gateway's own typed envelope instead of a
            // synthesized X402_PAYMENT_OUTCOME_AMBIGUOUS. Scoped to responses
            // genuinely from the configured API origin — an arbitrary
            // third-party paid URL (this default fetch's other callers) stays
            // ambiguous, unchanged, exactly as today.
            async classifyPaymentResponse(response) {
                if (!isConfiguredApiOrigin(configuredApiBase, response))
                    return "ambiguous";
                const envelope = await readPaymentErrorEnvelope(response);
                return isTerminalRoomInviteRefusal(envelope) ? "failed" : "ambiguous";
            },
        });
        const supportedNetworks = [
            ...(mainnetSigner ? ["eip155:8453"] : []),
            ...(sepoliaSigner ? ["eip155:84532"] : []),
        ];
        const buyer = createX402BuyerFetch(client, {
            supportedNetworks,
            payerAddresses: [
                ...(mainnetSigner ? [mainnetSigner.address] : []),
                ...(sepoliaSigner ? [sepoliaSigner.address] : []),
            ],
            offerReceipt: stack.offerReceipt,
        });
        return withPayer(trackedFetch, {
            source: options.paymentSigner ? "payment_signer" : resolvedWallet.source,
            rail: "x402",
            payers: [
                ...(mainnetSigner ? [{ address: mainnetSigner.address, network: "eip155:8453" }] : []),
                ...(sepoliaSigner ? [{ address: sepoliaSigner.address, network: "eip155:84532" }] : []),
            ],
        }, refreshBalances, buyer);
    }
    catch (err) {
        // Missing optional peers are a stable local capability state. Other setup
        // failures are thrown so lazy initialization can try again on a later call
        // instead of permanently caching an unwrapped/degraded fetch.
        if (err instanceof PaidStackUnavailable) {
            // Remember WHY the buyer is absent. Without this the eventual 402 is
            // reported as an unfunded wallet, which is the wrong remedy entirely.
            missingPaidStackPackages = err.missingPackages;
            if (!warnedMissingDeps) {
                warnedMissingDeps = true;
                console.warn(`[run402] ${err.message}`);
            }
            return null;
        }
        throw err;
    }
}
function isRetryableBalanceError(error) {
    if (!error || typeof error !== "object")
        return false;
    const candidate = error;
    return (typeof candidate.code === "string" &&
        candidate.code.startsWith("X402_RPC_") &&
        candidate.safeToRetry === true);
}
function isBalancePreflightError(error) {
    if (!error || typeof error !== "object")
        return false;
    const code = error.code;
    return typeof code === "string" &&
        (code.startsWith("X402_RPC_") || code === "X402_INSUFFICIENT_FUNDS");
}
function createLazyPaidFetchFrom(setup) {
    let cached;
    let initializing;
    return async (input, init) => {
        if (cached === undefined) {
            initializing ??= setup()
                .then((initialized) => {
                cached = initialized;
                return initialized;
            })
                .finally(() => {
                initializing = undefined;
            });
            await initializing;
        }
        // `sdkFetch` defers to `globalThis.fetch` whenever a test has replaced
        // it (captured-original comparison), so suites that override the global
        // after the SDK is constructed still see their mocks.
        if (!cached)
            return sdkFetch(input, init);
        try {
            return await cached(input, init);
        }
        catch (err) {
            // A retryable RPC preflight never created a payment payload. Drop only
            // that failed initialization so the next call re-reads provider health.
            if (isRetryableBalanceError(err))
                cached = undefined;
            throw err;
        }
    };
}
/**
 * Returns a fetch that lazily initializes the x402 wrapper on first call.
 * Failed initialization is not cached; concurrent first calls share one
 * attempt, and a later request can recover after transient RPC/setup failure.
 */
export function createLazyPaidFetch(options = {}) {
    let cached;
    let pending;
    let refreshBeforeNextCall = false;
    const initialize = async () => {
        if (cached === undefined) {
            pending ??= setupPaidFetch(options).finally(() => {
                pending = undefined;
            });
            const initialized = await pending;
            // Cache only a successful wrapper. Missing/recoverable local state is
            // retried on the next request instead of pinning an unwrapped fetch for
            // the lifetime of the client.
            if (initialized)
                cached = initialized;
        }
        return cached ?? null;
    };
    const lazy = async (input, init) => {
        await initialize();
        // `sdkFetch` (used below) defers to an overridden `globalThis.fetch`,
        // preserving the test seam.
        if (cached) {
            if (refreshBeforeNextCall) {
                await cached.refreshBalances();
                refreshBeforeNextCall = false;
            }
            try {
                return await cached(input, init);
            }
            catch (err) {
                // Preserve the selected signer/address. Only mutable RPC-derived
                // balance state is refreshed before the caller's next retry.
                if (isBalancePreflightError(err))
                    refreshBeforeNextCall = true;
                throw err;
            }
        }
        return sdkFetch(input, init);
    };
    return Object.assign(lazy, {
        async getPayer() {
            return (await initialize())?.payer ?? null;
        },
        async pay(url, init, payOptions) {
            const configured = await initialize();
            if (payOptions.lightningPairing) {
                // A bring-your-own pairing pays the `lnbtc` entry on its node's
                // network; a 402 without one falls to the configured buyer, if any.
                const executor = createLightningPayExecutor({
                    pairingUri: payOptions.lightningPairing,
                    baseFetch: sdkFetch,
                    fallback: async () => null,
                    walletNetwork: "probe",
                });
                const result = await executor(url, init, payOptions);
                if (result.payment || result.response.status !== 402 || !configured?.pay)
                    return result;
                await result.response.body?.cancel().catch(() => undefined);
                return configured.pay(url, init, payOptions);
            }
            if (configured?.pay)
                return configured.pay(url, init, payOptions);
            const response = await sdkFetch(url, init);
            if (response.status !== 402) {
                return {
                    response,
                    payment: null,
                    outcome: "not_required",
                    replay: await responseSignalsReplay(response, payOptions.idempotencyKey !== undefined),
                    ...payResponseMetadata(response),
                };
            }
            if (configured?.payer.rail === "mpp" || configured?.payer.rail === "lightning") {
                const offers = paymentOffers(response);
                throw paymentNetworkUnsupportedError(offerNetworks(offers), configured.payer.payers.flatMap((payer) => payer.network ? [payer.network] : []), {}, offers, configured.payer.rail);
            }
            if (missingPaidStackPackages !== null) {
                throw paidStackUnavailableError(missingPaidStackPackages, {
                    challenge_networks: challengeNetworks(response),
                });
            }
            throw walletUnavailableError({ challenge_networks: challengeNetworks(response) });
        },
    });
}
function validatePaymentSource(options) {
    if (options.paymentSigner && options.walletPath) {
        throw new LocalError("Configure exactly one explicit payment source: paymentSigner or walletPath", "configuring paid fetch", {
            code: "PAYMENT_SOURCE_CONFLICT",
            details: { fields: ["paymentSigner", "walletPath"] },
        });
    }
}
async function resolveWallet(options) {
    try {
        // An explicit payment path is authoritative even when auth uses a custom
        // credentials provider. Do not fall back if it is absent or malformed.
        if (options.walletPath !== undefined) {
            const localWallet = readWallet(options.walletPath);
            return localWallet ? { localWallet, source: "wallet_path" } : null;
        }
        // Once a credentials provider is supplied it is the only implicit payment
        // source. Providers without wallet capability fail closed; they never
        // inherit the process-global wallet.
        if (options.credentials !== undefined) {
            if (typeof options.credentials.readWallet !== "function")
                return null;
            const localWallet = await options.credentials.readWallet();
            return localWallet ? { localWallet, source: "credentials" } : null;
        }
        // Direct setupPaidFetch()/createLazyPaidFetch() calls retain the Node
        // default for backwards compatibility.
        const localWallet = readWallet();
        return localWallet ? { localWallet, source: "default_wallet" } : null;
    }
    catch {
        return null;
    }
}
function withPayer(fetchFn, payer, refreshBalances = async () => { }, pay) {
    return Object.assign(fetchFn, { payer, refreshBalances, ...(pay ? { pay } : {}) });
}
async function localWalletSigners(stack, localWallet, mainnetClient, sepoliaClient) {
    // A key-backed wallet or a signer-backed (KMS) one: the same x402 signer either way.
    const account = await resolveWalletAccount(localWallet, { privateKeyToAccount: stack.privateKeyToAccount });
    return [
        stack.toClientEvmSigner(account, mainnetClient),
        stack.toClientEvmSigner(account, sepoliaClient),
    ];
}
/** @internal Source-level unit-test seam; not re-exported by the package. */
export const __paidFetchInternals = {
    createLazyPaidFetchFrom,
};
/**
 * Receipt-bearing buyer orchestration over an already-configured x402 client.
 * Signed proofs live only in this SDK instance's memory. An ambiguous retry of
 * the identical request re-presents that proof; it never mints a replacement.
 */
export function createX402BuyerFetch(client, options) {
    const calls = new AsyncLocalStorage();
    const proofs = new Map();
    const creatingProofs = new Map();
    const tracked = createTrackedX402Fetch((baseFetch) => async (input, init) => {
        const call = calls.getStore();
        if (!call) {
            throw settlementError("The x402 buyer lost its request context before dispatch.", false);
        }
        const request = new Request(input, init);
        const cached = proofs.get(call.fingerprint);
        if (cached) {
            call.proof = cached;
            call.replayedProof = true;
            return baseFetch(withProof(request, cached.headers));
        }
        const retryRequest = request.clone();
        const firstResponse = await baseFetch(request);
        if (firstResponse.status !== 402)
            return firstResponse;
        const offers = paymentOffers(firstResponse);
        if (!firstResponse.headers.get("PAYMENT-REQUIRED") && offers.length > 0) {
            // An MPP-only 402: name what it offers and the rail that pays it.
            throw paymentNetworkUnsupportedError(offerNetworks(offers), [...options.supportedNetworks], {}, offers, "x402");
        }
        const required = await decodePaymentRequired(firstResponse);
        const guarded = await guardPaymentRequired(required, options.supportedNetworks, call.maxUsdMicros, request.url, call.requireReceipt, options.offerReceipt, options.verifyAtSeconds?.() ?? Math.floor(Date.now() / 1000), offers);
        let pending = creatingProofs.get(call.fingerprint);
        if (!pending) {
            pending = createProof(client, guarded)
                .finally(() => creatingProofs.delete(call.fingerprint));
            creatingProofs.set(call.fingerprint, pending);
        }
        const proof = await pending;
        proofs.set(call.fingerprint, proof);
        call.proof = proof;
        return baseFetch(withProof(retryRequest, proof.headers));
    }, client, {
        store: options.store,
        createAttemptId: options.createAttemptId,
        now: options.now,
        fetch: options.fetch,
        async classifyPaymentResponse(response) {
            const call = calls.getStore();
            const envelope = await readPaymentErrorEnvelope(response);
            if (call && isTrustedRun402PendingResponse({
                requestUrl: call.requestUrl,
                response,
                envelope,
                paymentBearing: true,
                redirectsDisabled: true,
            })) {
                return "intent_pending";
            }
            if (call && envelope && trustedRun402ResponseOrigin(call.requestUrl, response) &&
                gatewayPaymentBuyerError(response, envelope)) {
                return "failed";
            }
            // A seller may settle the payment and then deliberately answer a
            // well-formed non-2xx (the paid action was refused on its merits). A
            // valid standard settlement receipt for the accepted challenge proves
            // the funds moved, so this is a completed payment whose delivery
            // failed, not an ambiguous outcome (GH-594).
            if (call?.proof && hasValidSettlementReceipt(response, call.proof.accepted)) {
                call.settledWithFailedDelivery = true;
                return "completed";
            }
            const failure = await upstreamFailure(response);
            if (call?.replayedProof && isAlreadyUsedFailure(failure)) {
                call.alreadySettled = true;
                return "already_settled";
            }
            return isProvenNoSettlementFailure(failure) ? "failed" : "ambiguous";
        },
    });
    return async (url, init, payOptions) => {
        const maxUsdMicros = payOptions.maxUsdMicros ?? DEFAULT_PAYMENT_MAX_USD_MICROS;
        const requireReceipt = payOptions.requireReceipt === true;
        const fingerprint = await paymentRequestFingerprint(url, init, maxUsdMicros, requireReceipt);
        const call = {
            fingerprint,
            requestUrl: url,
            maxUsdMicros,
            replayedProof: false,
            alreadySettled: false,
            requireReceipt,
        };
        try {
            const response = await calls.run(call, () => tracked(url, init));
            if (!call.proof) {
                return {
                    response,
                    payment: null,
                    outcome: "not_required",
                    replay: await responseSignalsReplay(response, payOptions.idempotencyKey !== undefined),
                    ...payResponseMetadata(response),
                };
            }
            if (call.alreadySettled) {
                proofs.delete(fingerprint);
                return {
                    response,
                    payment: null,
                    outcome: "already_settled",
                    replay: true,
                    ...payResponseMetadata(response),
                };
            }
            if (!response.ok && !call.settledWithFailedDelivery) {
                const envelope = await readPaymentErrorEnvelope(response);
                if (envelope && trustedRun402ResponseOrigin(url, response)) {
                    const gatewayError = gatewayPaymentBuyerError(response, envelope);
                    if (gatewayError)
                        throw gatewayError;
                }
                const failure = await upstreamFailure(response);
                proofs.delete(fingerprint);
                if (failure.code === "payment_insufficient_funds") {
                    throw walletUnavailableError({
                        upstream_code: failure.code,
                        ...(failure.details ?? {}),
                    });
                }
                throw settlementError("The x402 payment proof was rejected before settlement.", false, {
                    upstream_code: failure.code,
                    ...(failure.x402Error ? { x402_error: failure.x402Error } : {}),
                });
            }
            const payment = await receiptFromResponse(response, call.proof, url, requireReceipt, options.offerReceipt, options.payerAddresses ?? []);
            proofs.delete(fingerprint);
            const result = {
                response,
                payment,
                outcome: "settled",
                replay: call.replayedProof,
                nextActions: [],
                ...payResponseMetadata(response),
            };
            if (requireReceipt &&
                payment.merchantReceipt.status !== "verified") {
                const recovery = receiptRecovery(url, response, payOptions.idempotencyKey);
                throw new PaymentPolicyError({
                    response,
                    result,
                    message: "The payment settled, but the promised merchant receipt could not be verified.",
                    fundsMoved: payment.fundsMoved,
                    mutationState: payment.delivery.status === "fulfilled" ? "committed" : "unknown",
                    safeToRetry: recovery.safeToRetry,
                    nextActions: recovery.nextActions,
                });
            }
            return result;
        }
        catch (cause) {
            if (cause instanceof PaymentBuyerError)
                throw cause;
            if (cause instanceof X402BalanceError) {
                if (cause.code === "X402_INSUFFICIENT_FUNDS") {
                    throw walletUnavailableError(asRecord(cause.details));
                }
                throw settlementError("The buyer could not confirm wallet funds before signing; no funds moved.", false, { upstream_code: cause.code, ...(asRecord(cause.details)) }, cause, true);
            }
            if (cause instanceof PaymentAttemptError) {
                const fundsMoved = cause.mutationState === "ambiguous" ? "unknown" : false;
                throw settlementError(fundsMoved === "unknown"
                    ? "The payment-bearing request lost its response; settlement may have occurred. The same SDK instance retains the original proof for an identical retry."
                    : "The payment attempt failed before funds could move.", fundsMoved, {
                    payment_attempt_id: cause.paymentAttemptId,
                    phase: cause.phase,
                    provider_started: cause.providerStarted,
                    upstream_code: cause.code,
                    // The status the buyer actually saw. Omitting it forced a live
                    // repro with a patched SDK just to learn the response was a 402.
                    ...(cause.responseStatus !== undefined && cause.responseStatus !== null
                        ? { response_status: cause.responseStatus }
                        : {}),
                }, cause, cause.safeToRetry, cause.nextActions);
            }
            throw settlementError("The x402 payment authorization could not be created; no funds moved.", false, {}, cause);
        }
    };
}
async function createProof(client, guarded) {
    try {
        const payload = await client.createPaymentPayload(guarded.required);
        return {
            accepted: payload.accepted,
            offer: guarded.verifiedOffers.find((offer) => sameRequirement(offer.requirement, payload.accepted)) ?? null,
            headers: {
                [payload.x402Version === 1 ? "X-PAYMENT" : "PAYMENT-SIGNATURE"]: encodeBase64Json(payload),
            },
        };
    }
    catch (cause) {
        throw unwrapPolicyError(cause) ?? cause;
    }
}
async function guardPaymentRequired(required, supportedNetworks, maxUsdMicros, requestUrl, requireReceipt, offerReceipt, verifyAtSeconds, offers = []) {
    let challengeUrl;
    try {
        challengeUrl = new URL(required.resource.url).toString();
    }
    catch (cause) {
        throw settlementError("The x402 challenge resource URL is invalid.", false, { challenge_url: required.resource.url, request_url: requestUrl }, cause);
    }
    if (challengeUrl !== new URL(requestUrl).toString()) {
        throw settlementError("The x402 challenge is bound to a different resource URL.", false, { challenge_url: challengeUrl, request_url: requestUrl });
    }
    const supportedByNetwork = required.accepts.filter((accept) => accept.scheme === "exact" && supportedNetworks.includes(accept.network));
    if (supportedByNetwork.length === 0) {
        throw paymentNetworkUnsupportedError([...new Set([...required.accepts.map((accept) => accept.network), ...offerNetworks(offers)])], [...supportedNetworks], {}, offers, "x402");
    }
    const supported = supportedByNetwork.filter((accept) => accept.asset.toLowerCase() === supportedPaymentAsset(accept.network)?.toLowerCase());
    if (supported.length === 0) {
        throw paymentNetworkUnsupportedError([...new Set(required.accepts.map((accept) => accept.network))], [...supportedNetworks], {
            challenge_assets: [...new Set(supportedByNetwork.map((accept) => accept.asset))],
            wallet_assets: [...new Set(supportedNetworks.flatMap((network) => {
                    const asset = supportedPaymentAsset(network);
                    return asset ? [asset] : [];
                }))],
        });
    }
    const valid = supported.map((accept) => ({ accept, amount: atomicAmount(accept.amount) }));
    if (valid.some(({ amount }) => amount === null)) {
        throw settlementError("The x402 challenge contains an invalid atomic amount.", false, { challenge_amounts: supported.map((accept) => accept.amount) });
    }
    const withinLimit = valid.filter((entry) => entry.amount !== null && entry.amount <= maxUsdMicros);
    if (withinLimit.length === 0) {
        const challenged = Math.min(...valid.map(({ amount }) => amount));
        throw paymentExceedsMaxError(challenged, maxUsdMicros);
    }
    const accepted = withinLimit.map(({ accept }) => accept);
    const verifiedOffers = offerReceipt
        ? await verifyDirectOffers(required, accepted, requestUrl, verifyAtSeconds, offerReceipt)
        : [];
    if (requireReceipt && verifiedOffers.length === 0) {
        throw merchantReceiptRequiredError({
            offered_requirements: accepted.length,
            valid_wallet_rooted_offers: 0,
        });
    }
    const eligible = requireReceipt
        ? accepted.filter((requirement) => verifiedOffers.some((offer) => sameRequirement(offer.requirement, requirement)))
        : accepted;
    return {
        required: { ...required, accepts: eligible },
        verifiedOffers,
    };
}
function supportedPaymentAsset(network) {
    if (network === "eip155:8453")
        return USDC_MAINNET;
    if (network === "eip155:84532")
        return USDC_SEPOLIA;
    return null;
}
async function verifyDirectOffers(required, eligibleRequirements, requestUrl, verifyAtSeconds, runtime) {
    let decoded;
    try {
        decoded = runtime.decodeSignedOffers(runtime.extractOffersFromPaymentRequired(required));
    }
    catch {
        return [];
    }
    const verified = [];
    for (const value of decoded) {
        const offer = asRecord(value);
        const signed = offer.signedOffer;
        if (!runtime.isEIP712SignedOffer(signed))
            continue;
        const requirement = runtime.findAcceptsObjectFromSignedOffer(offer, eligibleRequirements);
        if (!requirement)
            continue;
        try {
            const checked = await runtime.verifyOfferSignatureEIP712(signed);
            const payload = checked.payload;
            const validUntil = safeInteger(payload.validUntil);
            if (payload.version !== 1 ||
                normalizedUrl(payload.resourceUrl) !== normalizedUrl(requestUrl) ||
                payload.scheme !== requirement.scheme ||
                payload.network !== requirement.network ||
                !sameAddressOrText(payload.asset, requirement.asset) ||
                payload.amount !== requirement.amount ||
                !sameAddressOrText(payload.payTo, requirement.payTo) ||
                validUntil === null ||
                validUntil <= verifyAtSeconds ||
                !sameAddressOrText(checked.signer, requirement.payTo)) {
                continue;
            }
            verified.push({
                signed,
                decoded: offer,
                requirement,
                signer: checked.signer,
                relationship: "direct",
            });
        }
        catch {
            // A malformed, invalid, delegated, or untrusted offer is ineligible.
        }
    }
    return verified;
}
function sameRequirement(left, right) {
    return (left.scheme === right.scheme &&
        left.network === right.network &&
        sameAddressOrText(left.asset, right.asset) &&
        left.amount === right.amount &&
        sameAddressOrText(left.payTo, right.payTo));
}
function sameAddressOrText(left, right) {
    if (typeof left !== "string" || typeof right !== "string")
        return false;
    return /^0x[0-9a-fA-F]{40}$/.test(left) &&
        /^0x[0-9a-fA-F]{40}$/.test(right)
        ? left.toLowerCase() === right.toLowerCase()
        : left === right;
}
function normalizedUrl(value) {
    if (typeof value !== "string")
        return null;
    try {
        return new URL(value).toString();
    }
    catch {
        return null;
    }
}
function safeInteger(value) {
    return typeof value === "number" && Number.isSafeInteger(value)
        ? value
        : null;
}
function atomicAmount(value) {
    if (!/^\d+$/.test(value))
        return null;
    const amount = Number(value);
    return Number.isSafeInteger(amount) ? amount : null;
}
function withProof(request, proofHeaders) {
    const headers = new Headers(request.headers);
    for (const [name, value] of Object.entries(proofHeaders))
        headers.set(name, value);
    return new Request(request, { headers });
}
async function decodePaymentRequired(response) {
    const header = response.headers.get("PAYMENT-REQUIRED");
    if (!header) {
        throw settlementError("The 402 response did not include a PAYMENT-REQUIRED challenge header.", false);
    }
    let decoded;
    try {
        decoded = decodeBase64Json(header);
    }
    catch (cause) {
        throw settlementError("The PAYMENT-REQUIRED challenge header is invalid.", false, {}, cause);
    }
    if (!isPaymentRequired(decoded)) {
        throw settlementError("The PAYMENT-REQUIRED challenge has an invalid shape.", false);
    }
    return decoded;
}
function isPaymentRequired(value) {
    if (!value || typeof value !== "object" || Array.isArray(value))
        return false;
    const candidate = value;
    return Number.isInteger(candidate.x402Version) &&
        Boolean(candidate.resource && typeof candidate.resource.url === "string") &&
        Array.isArray(candidate.accepts) &&
        candidate.accepts.every((accept) => Boolean(accept &&
            typeof accept.scheme === "string" &&
            typeof accept.network === "string" &&
            typeof accept.asset === "string" &&
            typeof accept.amount === "string" &&
            typeof accept.payTo === "string"));
}
/**
 * True when the response carries a standard x402 settlement receipt that
 * proves a successful on-chain settlement of the accepted challenge: the same
 * structural checks `receiptFromResponse` enforces before reporting a receipt.
 */
function hasValidSettlementReceipt(response, accepted) {
    const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
    if (!header)
        return false;
    let record;
    try {
        record = asRecord(decodeBase64Json(header));
    }
    catch {
        return false;
    }
    return (record.success === true &&
        typeof record.transaction === "string" &&
        record.transaction !== "" &&
        record.network === accepted.network &&
        atomicAmount(accepted.amount) !== null);
}
async function receiptFromResponse(response, proof, url, requireReceipt, runtime, payerAddresses) {
    const accepted = proof.accepted;
    const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
    if (!header) {
        throw settlementError("The paid response omitted the standard PAYMENT-RESPONSE settlement receipt, so settlement cannot be reported faithfully.", "unknown", { response_status: response.status });
    }
    let settlement;
    try {
        settlement = decodeBase64Json(header);
    }
    catch (cause) {
        throw settlementError("The PAYMENT-RESPONSE settlement receipt is invalid.", "unknown", { response_status: response.status }, cause);
    }
    const record = asRecord(settlement);
    if (record.success !== true || typeof record.transaction !== "string" || record.transaction === "") {
        throw settlementError("The payment response does not prove a successful on-chain settlement.", record.success === false ? false : "unknown", { response_status: response.status });
    }
    if (record.network !== accepted.network) {
        throw settlementError("The payment response network does not match the accepted x402 challenge.", "unknown", {
            accepted_network: accepted.network,
            receipt_network: typeof record.network === "string" ? record.network : null,
            response_status: response.status,
        });
    }
    const amount = atomicAmount(accepted.amount);
    if (amount === null) {
        throw settlementError("The selected payment amount cannot be represented as usd_micros.", "unknown");
    }
    const metadata = payResponseMetadata(response);
    const settlementPayer = typeof record.payer === "string" && record.payer.length > 0
        ? record.payer
        : null;
    let receiptStatus = proof.offer
        ? "unavailable"
        : "absent";
    let signedReceipt = null;
    let receiptIssuedAt = null;
    let receiptSigner = null;
    if (runtime) {
        try {
            signedReceipt = runtime.extractReceiptFromResponse(response) ?? null;
            if (signedReceipt && !proof.offer) {
                receiptStatus = "untrusted";
            }
            else if (signedReceipt && !runtime.isEIP712SignedReceipt(signedReceipt)) {
                receiptStatus = "untrusted";
            }
            else if (signedReceipt && proof.offer) {
                const checked = await runtime.verifyReceiptSignatureEIP712(signedReceipt);
                const payload = checked.payload;
                const issuedAt = safeInteger(payload.issuedAt);
                const payerMatches = typeof payload.payer === "string" &&
                    [
                        ...(settlementPayer ? [settlementPayer] : []),
                        ...payerAddresses,
                    ].some((payer) => sameAddressOrText(payload.payer, payer));
                const transactionMatches = typeof payload.transaction === "string" &&
                    payload.transaction === record.transaction;
                if (payload.version === 1 &&
                    normalizedUrl(payload.resourceUrl) === normalizedUrl(url) &&
                    payload.network === accepted.network &&
                    payerMatches &&
                    transactionMatches &&
                    issuedAt !== null &&
                    sameAddressOrText(checked.signer, proof.offer.signer) &&
                    sameAddressOrText(checked.signer, accepted.payTo)) {
                    receiptStatus = "verified";
                    receiptSigner = checked.signer;
                    receiptIssuedAt = new Date(issuedAt * 1000).toISOString();
                }
                else {
                    receiptStatus = "invalid";
                }
            }
        }
        catch {
            receiptStatus = signedReceipt ? "invalid" : receiptStatus;
        }
    }
    const fundsMoved = metadata.fundsMoved ?? true;
    const deliveryReplay = metadata.delivery === "replay" || metadata.deduplicated === true;
    const offerPayload = proof.offer?.decoded ?? null;
    const offerValidUntil = safeInteger(offerPayload?.validUntil);
    const noSignerAuthorizationExpiry = null;
    return {
        network: record.network,
        paymentId: metadata.paymentId,
        amountUsdMicros: amount,
        asset: accepted.asset,
        payer: settlementPayer,
        protocol: "x402",
        method: "exact",
        payTo: accepted.payTo,
        transaction: record.transaction,
        resourceUrl: url,
        settlement: { status: "verified" },
        fundsMoved,
        deduplicated: metadata.deduplicated ?? false,
        delivery: {
            status: response.ok ? "fulfilled" : "failed",
            replay: deliveryReplay,
        },
        offer: {
            status: proof.offer ? "verified" : "absent",
            resourceUrl: typeof offerPayload?.resourceUrl === "string"
                ? offerPayload.resourceUrl
                : null,
            validUntil: offerValidUntil === null
                ? null
                : new Date(offerValidUntil * 1000).toISOString(),
        },
        merchantReceipt: {
            status: receiptStatus,
            claim: receiptStatus === "verified" ? "service_delivered" : null,
            issuedAt: receiptIssuedAt,
        },
        signerRelationship: {
            kind: receiptStatus === "verified"
                ? proof.offer?.relationship ?? null
                : proof.offer
                    ? "unverified"
                    : null,
            merchantRoot: proof.offer?.relationship === "direct" ? accepted.payTo : null,
            signer: receiptSigner ?? proof.offer?.signer ?? null,
            authorizationExpiresAt: noSignerAuthorizationExpiry,
        },
        policy: {
            requireReceipt,
            status: requireReceipt
                ? receiptStatus === "verified"
                    ? "satisfied"
                    : "unsatisfied"
                : "not_required",
        },
        evidence: {
            offer: proof.offer?.signed ?? null,
            merchantReceipt: signedReceipt,
            signerAuthorization: null,
        },
    };
}
function receiptRecovery(url, response, idempotencyKey) {
    const recoverable = Boolean(idempotencyKey) &&
        isTrustedRun402PaymentUrl(url, { allowTestLocalhost: true }) &&
        response.headers.get("x-run402-merchant-evidence-state") ===
            "unavailable";
    if (recoverable) {
        return {
            safeToRetry: true,
            nextActions: [
                {
                    type: "retry",
                    request: "repeat_identical",
                    reusePayer: true,
                    reuseIdempotencyKey: true,
                    why: "Repeat the identical request with the same payer and idempotency key to recover the original receipt without another settlement.",
                },
            ],
        };
    }
    return {
        safeToRetry: false,
        nextActions: [
            {
                type: "reconcile_payment",
                why: "Reconcile the existing settlement with the merchant; do not authorize a second payment.",
            },
        ],
    };
}
async function upstreamFailure(response) {
    const challengeError = paymentRequiredError(response);
    if (!response.headers.get("content-type")?.includes("application/json")) {
        return { code: null, x402Error: challengeError, details: null };
    }
    try {
        const body = asRecord(await response.clone().json());
        const details = asRecord(body.details);
        return {
            code: typeof body.code === "string" ? body.code : null,
            x402Error: typeof details.x402_error === "string" ? details.x402_error : challengeError,
            details: Object.keys(details).length > 0 ? details : null,
        };
    }
    catch {
        return { code: null, x402Error: challengeError, details: null };
    }
}
function paymentRequiredError(response) {
    const header = response.headers.get("PAYMENT-REQUIRED");
    if (!header)
        return null;
    try {
        const decoded = asRecord(decodeBase64Json(header));
        return typeof decoded.error === "string" ? decoded.error : null;
    }
    catch {
        return null;
    }
}
function isAlreadyUsedFailure(failure) {
    if (failure.code !== "TENANT_X402_PAYMENT_INVALID")
        return false;
    return /already.{0,20}used|used.{0,20}(authorization|nonce)|nonce.{0,20}used/i.test(failure.x402Error ?? "");
}
function isProvenNoSettlementFailure(failure) {
    if (failure.code === "payment_insufficient_funds")
        return true;
    if (failure.code === "TENANT_X402_SETTLEMENT_FAILED")
        return true;
    // A bare `PAYMENT_REQUIRED` on the PAID retry is the seller re-issuing its
    // 402 challenge: it did not accept the payment, so it did not settle.
    // Without this the outcome fell through to `ambiguous`, telling callers their
    // funds might have moved when they provably had not — and because correct
    // policy on ambiguity is stop-and-reconcile with safe_to_retry false, an
    // unattended agent is stranded permanently on a payment that never
    // happened.
    //
    // Distinct from the replay case: a consumed authorization reports
    // TENANT_X402_PAYMENT_INVALID with "nonce already used", which stays
    // ambiguous via isAlreadyUsedFailure below — there the proof WAS spent.
    if (failure.code === "PAYMENT_REQUIRED")
        return true;
    return failure.code === "TENANT_X402_PAYMENT_INVALID" && !isAlreadyUsedFailure(failure);
}
export function paymentNetworkUnsupportedError(challengeNetworkValues, walletNetworkValues, extra = {}, offers = [], configuredRail) {
    const switchTo = switchRailFor(offers, configuredRail);
    return new PaymentBuyerError({
        code: "PAYMENT_NETWORK_UNSUPPORTED",
        message: switchTo
            ? `The 402 offers no payment this wallet's ${configuredRail ?? "configured"} rail can make; the ${switchTo} rail can pay it.`
            : "The x402 challenge does not offer an exact-scheme network and asset supported by this wallet.",
        fundsMoved: false,
        details: {
            challenge_networks: challengeNetworkValues,
            wallet_networks: walletNetworkValues,
            ...(offers.length > 0 ? { offers } : {}),
            ...(configuredRail ? { configured_rail: configuredRail } : {}),
            ...extra,
        },
        nextActions: [
            ...(switchTo
                ? [{
                        type: "run_command",
                        command: `run402 init ${switchTo} --switch-rail`,
                        why: `Switch this wallet to the ${switchTo} rail, which pays the seller's first offer, then retry.`,
                    }]
                : []),
            {
                type: "edit_request",
                why: "Use a priced endpoint on one of the wallet networks, or configure a signer for a challenge network.",
            },
        ],
    });
}
function settlementError(message, fundsMoved, details = {}, cause, retryable = false, nextActions) {
    return new PaymentBuyerError({
        code: "PAYMENT_SETTLEMENT_FAILED",
        message,
        fundsMoved,
        details,
        retryable,
        safeToRetry: fundsMoved === false,
        cause,
        nextActions: nextActions && nextActions.length > 0
            ? nextActions
            : fundsMoved === "unknown"
                ? [
                    {
                        type: "reconcile_payment",
                        safe_to_auto_execute: false,
                        why: "Confirm the original settlement before authorizing a different payment proof.",
                    },
                ]
                : [
                    {
                        type: "retry",
                        why: "No funds moved; retry after correcting the reported pre-settlement failure.",
                    },
                ],
    });
}
/** Every network a 402 offers, x402 entries and MPP challenges alike. */
function challengeNetworks(response) {
    return offerNetworks(paymentOffers(response));
}
function offerNetworks(offers) {
    return [...new Set(offers.flatMap((offer) => offer.network ? [offer.network] : []))];
}
async function paymentRequestFingerprint(url, init, maxUsdMicros, requireReceipt) {
    let request;
    try {
        request = new Request(url, init);
    }
    catch (cause) {
        throw settlementError("The paid request could not be constructed.", false, {}, cause);
    }
    const headerEntries = [];
    request.headers.forEach((value, name) => headerEntries.push([name, value]));
    const headers = headerEntries
        .filter(([name]) => ![
        "payment-signature",
        "x-payment",
        "x-run402-payment-attempt-id",
    ].includes(name.toLowerCase()))
        .sort(([a], [b]) => a.localeCompare(b));
    let bodyHash = "";
    try {
        const body = new Uint8Array(await request.clone().arrayBuffer());
        bodyHash = createHash("sha256").update(body).digest("hex");
    }
    catch (cause) {
        throw settlementError("The paid request body cannot be replayed safely.", false, {}, cause);
    }
    return createHash("sha256")
        .update(JSON.stringify({
        method: request.method,
        url: request.url,
        headers,
        bodyHash,
        maxUsdMicros,
        requireReceipt,
    }))
        .digest("hex");
}
/**
 * Encode an x402 wire payload — STANDARD base64, never base64url.
 *
 * The x402 `PAYMENT-SIGNATURE` / `X-PAYMENT` header is standard base64. This
 * encoded base64url, which substitutes `-` for `+` and `_` for `/` and drops
 * the `=` padding, so any payload whose bytes produced a `+` or `/` reached the
 * seller as a string its decoder could not read. The seller then saw NO payment
 * at all and re-issued its original 402 challenge — indistinguishable, from the
 * outside, from "the payment was rejected", which is why this survived so long:
 * there is no verification error anywhere to find, on either side.
 *
 * A real proof is ~2.4 KB, so it essentially always contains a `+` or `/`.
 * Verified against our own seller: the SAME signed authorization, re-encoded as
 * standard base64 and presented once, settles 200 with the paid response.
 *
 * `decodeBase64Json` deliberately stays on `base64url`: Node's base64url decoder
 * accepts BOTH alphabets, so it reads standard-base64 seller headers correctly
 * and is the more forgiving choice for input we do not control.
 */
function encodeBase64Json(value) {
    return Buffer.from(JSON.stringify(value)).toString("base64");
}
function decodeBase64Json(value) {
    return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
}
function asRecord(value) {
    return value && typeof value === "object" && !Array.isArray(value)
        ? value
        : {};
}
/**
 * Request-scoped payment tracker around `@x402/fetch`.
 *
 * The wrapped package may call its base fetch twice: first without payment to
 * obtain the 402 challenge, then with a signed payment authorization. The
 * AsyncLocalStorage context lets the base-fetch boundary update the correct
 * durable attempt even when multiple paid requests run concurrently.
 *
 * Exported from this module for deterministic boundary tests, but intentionally
 * not re-exported from the package entry point.
 */
export function createTrackedX402Fetch(wrapFetchWithPayment, client, opts = {}) {
    const storage = new AsyncLocalStorage();
    const store = opts.store ?? createFilePaymentAttemptStore();
    const makeId = opts.createAttemptId ?? createPaymentAttemptId;
    const now = opts.now ?? (() => new Date().toISOString());
    const baseFetch = async (input, init) => {
        const context = storage.getStore();
        if (!context)
            return (opts.fetch ?? sdkFetch)(input, init);
        const paymentBearing = hasPaymentAuthorization(input, init);
        if (paymentBearing) {
            ensureIntent(context, store, now);
            context.phase = "payment_submission";
            const startedAt = now();
            writeRecord(context, store, {
                state: "submitting",
                mutation_state: "in_progress",
                provider_started_at: startedAt,
            });
        }
        const [nextInput, nextInit] = withPaymentAttemptHeader(input, init, context.id, paymentBearing);
        if (paymentBearing) {
            // This assignment is deliberately immediately before the external call.
            // From this line onward a thrown transport error has an unknown outcome.
            context.providerStarted = true;
        }
        let response;
        try {
            response = await (opts.fetch ?? sdkFetch)(nextInput, nextInit);
        }
        catch (cause) {
            context.transportFailure = cause;
            throw cause;
        }
        if (!paymentBearing && response.status === 402) {
            context.phase = "challenge_received";
            context.challengeBody = await response.clone().json().then((body) => (body && typeof body === "object" && !Array.isArray(body) ? body : null), () => null);
            ensureIntent(context, store, now);
        }
        else if (paymentBearing) {
            context.phase = "payment_response";
            context.responseStatus = response.status;
            writeRecord(context, store, {
                state: "response_received",
                mutation_state: "in_progress",
                response_status: response.status,
            });
        }
        return response;
    };
    const paidFetch = wrapFetchWithPayment(baseFetch, client);
    return async (input, init) => {
        const suppliedAttemptId = attemptIdFromRequest(input, init);
        const request = requestSummary(input, init);
        if (suppliedAttemptId) {
            let existing;
            try {
                existing = store.read(suppliedAttemptId);
            }
            catch (cause) {
                const code = cause instanceof Run402Error && cause.code
                    ? cause.code
                    : "X402_ATTEMPT_JOURNAL_FAILED";
                throw new PaymentAttemptError({
                    code,
                    message: "The existing x402 payment attempt could not be inspected; no payment was dispatched.",
                    phase: "initial_request",
                    paymentAttemptId: suppliedAttemptId,
                    providerStarted: false,
                    mutationState: "not_started",
                    safeToRetry: true,
                    retryable: false,
                    nextActions: [
                        {
                            type: "contact_support",
                            payment_attempt_id: suppliedAttemptId,
                            safe_to_auto_execute: false,
                            why: "Preserve and repair the unreadable local payment-attempt record before creating a fresh attempt.",
                        },
                    ],
                    cause,
                    request,
                });
            }
            if (existing) {
                throw new PaymentAttemptError({
                    code: "X402_ATTEMPT_ID_ALREADY_EXISTS",
                    message: "This x402 payment attempt id already exists; reconcile it before authorizing another payment.",
                    phase: "payment_response",
                    paymentAttemptId: suppliedAttemptId,
                    providerStarted: existing.mutation_state !== "not_started",
                    responseStatus: existing.response_status ?? null,
                    mutationState: existing.mutation_state,
                    safeToRetry: false,
                    cause: null,
                    request,
                });
            }
        }
        const context = {
            id: suppliedAttemptId ?? makeId(),
            request,
            createdAt: now(),
            phase: "initial_request",
            providerStarted: false,
            responseStatus: null,
            record: null,
            journalFailure: false,
            now,
        };
        if (suppliedAttemptId) {
            let claimed;
            try {
                claimed = claimIntent(context, store, now);
            }
            catch (cause) {
                throw new PaymentAttemptError({
                    code: "X402_ATTEMPT_JOURNAL_FAILED",
                    message: "The x402 payment attempt id could not be reserved durably; no request was dispatched.",
                    phase: "initial_request",
                    paymentAttemptId: suppliedAttemptId,
                    providerStarted: false,
                    mutationState: "not_started",
                    safeToRetry: true,
                    cause: cause instanceof AttemptJournalWriteError ? cause.cause : cause,
                    request,
                });
            }
            if (!claimed) {
                throw new PaymentAttemptError({
                    code: "X402_ATTEMPT_ID_ALREADY_EXISTS",
                    message: "This x402 payment attempt id was claimed concurrently; reconcile it before authorizing another payment.",
                    phase: "initial_request",
                    paymentAttemptId: suppliedAttemptId,
                    providerStarted: true,
                    mutationState: "ambiguous",
                    safeToRetry: false,
                    cause: null,
                    request,
                });
            }
        }
        return storage.run(context, async () => {
            try {
                const response = await paidFetch(input, init);
                if (context.providerStarted) {
                    // A 402 on the PAID retry is a clean rejection, not an ambiguity: the
                    // seller is re-issuing its payment challenge, which means it did not
                    // accept the payment and therefore did not settle. Reporting that as
                    // `ambiguous` tells an agent its funds may have moved when they
                    // provably have not, and correct agent policy on ambiguity is to stop
                    // and reconcile — so the caller halts on a payment that never
                    // happened. Verified against a live seller: the paid request is
                    // re-challenged 402 with the original `PAYMENT_REQUIRED` body and the
                    // payer's on-chain balance is unchanged.
                    const classification = response.ok
                        ? "completed"
                        : await opts.classifyPaymentResponse?.(response) ?? "ambiguous";
                    const completed = classification === "completed" || classification === "already_settled";
                    const failed = classification === "failed";
                    const intentPending = classification === "intent_pending";
                    const retryAfter = retryAfterSeconds(response);
                    writeRecordBestEffort(context, store, {
                        state: completed ? "completed" : failed ? "failed" : intentPending ? "intent_pending" : "ambiguous",
                        mutation_state: completed ? "completed" : failed ? "not_started" : "ambiguous",
                        response_status: response.status,
                        ...(intentPending ? {
                            last_error_code: "PAYMENT_INTENT_PENDING",
                            ...(nonEmptyHeader(response, "x-run402-payment-id")
                                ? { payment_id: nonEmptyHeader(response, "x-run402-payment-id") }
                                : {}),
                            intent_state: "pending",
                            ...(retryAfter !== null ? { retry_after_seconds: retryAfter } : {}),
                        } : {}),
                    });
                    if (intentPending)
                        return response;
                    if (!completed && !failed) {
                        throw new PaymentAttemptError({
                            code: "X402_PAYMENT_OUTCOME_AMBIGUOUS",
                            message: "The x402 payment target returned a non-success response after provider dispatch; reconcile the attempt before paying again.",
                            phase: "payment_response",
                            paymentAttemptId: context.id,
                            providerStarted: true,
                            responseStatus: response.status,
                            mutationState: "ambiguous",
                            safeToRetry: false,
                            cause: null,
                            request: context.request,
                        });
                    }
                }
                else if (context.record && context.phase === "initial_request") {
                    writeRecordBestEffort(context, store, {
                        state: "completed",
                        mutation_state: "not_started",
                        response_status: response.status,
                    });
                }
                return response;
            }
            catch (cause) {
                const balanceError = cause instanceof X402BalanceError ? cause : unwrapPolicyError(cause);
                if (balanceError) {
                    writeRecordBestEffort(context, store, {
                        state: "failed",
                        mutation_state: "not_started",
                        last_error_code: balanceError.code,
                    });
                    throw withAllowanceShortfall(balanceError, context.challengeBody ?? null);
                }
                if (cause instanceof PaymentAttemptError)
                    throw cause;
                if (cause instanceof Run402Error && !context.providerStarted) {
                    writeRecordBestEffort(context, store, {
                        state: "failed",
                        mutation_state: "not_started",
                        last_error_code: cause.code,
                    });
                    throw cause;
                }
                const providerStarted = context.providerStarted;
                const journalFailure = cause instanceof AttemptJournalWriteError || context.journalFailure;
                const code = journalFailure
                    ? "X402_ATTEMPT_JOURNAL_FAILED"
                    : providerStarted
                        ? "X402_PAYMENT_OUTCOME_AMBIGUOUS"
                        : context.phase === "challenge_received"
                            ? "X402_PAYMENT_SIGNING_FAILED"
                            : "X402_INITIAL_REQUEST_FAILED";
                const phase = providerStarted
                    ? context.responseStatus === null
                        ? "payment_submission"
                        : "payment_response"
                    : context.phase === "challenge_received"
                        ? "payment_signing"
                        : context.phase;
                const mutationState = providerStarted ? "ambiguous" : "not_started";
                writeRecordBestEffort(context, store, {
                    state: providerStarted ? "ambiguous" : "failed",
                    mutation_state: mutationState,
                    ...(context.responseStatus !== null ? { response_status: context.responseStatus } : {}),
                    last_error_code: code,
                });
                const transportCode = code === "X402_INITIAL_REQUEST_FAILED" && context.transportFailure === cause ? initialTransportCode(cause) : null;
                throw new PaymentAttemptError({
                    code,
                    ...(transportCode ? { transportCode } : {}),
                    category: code === "X402_INITIAL_REQUEST_FAILED" && context.transportFailure === cause ? "network" : "payment",
                    message: providerStarted
                        ? "The x402 payment request failed after provider dispatch; its outcome is unknown."
                        : journalFailure
                            ? "The x402 payment was not dispatched because its durable attempt could not be recorded."
                            : phase === "payment_signing"
                                ? "The x402 payment authorization could not be created; no payment was dispatched."
                                : context.transportFailure === cause ? `Network request failed${transportCode ? " (" + transportCode + ")" : ""} before payment dispatch. Check connectivity to the API; no payment was dispatched.` : "The initial request failed before an x402 payment was dispatched.",
                    phase,
                    paymentAttemptId: context.id,
                    providerStarted,
                    responseStatus: context.responseStatus,
                    mutationState,
                    safeToRetry: !providerStarted,
                    cause: cause instanceof AttemptJournalWriteError ? cause.cause : cause,
                    request: context.request,
                });
            }
        });
    };
}
function ensureIntent(context, store, now) {
    if (context.record)
        return;
    if (!claimIntent(context, store, now)) {
        throw new AttemptJournalWriteError(new LocalError("The generated x402 payment attempt id already exists.", "reserving x402 payment attempt", { code: "X402_ATTEMPT_ID_COLLISION" }));
    }
}
function claimIntent(context, store, now) {
    if (context.record)
        return true;
    const updatedAt = now();
    const record = {
        version: 1,
        payment_attempt_id: context.id,
        rail: "x402",
        state: "intent",
        mutation_state: "not_started",
        method: context.request.method,
        origin: context.request.origin,
        path_sha256: context.request.path_sha256,
        ...(context.request.caller_key_sha256
            ? { caller_key_sha256: context.request.caller_key_sha256 }
            : {}),
        created_at: context.createdAt,
        updated_at: updatedAt,
    };
    try {
        const claimed = store.claim(record);
        if (claimed)
            context.record = record;
        return claimed;
    }
    catch (cause) {
        context.journalFailure = true;
        throw new AttemptJournalWriteError(cause);
    }
}
function trustedRun402ResponseOrigin(requestUrl, response) {
    if (!response.url || response.redirected)
        return false;
    try {
        const request = new URL(requestUrl);
        const returned = new URL(response.url);
        return request.origin === returned.origin && isTrustedRun402PaymentUrl(request);
    }
    catch {
        return false;
    }
}
function retryAfterSeconds(response) {
    const raw = response.headers.get("retry-after");
    if (!raw || !/^\d+$/.test(raw))
        return null;
    const seconds = Number(raw);
    return Number.isSafeInteger(seconds) ? seconds : null;
}
function nonEmptyHeader(response, name) {
    const value = response.headers.get(name)?.trim();
    return value ? value : null;
}
function writeRecord(context, store, patch) {
    if (!context.record)
        throw new AttemptJournalWriteError("payment intent missing");
    const next = {
        ...context.record,
        ...patch,
        updated_at: context.now(),
    };
    try {
        store.write(next);
        context.record = next;
    }
    catch (cause) {
        context.journalFailure = true;
        throw new AttemptJournalWriteError(cause);
    }
}
function writeRecordBestEffort(context, store, patch) {
    if (!context.record)
        return;
    try {
        const next = {
            ...context.record,
            ...patch,
            updated_at: context.now(),
        };
        store.write(next);
        context.record = next;
    }
    catch {
        // The failure already carries the in-process structured outcome. Never
        // replace a known successful response with a local journal write error.
    }
}
/** Safe cause-code vocabulary only: never expose raw request/error text. */
function initialTransportCode(cause) {
    const known = new Set(["ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "ENETUNREACH", "EHOSTUNREACH", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_SOCKET"]);
    let current = cause;
    for (let i = 0; i < 5 && current && typeof current === "object"; i++) {
        const error = current;
        if (typeof error.code === "string" && known.has(error.code))
            return error.code;
        current = error.cause;
    }
    return null;
}
//# sourceMappingURL=paid-fetch.js.map