import { type LedgerCoverage, type PlanCatalog } from "./plan-model.js"; import { type TaxMode } from "./tax.js"; import type { BillingConfig } from "./types.js"; export type CheckLevel = "ok" | "warn" | "error"; export type Check = { level: CheckLevel; title: string; detail: string; /** What to do about it, when there is something to do. */ fix?: string; }; export type DoctorResult = { livemode: boolean; checks: Check[]; /** True when no check failed at `error`. */ healthy: boolean; }; /** * Inspect a Stripe environment for the misconfigurations that fail silently. * * @param webhookUrl - the endpoint you expect to be registered. Omit to skip the * webhook checks (correct for a local machine, which has none by design). */ export declare function checkBillingSetup(opts?: { webhookUrl?: string; /** * WHO calculates tax on this account — a choice the deployment makes, so it is * named after the thing doing the calculating rather than after how it feels: * * - `"local"` (default) — this library: `taxRatesFor` derives the rate * from `eu-vat-rates-data` + VIES and applies it as an explicit Stripe TaxRate. No * per-transaction fee, and nothing to set up in the Dashboard. Same spelling * Named for WHERE the calculation happens, not for how it feels — and * deliberately not `"auto"`, because Stripe's own field is `automatic_tax`, so * `"auto"` would name this mode after the one it is the alternative to. * - `"stripe"` — Stripe Tax (`automatic_tax`), for an account that wants * evidence-of-location, threshold monitoring and filing handled. * - `"none"` — an account that charges no tax; tax is not inspected. * * It decides WHICH silent failure is worth looking for, so the wrong mode is * worse than no check: `"stripe"` audits the head office, the registrations and * `tax_behavior`, none of which a `"local"` account has any reason to * hold — reporting those as errors is how a doctor sends someone to fix a config * that was already right. */ taxMode?: TaxMode; /** * Your `BillingConfig`, so the mode is read from `config.tax` rather than stated * again here — the point of one declaration is that nothing can disagree with it. * `taxMode` and `currency` above still win if passed. */ config?: BillingConfig; /** `config.currency`. Pass it to check for customers pinned to another one — * the half-applied currency change that produces no error anywhere. */ currency?: string; /** Flag customers with more than one ACTIVE subscription (double billing). * Default true. */ expectSingleSubscription?: boolean; }): Promise; /** * Inspect a plans config for the mistakes that don't announce themselves. * * Static: no Stripe call, so it can run in CI next to a typecheck. Separate from * `checkBillingSetup` because it asks about the CONFIG rather than the account. */ export declare function checkPlansConfig(plans: PlanCatalog, options?: { /** Whether this deployment can actually sell a plan. Pass true when a * checkout is mounted; without it, self-serve plans are flagged as * advertised-but-unbuyable. */ hasCheckout?: boolean; /** * What the wired ledger can count. * * Pass the ledger's own `covers` (every implementation here declares one) — * or `true`/`false` for the older shorthand, which meant "a per-member store * is wired". The coverage form is strictly better because it catches the case * the boolean cannot express: a ledger that counts per-member usage but not * ORG-wide included usage, which reads 0% on a pooled plan forever. * * OMIT it and the check is skipped entirely: undefined means "the caller did * not say", which is not the same as "nothing is wired". Defaulting it to * false would fail every existing consumer's CI over a config that may be * perfectly wired, and `createMeter` already warns at boot when it really is * missing. Only a plan that includes usage or rate-limits it needs one. */ usageLedger?: boolean | LedgerCoverage; }): DoctorResult; /** Render a DoctorResult for a terminal. Returns the exit code to use. */ export declare function formatDoctorResult(result: DoctorResult): { text: string; exitCode: number; }; /** * Which endpoint to work on: `--url ` overrides, `--no-webhook` means there * isn't one (correct on a laptop, which uses `stripe listen` instead). * * One parser, because `setup` and `doctor` take the same two flags and the whole * reason this plumbing moved into the library is that two hand-written copies of it * had already drifted. */ export declare function webhookUrlFromArgv(argv: string[], fallback?: string): string | undefined; export interface RunDoctorOptions { /** The app's catalogue. Checked FIRST: it needs no network, and a config mistake * explains most account-level symptoms. */ plans?: PlanCatalog; /** The app's `BillingConfig`. Supplies currency and the tax mode, so the doctor * reads the same declaration the engine does rather than being told twice. */ config?: BillingConfig; /** What the wired ledger can count — pass the ledger's own `covers`. */ usageLedger?: boolean | LedgerCoverage; /** True when a checkout is mounted, so self-serve plans are not flagged as * advertised-but-unbuyable. */ hasCheckout?: boolean; /** The deployed endpoint. Lowest precedence: `--url`, then `--no-webhook`, then * `BILLING_WEBHOOK_URL`, then this. Prefer the env var — where a deployment lives * is an environment fact, and a production URL in source is one a laptop registers. */ webhookUrl?: string; /** Audit WorkOS too — the other half of the substrate. Pass `{ oauthProxy: true }` * when the app mounts the MCP OAuth proxy, which is what makes * `REFRESH_TOKEN_SECRET` required. Omit to skip. */ workos?: boolean | { oauthProxy?: boolean; }; /** Defaults to `process.argv.slice(2)`. */ argv?: string[]; /** Defaults to `process.exit`. Injectable so this is testable. */ exit?: (code: number) => never; log?: (line: string) => void; } /** * Run both doctors, print them, and exit non-zero when something is actually wrong. * * Flags: `--url ` checks a different endpoint, `--no-webhook` skips the webhook * check entirely (correct locally, where by design there IS no endpoint). * * Exits 2 with a clear message when `STRIPE_SECRET_KEY` is unset, because that * variable decides WHICH environment is being checked — a doctor run against the * wrong account is worse than no run. */ export declare function runBillingDoctor(opts?: RunDoctorOptions): Promise; /** * Which environment a WorkOS key names — and `"unknown"` is a real answer. * * Older keys are `sk_test_…` / `sk_live_…`. Newer ones are `sk_` * (decoding to `key_01…`), which carries no environment marker at all. Reading * "anything that is not `sk_test` is production" therefore misreported every * new-format staging key as production — and then made `environmentMismatch` accuse * a perfectly matched local setup, which is the worse failure: a doctor whose errors * are sometimes fiction is one people learn to scroll past. */ export declare function workosEnvironmentOf(apiKey: string): "test" | "live" | "unknown"; /** * The two keys, disagreeing about which environment this is. * * Nothing compared them before, and both halves of the report state their own * environment plainly — "LIVE MODE" from Stripe, "production key" from WorkOS — so * a mixed pair printed both facts, passed every check and read as healthy. That is * the worst deploy mistake available here: a live Stripe key beside a staging * WorkOS key means real cards are charged against orgs, memberships and `sk_` keys * that live in the wrong environment, and the mapping between the two (the org's * `stripeCustomerId`) is written into the environment nobody is looking at. * * Pure, and separate from the network call, so it is testable offline. Silent when * the key does not name its environment: a comparison needs two answers, and * inventing the missing one is how a guard becomes a false alarm. */ export declare function environmentMismatch(apiKey: string, expectLivemode: boolean): Check | null; export declare function checkWorkOSSetup(opts?: { /** The app's `config.baseUrl`, so the report can print the exact AuthKit redirect * URI to allowlist. Omitted, that line is skipped rather than guessed. */ baseUrl?: string; /** True when the app mounts the MCP OAuth proxy (`createBilling({ oauthProxy })`). * Only then is `REFRESH_TOKEN_SECRET` required. */ oauthProxy?: boolean; /** * Which environment the STRIPE half is pointed at, so the two keys can be * compared. Omit for a WorkOS-only audit: absent means "the caller did not say * there is a Stripe half", never "the halves agree". */ expectLivemode?: boolean; }): Promise; //# sourceMappingURL=doctor.d.ts.map