import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { BillingAdapter, BillingConfig } from "./types.js"; import { type RegisterBillingToolsOptions } from "./tools/register.js"; import { type WebhookOptions } from "./routes/webhook.js"; import { type AgentAuthBranding, type AgentAuthPaths, type AgentAuthPolicy, type AgentIdentityType } from "./agent-auth/index.js"; import type { ClaimStore } from "./agent-auth/claim-store.js"; import { type MachinePaymentOptions } from "./machine-payment/index.js"; import { type OAuthProxyOptions } from "./oauth-proxy/index.js"; import type { Notifier } from "./notifications/index.js"; import { type UsageLedger } from "./usage-ledger.js"; import type { PlanCatalog } from "./plans.js"; export interface CreateBillingOptions { /** Storage adapter (WorkOSOrgAdapter or your own). */ adapter: BillingAdapter; /** Billing config; resolved once internally. */ config: BillingConfig; /** Per-tool credit costs (echoed by get_credit_balance + the REST tool list). */ toolCosts?: Record; /** WWW-Authenticate realm on 401s. */ realm?: string; /** Declarative plans → auto-provisioned Stripe products/prices + list_plans. */ plans?: PlanCatalog; defaultPlan?: string; /** Tax and return URLs for `buy_credits`. Supply `taxRates` on any account that * charges tax on its subscriptions — a top-up has no address form of its own, * so without this it invoices at 0%. */ topUp?: RegisterBillingToolsOptions["topUp"]; /** How to find an org's plan when it isn't on the adapter's subscription. */ resolvePlan?: (orgId: string) => Promise; /** Lifecycle tools (`change_plan`, `preview_plan_change`, …). Default on when * `plans` is set; pass false to keep plan changes in the app's own UI. */ subscriptionTools?: RegisterBillingToolsOptions["subscriptionTools"]; /** * Membership: the invitation service, and the roles this deployment invites into. * * Passing it is what turns on `invite_member` / `list_invitations` / `revoke_invitation` and * `api.members.invite` — there is nowhere to put an invitation record without one. The other * three member tools need only an adapter that can describe and change a membership. */ members?: RegisterBillingToolsOptions["members"]; /** Register your app's own product tools alongside the billing tools. */ registerTools?: (server: McpServer) => void; /** Enable auth.md agent self-registration. Omit to leave it off. */ agentAuth?: { branding: AgentAuthBranding; identityTypes?: AgentIdentityType[]; baseUrl?: string | ((request: Request) => string); policy?: AgentAuthPolicy; asMetadataExtra?: Record; paths?: AgentAuthPaths; claimStore?: ClaimStore; }; /** * Stripe webhook handler. Defaults on (currency from config); `false` to skip. * * Pass `onOtherEvent` to handle the events the route doesn't credit itself — * typically `createStripeEventHandler(...)`, so invoice.paid and * invoice.payment_failed run the same code the poller would. */ webhook?: (WebhookOptions & { currency?: string; }) | false; /** MCP transport overrides. */ /** MCP transport overrides. `requireAuth` gates the handshake itself — see * `createMcpTransport`. */ mcp?: { apiKeyPrefix?: string; maxDuration?: number; requireAuth?: boolean; }; /** * Enable MPP machine payments (pay-per-request 402). Omit to leave it off. * * `amount` is optional HERE, unlike in the standalone handler: omitted, a request is * priced at what the tool it is calling costs — `toolCosts[]`, read from the * path, which is the same map `get_credit_balance` and the REST tool list publish. A * consumer that wrote that function by hand was re-deriving its own rate card, and a * flat fee is wrong in both directions across a catalogue that spans 0 to 80 credits. * Pass a number to charge one price per request regardless of tool. */ machinePayment?: Omit & { amount?: MachinePaymentOptions["amount"]; }; /** * Where to send the things this library learns and cannot say itself. * * It knows the moment a member is invited, the moment somebody asks their admin for more * credit, and the moment an allowance crosses a threshold — and it renders no email, in * no language. Configure a notifier and each of those becomes an event, with the * recipients already resolved from the workspace's membership; the consumer renders and * sends. Omit it and every emission is a no-op that costs nothing. * * `webhookNotifier({ endpoint, secret })` is the shipped transport, for the common case * where the code that CAN render the email is an HTTP route away (a Next app whose * templates are JSX the billing package cannot compile). Any object with `deliver` works. */ notifications?: Notifier; /** Enable the MCP OAuth 2.1 + Dynamic Client Registration proxy, so MCP * clients (Claude Desktop, Claude.ai) can connect without an API key. When * set, the authorization_code + refresh_token grants are advertised, the * authorization/registration endpoints appear in AS metadata, and * `billing.oauth` exposes the four route handlers. Requires * REFRESH_TOKEN_SECRET. */ oauthProxy?: OAuthProxyOptions | true; /** Enable the bound call-site meter (`billing.meter`) + the API route guard * (`billing.meterRequest`). Pass your rate card; the meter uses `plans` above * for seat packs. Plan resolution defaults to the org's `plan` metadata key * (override via `resolvePlan`). Omit to leave metering off. */ meter?: { /** action → credit cost (per unit). Consumer-authored product data. */ rateCard?: Record; /** Resolve the org's current plan key. Default: the org's `plan` metadata * (via adapter.getOrgMetadata). Override to read a subscription, etc. */ resolvePlan?: (orgId: string) => Promise; /** Seat-type keys a caller maps to by identity. Default standard / api. */ seatDefaults?: { user?: string; api?: string; }; /** Plan-cache TTL (ms). Default 60_000. */ planCacheTtlMs?: number; /** Cycle window (unix s) + key. Default: 1st-of-month UTC / "YYYY-MM". */ cycleStart?: () => number; cycleKey?: () => string; /** * Where usage is COUNTED. Default: `stripeUsageLedger()` — the composite. * * It counts every ORG-wide window in Stripe (a meter summary, which sees * included usage and costs one request at any window width), and reads * per-CALLER windows from balance-transaction metadata. So a config whose * windows are all org-scoped — `cap: pool`, `cap: wallet`, `scope: "org"` * limits — needs no store at all. * * When a window is both INCLUDED and PER-MEMBER — a seat pack, or a * `scope: "caller"` limit — the default cannot see it, and it reads 0 forever. * Pass `stripeUsageLedger({ perCaller: stripeScopeUsageLedger() })`, which * counts that pair in Stripe too, with no store: each usage scope gets a Stripe * Customer of its own, which is the only second grouping key Stripe offers. * It is opt-in rather than default because creating those customers is a side * effect a consumer should choose. `checkPlansConfig` names the plans that * need it, and `createMeter` warns at boot. */ ledger?: UsageLedger; /** * Percentages of an included allowance worth telling somebody about. Default `[80, 100]`. * * Only meaningful with `notifications` configured, and only for allowances the plan * GIVES (a seat pack, the shared pool). Rate limits are deliberately never alerted on: * they reset within days and the customer cannot act on one. The customer's own spend * alerts are theirs, set through `set_spend_controls`, and are not a deployment default. */ alertThresholds?: readonly number[]; }; } export declare function createBilling(opts: CreateBillingOptions): { adapter: BillingAdapter; config: import("./types.js").ResolvedConfig; /** * What `runBillingCli` needs, taken from THIS composition — spread it into the * app's billing script and add only the webhook URL: * * runBillingCli({ ...billing.cli, webhookUrl: "https://myapp.example/api/stripe/webhook" }) * * Derived rather than restated, because every value here is one the script used * to name a second time: the catalogue, the config, and — the sharp one — what * the wired ledger can COUNT. A script that states its own coverage can be * right while the app is wrong, which is the exact shape of the worst bug this * library has had (a wallet-only ledger counting pooled usage as 0, so every * subscriber got unlimited requests, with every check passing). * * `hasCheckout` is true when a catalogue is registered with the lifecycle tools * left on, because `change_plan` then opens a hosted Checkout Session itself — * so a self-serve plan really is buyable without the app mounting anything. * * `workos` audits by default (WorkOS is the substrate every adapter here * assumes) and asks for `REFRESH_TOKEN_SECRET` only when the OAuth proxy is * actually mounted, which is the one thing that makes it required. * * The webhook URL stays the app's to give: it is a deployment fact, and putting * a production URL in this object would let a laptop run register it. */ cli: { plans: PlanCatalog | undefined; config: import("./types.js").ResolvedConfig; usageLedger: import("./plan-model.js").LedgerCoverage | undefined; hasCheckout: boolean; workos: true | { oauthProxy: true; }; }; /** The bound, org-scoped API: `api.invoices.list(orgId)`, `api.usage.summary(orgId)`, … */ api: { customerId: (orgId: string, email?: string) => Promise; customerIdIfAny: (orgId: string) => Promise; isInternal: (orgId: string) => Promise; plan: (orgId: string) => Promise; profile: { get: (orgId: string) => Promise; update: (orgId: string, patch: Parameters[2]) => Promise; }; taxIds: { list: (orgId: string) => Promise; set: (orgId: string, input: Parameters[2]) => Promise; }; cards: { list: (orgId: string) => Promise; setDefault: (orgId: string, paymentMethodId: string) => Promise; remove: (orgId: string, paymentMethodId: string) => Promise; setupIntent: (orgId: string, opts: Parameters[2]) => Promise<{ clientSecret: string; customerId: string; }>; setupCheckout: (orgId: string, opts: Parameters[2]) => Promise<{ clientSecret: string; sessionId: string; }>; attached: (orgId: string, paymentMethodId: string, opts?: { setDefault?: boolean; }) => Promise<{ madeDefault: boolean; }>; prune: (orgId: string, max?: number) => Promise; touch: (paymentMethodId: string) => Promise; }; invoices: { list: (orgId: string, limit?: number) => Promise; get: (orgId: string, invoiceId: string) => Promise; pdfUrl: (orgId: string, invoiceId: string) => Promise; }; subscription: { get: (orgId: string) => Promise<{ plan: string | null; status: string | null; subscriptionId: string | null; periodStart?: string | null; periodEnd: string | null; seats?: number | null; seatCounts?: Record | null; }>; change: (orgId: string, to: Parameters[2]["to"], opts?: Omit[2], "plans" | "to" | "config" | "currency">) => Promise; preview: (orgId: string, to: Parameters[2]["to"], opts?: Omit[2], "plans" | "to" | "currency">) => Promise; requests: { list: (orgId: string) => Promise; pending: (orgId: string, memberId: string) => Promise; ask: (orgId: string, memberId: string, opts?: { plan?: string; note?: string; metadata?: Record; contact?: { firstName: string; lastName: string; email: string; }; }) => Promise<{ ok: boolean; id?: string; plan?: string; pending?: import("./ladder.js").PlanRequest; reason?: "no_upgrade" | "unknown_plan" | "already_pending" | "already_on_it" | "queue_full"; }>; askSeat: (orgId: string, memberId: string, opts?: { seatType?: string; note?: string; }) => Promise<{ ok: boolean; id?: string; seatType?: string; pending?: import("./ladder.js").PlanRequest; reason?: "no_upgrade" | "unknown_plan" | "already_pending" | "already_on_it" | "queue_full"; }>; next: (orgId: string, memberId: string, actor?: { isAdmin?: boolean; }) => Promise<{ rung: "seat" | "credits" | "usage" | "plan"; to: string; actor: "self" | "admin"; action: import("./ladder.js").UsageActionTool; ask: "seat"; } | { rung: "seat" | "credits" | "usage" | "plan"; to?: string; actor: "self" | "admin"; action: import("./ladder.js").UsageActionTool; ask: "credits"; } | { rung: "seat" | "credits" | "usage" | "plan"; to?: string; actor: "self" | "admin"; action: import("./ladder.js").UsageActionTool; ask: "usage"; } | { rung: "seat" | "credits" | "usage" | "plan"; to: string; actor: "self" | "admin"; action: import("./ladder.js").UsageActionTool; ask: "plan"; } | null>; resolve: (orgId: string, requestId: string, decision: "done" | "denied") => Promise; satisfied: (orgId: string, request: Parameters[0]) => Promise; }; cancel: (orgId: string, opts?: Omit[2], "plans" | "currency">) => Promise; }; seats: { list: (orgId: string) => Promise<{ [x: string]: string; }>; get: (orgId: string, memberId: string) => Promise; defaultType: (orgId: string) => Promise; clearRecords: (orgId: string, memberIds: readonly string[]) => Promise; assign: (orgId: string, memberId: string, seatType: string | null) => Promise; capacity: (orgId: string, seatType: string) => Promise<{ seatType: string; assigned: number; purchased: number | null; max: number | null; remaining: number | null; }>; ladder: (orgId: string) => Promise; assignUnchecked: (orgId: string, memberId: string, seatType: string | null) => Promise; }; usage: { org: (orgId: string, members: readonly { id: string; kind?: "user" | "api"; }[], opts?: { now?: number; }) => Promise; summary: (orgId: string, opts?: { caller?: { kind: "user" | "api"; id?: string; seatType?: string; }; locale?: Parameters[2]["locale"]; now?: number; }) => Promise; byMember: (orgId: string, members: Parameters[2]["members"], opts?: { now?: number; }) => Promise; allowance: (orgId: string, opts?: Omit[2], "orgId" | "plans" | "plan" | "ledger">) => Promise; cycle: (orgId: string, opts?: { now?: number; }) => Promise; }; topUps: { list: (orgId: string) => Promise; request: (orgId: string, req: { memberId: string; amount: number; id?: string; cycle?: string; }) => Promise<{ id: string; cycle: string; }>; requestExtra: (orgId: string, memberId: string, opts?: { percent?: number; amount?: number; id?: string; }) => Promise<{ ok: boolean; id?: string; amount?: number; packSize?: number; cycle?: string; pending?: import("./topup.js").TopUpRequest; reason?: "invalid_amount" | "not_capped" | "already_pending" | "limit_reached" | "not_blocked"; }>; grantable: (orgId: string) => Promise<{ ok: boolean; reason?: "not_capped"; }>; pending: (orgId: string, memberId: string, cycle: string) => Promise; approve: (orgId: string, requestId: string) => Promise<{ ok: boolean; reason?: "not_found"; }>; deny: (orgId: string, requestId: string) => Promise<{ ok: boolean; reason?: "not_found"; }>; grant: (orgId: string, input: Parameters[2]) => Promise<{ ok: boolean; total: number; reason?: "invalid_amount" | "duplicate"; }>; grantExtra: (orgId: string, memberId: string, opts?: { percent?: number; grantedBy?: string; id?: string; }) => Promise<{ ok: boolean; total?: number; granted?: number; packSize?: number; cycle?: string; reason?: "invalid_amount" | "not_capped" | "duplicate"; }>; granted: (orgId: string, memberId: string, cycle: string) => Promise; }; members: { list: (orgId: string) => Promise; seats: (orgId: string) => Promise; isLastAdmin: (orgId: string, userId: string) => Promise; lastAdminId: (orgId: string) => Promise; invite: (orgId: string, input: { email: string; roleSlug?: string; inviterUserId?: string; seatType?: string | null; }) => Promise<{ ok: true; invitation: import("./invitations.js").Invitation; seats: import("./members.js").MemberSeats; seatType?: string | null; } | { ok: false; reason: import("./members.js").MemberRefusal; seats: import("./members.js").MemberSeats; }>; invitations: { list: (orgId: string) => Promise; revoke: (orgId: string, invitationId: string) => Promise; accept: (invitationId: string, user: Parameters[1]) => Promise<{ orgId: string; }>; }; setRole: (orgId: string, userId: string, roleSlug: string) => Promise<{ ok: true; roleSlug: string; } | { ok: false; reason: import("./members.js").MemberRefusal; }>; remove: (orgId: string, userId: string) => Promise<{ ok: true; cleared: number; } | { ok: false; reason: import("./members.js").MemberRefusal; }>; }; checkout: { complete: (sessionId: string, opts?: Parameters[2]) => Promise; }; workspace: { close: (orgId: string, opts?: Parameters[2]) => Promise; orphans: (opts?: Parameters[1]) => Promise<{ subscriptionId: string; customerId: string | null; orgId: string | null; amount: number | null; }[]>; }; quotes: { list: (orgId: string) => Promise; send: (orgId: string, input: Parameters[2]) => Promise<{ ok: true; request: import("./ladder.js").PlanRequest; } | { ok: false; reason: "not_found" | "invalid_amount" | "queue_full"; }>; accepted: (orgId: string, input: Parameters[2]) => Promise; }; auth: { access: () => Promise; admin: (action: string) => Promise; member: (orgId: string, memberId: string, action: string) => Promise; credits: (orgId: string, toolName: string, cost: number) => Promise; }; }; register: (server: McpServer, ctx?: { operator: boolean; }) => void; dispatcher: { dispatchTool: (name: string, args: Record) => Promise; getToolNames: () => string[]; }; /** MCP transport: mount `export const { GET, POST } = mcp` in app/[transport]/route.ts. */ mcp: { GET: (request: Request) => Promise; POST: (request: Request) => Promise; maxDuration: number; }; /** GET /api/v0 tool list handler. */ restList: (request: Request) => Promise; /** POST /api/v0/[tool] dispatch handler. */ restDispatch: (request: Request, ctx: { params: Promise<{ tool: string; }>; }) => Promise; /** Stripe webhook POST handler (undefined if `webhook: false`). */ webhook: ((request: Request) => Promise) | undefined; /** Bound call-site meter (undefined unless `meter` was configured): * `await meter(orgId, action, { caller })`. */ meter: import("./metering.js").Meter> | undefined; /** API route guard (undefined unless `meter` was configured): * `const gate = await meterRequest(req, action); if (gate) return gate`. */ meterRequest: import("./metering.js").ApiMeterGuard> | undefined; /** auth.md handlers (undefined unless `agentAuth` was configured). */ agentAuth: { protectedResource: (request: Request) => Response; authorizationServer: (request: Request) => Response; authMd: (request: Request) => Response; identity: (request: Request) => Promise; claim: (request: Request) => Promise; token: (request: Request) => Promise; handleClaimGrant: (params: Record) => Promise; revoke: (request: Request) => Promise; resourceMetadataUrl: (request: Request) => string; wwwAuthenticate: (request: Request) => string; } | undefined; /** MPP handler `{ requirePayment, buildChallenges }` (undefined unless `machinePayment` was configured). */ machinePayment: { requirePayment: (request: Request) => Promise; buildChallenges: (request: Request) => Promise; } | undefined; /** `/payment.md` handler (undefined unless `machinePayment` was configured). */ paymentMd: ((request: Request) => Response) | undefined; /** MCP OAuth proxy handlers (undefined unless `oauthProxy` was configured). * Mount as app/oauth/{authorize,register,callback,token}/route.ts. `token` * also serves the auth.md claim grant, so it replaces agentAuth.token. */ oauth: { authorize: (request: Request) => Promise; register: (request: Request) => Promise; callback: (request: Request) => Promise; token: (request: Request) => Promise; } | undefined; }; //# sourceMappingURL=create-billing.d.ts.map