import { type LocalTaxOrigin } from "./tax-origins.js"; import type { PartialMessages } from "./i18n.js"; /** * The role slug `WorkOSOrgAdapter.isAdmin` matches on, and the one the doctor checks exists. * * ONE definition — the adapter, the membership policy, the notification emitter (whose * "email the admins" audience is answered by it) and the doctor all read this. Four copies * of a magic string is how a check comes to disagree with the thing it checks, and the * failure is invisible in a headless run: org API keys keep working while every human gets * 403 from every admin-gated tool. * * It lives HERE, in the dependency-free module on the `/plans` leaf, so a client component * can compare a role against it without pulling Stripe and WorkOS into the browser bundle to * do it. That was not a hypothesis: a consumer had the literal in six files for that reason. */ export declare const ADMIN_ROLE_SLUG = "admin"; export interface BillingUser { id: string; email: string; firstName?: string | null; lastName?: string | null; profilePictureUrl?: string | null; } export interface ApiKeyInfo { id: string; name: string; obfuscatedValue: string; /** ISO timestamp the key was created. Optional so third-party adapters that * don't track it stay source-compatible. */ createdAt?: string; /** ISO timestamp of the key's last successful use, or null if never used. */ lastUsedAt?: string | null; /** Scopes granted to the key; empty/absent means full access. */ permissions?: string[]; } export interface BillingAdapter { /** Resolve a raw Bearer API key → org, or null if invalid/revoked. */ /** * Resolve a raw Bearer API key → org, or null if invalid/revoked. * * `keyId` is WHICH key was used, and it exists because dropping it here made a * documented capability impossible everywhere downstream. `MeterCaller.id` says * "API key id (api) — for per-caller attribution", but this seam returned only * the org, so `createApiMeterGuard` had nothing else to pass and sent the ORG id * instead: every metered API call recorded a `caller_id` that claimed to name a * key and named a workspace. No gate read it (an `api` caller's windows are * summed by KIND across the org, deliberately), so nothing was mis-charged — but * "which key burned the quota" was unanswerable for every consumer, and the * counter written under it looked like a member whose id happened to be a * workspace id. * * Optional, so an adapter that cannot tell keys apart stays source-compatible. * When it is absent the meter records NO caller id rather than a wrong one. */ validateApiKey(token: string): Promise<{ orgId: string; keyId?: string; } | null>; /** Optional: resolve an OAuth bearer (JWT) → org id. Omit if no OAuth. */ resolveOauthOrg?(token: string): Promise; /** Verified domains for the org (used for the internal-org unmetered check). */ getOrgDomains(orgId: string): Promise; /** Read the Stripe customer id pointer for the org (null if none yet). */ getBillingCustomerId(orgId: string): Promise; /** Persist the Stripe customer id pointer for the org. */ setBillingCustomerId(orgId: string, customerId: string): Promise; /** After magic-auth: find or create the org/workspace for this user. */ ensureOrgForUser(user: BillingUser): Promise<{ orgId: string; }>; /** Mint a new API key for the org. `createdBy` (the acting user id) is * passed when available; adapters that don't track it may ignore it. * Returns the raw value (shown once). */ mintApiKey(orgId: string, name: string, createdBy?: string): Promise<{ id: string; value: string; }>; /** List the org's (non-revoked) keys, obfuscated. */ listApiKeys(orgId: string): Promise; /** Revoke a key by id, scoped to the org (belongs-to check inside). */ revokeApiKey(orgId: string, id: string): Promise<{ id: string; name: string; } | null>; /** Revoke an API key by its raw value — for the RFC 7009 /oauth/revoke * endpoint, where only the token (not the org/id) is known. Optional. */ revokeApiKeyByToken?(token: string): Promise; /** Create an org with NO associated user (auth.md `anonymous` registration). * Optional — if absent, anonymous registration reports `anonymous_not_enabled`. */ createAnonymousOrg?(opts: { name: string; metadata?: Record; }): Promise<{ orgId: string; }>; /** Read the org's free-form metadata map (small key→string store). */ getOrgMetadata?(orgId: string): Promise>; /** Merge a patch into the org metadata (null value = delete the key). */ setOrgMetadata?(orgId: string, patch: Record): Promise; /** * The same store, for ONE MEMBER — where per-member records belong. * * The org map is a single shared budget (WorkOS: 600 chars per value), so a * per-member map packed into one of its values has a member ceiling. Measured, * that ceiling was 12: the 12th member's top-up grant overflowed the value and * the write failed. A record that is per-member is stored per-member instead, * where each one has a budget of its own and there is no ceiling. * * Optional, and the top-up engine falls back to the org map when it is absent — * so an existing adapter keeps working, with that ceiling. */ getUserMetadata?(userId: string): Promise>; /** Merge a patch into a member's metadata (null value = delete the key). */ setUserMetadata?(userId: string, patch: Record): Promise; /** Subscription state as the sync engine records it. */ getSubscription?(orgId: string): Promise<{ plan: string | null; status: string | null; subscriptionId: string | null; /** Start of the current period. An included allowance is measured over the * SUBSCRIPTION window, not the calendar month — an annual package measured * monthly would reset twelve times a year. */ periodStart?: string | null; periodEnd: string | null; /** PURCHASED seat quantity, summed across seat types. Sizes a `cap.perSeat` * pool. Purchased rather than active, because a workspace that bought ten * seats and filled six paid for ten; when it is absent the active member * count is used instead. */ seats?: number | null; /** The same quantity broken down by seat type. Required by * `cap.perSeat: "included"`, which multiplies each tier by its OWN * `includedCredits` — the only form that can size a pool for a plan with more * than one tier. The total above is its sum. */ seatCounts?: Record | null; }>; /** Record subscription state. `plan: undefined` leaves the plan as-is; `null` * clears it (back to the default plan). */ setSubscription?(orgId: string, sub: { plan?: string | null; status: string | null; subscriptionId: string | null; periodStart?: string | null; periodEnd: string | null; /** * What was PURCHASED — the mirror of the two fields `getSubscription` * above returns, and the reason a seat guard can answer at all. * * They were declared on the READ and not on the WRITE, so the only * implementation that could ever be told was the concrete * `WorkOSOrgAdapter` (which sync.ts is typed against); through this * interface the counts were unwritable, and any adapter satisfying it * reported `seatCounts: null` for ever. Everything measured against that * reads UNKNOWN — and unknown ALLOWS, so the dearest seat is free. * * `undefined` leaves them as they are; `null` clears them. */ seats?: number | null; seatCounts?: Record | null; }): Promise; /** Active members, for per-seat grants and seat limits. */ memberCount?(orgId: string): Promise; /** Active member ids. Needed by any read that must ENUMERATE a per-member * record (`listSeatAssignments`), because a per-member store can be asked * about a member but cannot be asked who the members are. */ listMemberIds?(orgId: string): Promise; /** Whether a user is an admin/owner of the org (gates auto-top-up + approvals). */ isAdmin?(orgId: string, userId: string): Promise; /** * Everyone in the workspace WITH their role — what `listMemberIds` cannot answer. * * The role is what the two membership rules turn on: a plan's member limit counts them, * and "is this the last admin" cannot be asked at all without it. An adapter that * implements this gets the member tools; one that does not keeps the seat tools and loses * nothing else, because a tool that could only ever fail is worse than an absent one. */ listMembers?(orgId: string): Promise; /** Move a member between roles. The last-admin rule is enforced above this, in * `members.ts`, so every surface refuses identically. */ setMemberRole?(orgId: string, userId: string, roleSlug: string): Promise; /** Drop a membership. Called AFTER the member's own records are cleared — see * `removeMember`, where the ordering and its reason live. */ removeMember?(orgId: string, userId: string): Promise; /** Remove the workspace. Called LAST by `closeWorkspace`, never before its billing has * been stopped — the org holds the Stripe pointer, so deleting it first orphans the * subscription. Absent means the caller removes the org itself. */ deleteOrg?(orgId: string): Promise; /** The workspace's display name, and how to change it. A name is what an invoice, a * members list and a workspace switcher all show, so a headless caller that can create * and close a workspace should be able to name it too. */ getOrgName?(orgId: string): Promise; renameOrg?(orgId: string, name: string): Promise; } /** * One person in a workspace, as the seam describes them. * * SDK-independent like every other DTO here (`BillingAdapter`, `ApiKeyInfo`): a non-WorkOS * adapter has to be able to satisfy it. `roleSlug` is nullable because an adapter may be able * to enumerate members without describing them — and a null role is what makes the last-admin * rule refuse rather than guess. */ export interface OrgMember { userId: string; email: string | null; name: string | null; /** `admin` gates the management tools. Null when the adapter cannot report it. */ roleSlug: string | null; status: "active" | "inactive" | "pending"; /** ISO 8601, when the adapter knows it. */ createdAt?: string; } export interface BillingConfig { /** Welcome credit granted on first Stripe customer creation. Default 100. */ freeCredits?: number; /** Stripe currency, e.g. "usd" | "eur". Default "usd". */ currency?: string; /** Base URL for Checkout success/cancel + billing-portal return. */ baseUrl: string; /** Domains whose orgs are unmetered (internal). Default []. */ internalDomains?: string[]; /** * Language new customers get their invoices in, as a BCP-47 code. * * Stripe's own fallback is English, which is wrong for a product sold in one * country: a settings screen showing "Italian" as the default while Stripe * mails English invoices is a lie the user only discovers on the first * invoice. Setting it at customer creation makes the default real. * * Existing customers are untouched — this is a default, not a migration. * Default: unset, i.e. Stripe's English. */ defaultLocale?: string; /** * WHO calculates tax, declared ONCE for the whole deployment. * * Every charge the library builds reads this — the seat Checkout Session, the * `buy_credits` top-up, and the auto-reload invoice — so the answer to "does * this account charge VAT" lives in one place instead of at each call site. An * explicit `taxRates` / `automaticTax` argument at a call site still wins. * * That per-site arrangement is why the two charges with no form behind them (the * auto-reload and the top-up) went out untaxed while every seat invoice on the * same account charged 22% IVA: nothing was wrong at any one site, there was * simply no single place that said what the account does. */ tax?: TaxConfig; /** * WHO, inside a workspace, may spend its money. * * Every other write that costs something is already an owner action, enforced in one * place: `change_plan`, `cancel_plan`, `assign_seat_type` (a seat is a price), * `grant_top_up`, `set_spend_controls`. Buying credits and arming auto-reload were not, * so an org API key held by any member could charge the card the owner saved — while the * consuming app's own UI refused exactly that. A rule the frontend enforces and the API * does not is the gap this library exists to close, so it moves here. * * `"admin"` (the default) means `buy_credits` and `set_auto_reload` require an admin * principal, the same way the rest of the money surface does. `"member"` restores the old * behaviour for a deployment where anyone may top up — say a per-seat product whose * members hold their own cards. * * This is also what `usageAction` reads to answer "do I buy this, or ask someone": a * member on a blocked window is offered a REQUEST precisely because the purchase is not * theirs to make. */ roles?: { purchase?: "admin" | "member"; }; /** * The library's own strings, overridden. * * Everything this package emits reads `Messages` — a refusal, a basket problem, a plan * table. `DEFAULT_MESSAGES` is English and always will be; a deployment selling in one * language passes its own here ONCE, and the sentence a refused caller reads comes back * in it through the API and the CLI as well as the UI. Before this the app translated * refusals by mapping reason codes on its own screens, so the same customer got Italian * in the browser and English from a tool call. */ messages?: PartialMessages; /** * The monthly spend ceiling every customer has whether or not they set one. * * `defaultCredits` is what applies when the customer's own record names no limit. * The meter reads it (`resolveAllowance`), so it is a real ceiling and not a * placeholder a settings page shows — which is exactly what it was in the one * consumer that had this: the app's read persisted the default on first access, so * the ceiling existed only for a workspace whose billing page somebody had opened. * An API-only workspace had none at all. * * `required: true` also refuses `set_spend_controls` clearing it. A deployment that * wants an uncapped default leaves both alone: null and not required is the shape * this library has always had. */ spendLimit?: { defaultCredits?: number | null; required?: boolean; }; /** What the payment forms offer. See `defaultPaymentMethodConfig`. */ paymentMethods?: { /** * Offer Stripe Link. Default FALSE, and that default is the whole point: * Link's inline signup ("Save my info for faster checkout") is drawn by the * Payment Element from the ACCOUNT's Link setting, so it survives both * `wallets.link: "never"` and `payment_method_types: ["card"]`. The only * lever is a payment-method configuration, which the library now provisions * itself rather than leaving every app to discover this. * * Set `true` to keep Stripe's behaviour (no configuration is imposed). */ link?: boolean; /** * How many cards a customer keeps. Default 3 (`DEFAULT_MAX_CARDS`). * * Not a Stripe limit — a product rule, and one a consumer was enforcing on its own * while the tools attached cards without it, so the same account could hold 3 cards * through the UI and any number through the API. */ maxCards?: number; }; } /** Settings every tax mode shares. */ type TaxConfigCommon = { /** * Where you are registered to collect tax. * * The second input a rate needs and no dataset can supply. `origin` says where you * are established; this says where you took on an obligation, which is what decides * whether a sale is taxed at all. * * ```ts * registrations: [{ country: "IT" }, { country: "GB" }] // VAT registrations * registrations: [{ country: "US", state: "CA" }] // US nexus * ``` * * **Undefined is "the caller did not say"**, never "registered nowhere": the regime * rules alone then decide, which is what every deployment predating this option * gets. Declared, there is ONE rule for everywhere including domestic — so `[]` says * something omitting it cannot, and is how a small-business exemption (France's * franchise en base, Germany's Kleinunternehmerregelung) is expressed: charge * nothing, anywhere. * * One obligation is deliberately not gated by it: destination VAT on a sale from * outside the EU to an EU consumer arises with no threshold to sit under. */ registrations?: readonly { country: string; state?: string; }[]; /** * Mandatory invoice wording, per outcome. * * ```ts * notes: { * exempt: "TVA non applicable, art. 293 B du CGI", * reverseCharge: "Autoliquidation, art. 196 dir. 2006/112/CE", * } * ``` * * Where a regime requires the invoice to state WHY a sale is untaxed, that mention * is not decoration: France fines €15 per invoice missing the 293 B wording, and the * CJEU held in C-247/21 that an omitted reverse-charge mention cannot be cured * afterwards. Supplying `exempt` mints a 0% Stripe TaxRate carrying it, so it renders * as a tax line on every invoice from every charge path. Supply nothing and an * untaxed sale carries no line, exactly as before. * * Stripe caps a TaxRate display name at 50 characters — the mention, not the * explanation. */ notes?: { exempt?: string; reverseCharge?: string; }; /** * Are you registered for the EU One-Stop Shop? Default true. `local` only. * * Decides ONE case: a cross-border EU customer with no valid VAT number. Reverse * charge needs a valid id, so without one the sale is taxed somewhere — registered, * at the CUSTOMER's rate; not registered, at YOUR OWN, which is what the sub-€10 000 * regime allows and the only rate you can remit without a foreign registration. */ oss?: boolean; /** * Resolve the TaxRate ids yourself, e.g. from your own records. * * **Wins over `mode` when it returns any**, because the hook exists to be * authoritative — which also makes it the one place a setting can go quietly dead: * whatever this function does not account for is not applied, whatever the config * says. Use `mode: "stripe"` for an establishment the local engine cannot compute; * this is for per-ORG rates that `config.tax` cannot express. */ rates?: (stripeCustomerId: string) => Promise | string[]; /** * Use Stripe Tax. Equivalent to `mode: "stripe"`, and ignored when `rates` returns * any (Stripe rejects manual rates and `automatic_tax` on one charge). * * Off unless set: a charge with neither is untaxed rather than quietly handed to * Stripe Tax, which without an active registration computes 0% and reports no error. */ automatic?: boolean; }; /** * WHO calculates tax, declared ONCE for the whole deployment. * * Every charge the library builds reads this — the seat Checkout Session, the * `buy_credits` top-up, the auto-reload invoice — so the answer to "does this account * charge VAT" lives in one place instead of at each call site. That per-site * arrangement is why the two charges with no form behind them once went out untaxed * while every seat invoice on the same account charged 22% IVA. * * **The union is what makes `origin: "US"` with the local mode impossible to write.** * The local engine has rates for 45 European countries and no others, so a US, AU, JP, * SG, CA, IN, BR or MX establishment cannot compute its own domestic tax here. That * used to typecheck and then throw on the first charge; it is now a compile error at * the config site, which is where the decision was made. */ export type TaxConfig = TaxConfigCommon & ({ /** * This library calculates, in process, from `eu-vat-rates-data` + VIES, and * applies the answer as an explicit Stripe TaxRate. The default. */ mode?: "local"; /** * Where YOU are established. Decides domestic vs cross-border, which is the * whole question a VAT rate turns on. * * Constrained to the countries the local engine has rates for. If yours is * absent — the US above all — that is a fact about published rate data, not an * omission: use `mode: "stripe"`. * * Omitted, it falls back to the Stripe account's own country, and a fallback * the local engine cannot compute is caught at boot instead. */ origin?: LocalTaxOrigin; } | { /** Stripe Tax (`automatic_tax`). 0.5% per taxed transaction, and it needs * registrations — without one it returns ZERO tax rather than an error. */ mode: "stripe"; origin?: string; } | { /** No tax on anything the library charges. Correct for an account that * genuinely charges none, and something you write down rather than arrive at * by omission. */ mode: "none"; origin?: string; }); export type ResolvedConfig = Required> & Pick & { roles: Required>; spendLimit: Required>; }; export declare function resolveConfig(c: BillingConfig): ResolvedConfig; /** Build the `internalDomains` allowlist from the environment: an optional * deployment root domain (whatever your host exposes — pass it if you want the * deployment's own domain treated as internal) plus a comma-separated env var * (default `INTERNAL_ORG_DOMAINS`). Orgs with a verified WorkOS domain matching * any entry get unmetered access (see enforceCredits → isInternalOrg). Result is * lowercased + de-duplicated. Host-agnostic: the caller supplies the root * domain (or nothing); the env var is generic. */ export declare function internalDomainsFromEnv(rootDomain?: string | null, envVar?: string): string[]; export type ToolResult = { content: Array<{ type: "text"; text: string; }>; isError?: boolean; }; export type ToolErrorResult = { isError: true; content: Array<{ type: "text"; text: string; }>; }; export {}; //# sourceMappingURL=types.d.ts.map