import { I as IntegrationRuntimeError, a as IntegrationUserAction } from '../errors-Bg3_rxnQ.js';
import { a as WebhookEnvelope } from '../router-BncoovUh.js';

/**
 * Subscription state machine.
 *
 * Stripe ships eight terminal states on `Subscription.status`. We model
 * them verbatim — never normalize, never collapse — so the state
 * persisted in product DBs round-trips Stripe webhooks losslessly.
 *
 *   incomplete           — first invoice not paid within 23 hours
 *   incomplete_expired   — first invoice failed, no retry coming
 *   trialing             — inside a trial window (treat as active)
 *   active               — paying, current
 *   past_due             — auto-renewal failed; grace period running
 *   canceled             — terminal; ended at period boundary or hard
 *   unpaid               — past_due → unpaid after retries exhausted
 *   paused               — operator-paused (collection_method=pause_collection)
 *
 * Transition rules below derive STRICTLY from the Stripe state diagram
 * (https://docs.stripe.com/billing/subscriptions/overview#subscription-statuses)
 * — the dispatcher in `webhooks.ts` calls `applyTransition()` which
 * rejects any state pair Stripe never emits. This catches manual-edit
 * bugs (someone POSTing a `force_state` admin endpoint) and tests for
 * the consumer's state store at the same time.
 *
 * Persistence: products pick an adapter (FS, D1, Postgres, in-memory).
 * The interface is intentionally minimal — load(), save(), and a
 * compare-and-set `saveIfVersion()` that defends against duplicate
 * webhook delivery racing the same store. Stripe re-delivers failed
 * webhooks for 3 days; the in-flight one and the retry will both write
 * the same key. `WebhookRouter`'s idempotency hook short-circuits at
 * the event level, but a misconfigured deploy with two routers in
 * different regions both processing the same event needs the second
 * line of defense here.
 *
 * `requireActiveSubscription()` (middleware) calls `gateAccess(state)`
 * to map state → access-decision. `past_due` is intentionally allowed
 * with a warning flag — the dunning period is when products MOST need
 * customers to keep using the product so they remember why they pay,
 * but the UI should render the "card declined" banner.
 */
type SubscriptionState = 'incomplete' | 'incomplete_expired' | 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | 'paused';
/** All eight states, exported so tests + consumers can enumerate. */
declare const SUBSCRIPTION_STATES: readonly SubscriptionState[];
/** Tristate access decision. `warn` means the route runs, but the UI
 *  should render a billing banner. */
type AccessDecision = {
    allowed: true;
    warn?: 'past_due' | 'trial_ending';
} | {
    allowed: false;
    reason: 'no_subscription' | 'subscription_inactive' | 'subscription_past_due' | 'trial_expired';
};
interface SubscriptionRecord {
    /** Tenant key the product uses to look up "is this workspace paying?" —
     *  typically a workspaceId, but products that bill per-user or
     *  per-organization can swap in their own scope. */
    workspaceId: string;
    /** Stripe customer id (`cus_...`). */
    customerId: string;
    /** Stripe subscription id (`sub_...`). */
    subscriptionId: string;
    /** Last-known subscription state — updated by webhook handlers. */
    state: SubscriptionState;
    /** Stripe price id active on the subscription. Null for canceled. */
    priceId: string | null;
    /** Current billing period end (unix seconds). Used by middleware to
     *  emit `trial_ending` warning in the last 72h of a trial. */
    currentPeriodEnd: number | null;
    /** Trial end (unix seconds), null for non-trial subs. */
    trialEnd: number | null;
    /** `cancel_at_period_end` flag — once true, state stays `active` until
     *  the period ends, then transitions to `canceled`. */
    cancelAtPeriodEnd: boolean;
    /** Monotonic write counter for optimistic concurrency. Incremented on
     *  every save; persistence adapters use it for CAS. */
    version: number;
    /** Last event id we processed for this subscription — defends against
     *  Stripe re-delivering the same event and us racing the dedupe store. */
    lastEventId: string | null;
    /** Wall-clock ms of last successful write. */
    updatedAt: number;
}
/** Persistence adapter contract. Three operations — pick the storage
 *  layer that matches the product's existing infra. Adapters live below
 *  (in-memory + filesystem). D1 / Postgres are one-liners on top. */
interface SubscriptionStore {
    load(workspaceId: string): Promise<SubscriptionRecord | null>;
    save(record: SubscriptionRecord): Promise<void>;
    /** Compare-and-set on `version`. Returns false if the stored record's
     *  version doesn't match `expectedVersion` (someone else wrote first).
     *  Implementations MUST return false rather than throw on contention —
     *  the caller branches on the bool. */
    saveIfVersion(record: SubscriptionRecord, expectedVersion: number): Promise<boolean>;
}
/** Returns true if `to` is reachable from `from` in one Stripe transition.
 *  Self-edges are accepted (a webhook can re-emit the current state on
 *  any field change). */
declare function isValidTransition(from: SubscriptionState, to: SubscriptionState): boolean;
/** Apply a state transition to a record. Throws `BillingError` if Stripe
 *  would never emit this edge (defensive — a bad admin tool POSTing a
 *  raw state update gets refused). Returns the new record without writing. */
declare function applyTransition(current: SubscriptionRecord, next: Partial<SubscriptionRecord> & {
    state: SubscriptionState;
}, options?: {
    eventId?: string;
    now?: () => number;
}): SubscriptionRecord;
/**
 * Map a state to an access decision.
 *
 * Rule rationale:
 *   active, trialing             → allow
 *   past_due                     → allow + warn (dunning grace)
 *   paused                       → deny (operator action; resume restores)
 *   canceled, unpaid             → deny (terminal financial states)
 *   incomplete, incomplete_expired → deny (never paid; first invoice failed)
 *
 * Note `requireActiveSubscription` in `middleware.ts` is the consumer of
 * this — gating is centralized here so the rule lives in one place. The
 * mapping is one assertion in the test suite. Changing the rule for one
 * product (e.g., legal-agent wants past_due to deny) is a per-call
 * `overrides` option on the middleware, NOT a fork of this function.
 */
declare function gateAccess(state: SubscriptionState): AccessDecision;
/**
 * Process-local store. Useful for tests; product instances should pick
 * `FileSystemSubscriptionStore` or wire D1 / Postgres. Implements proper
 * CAS — concurrent saves with stale versions are rejected.
 */
declare class InMemorySubscriptionStore implements SubscriptionStore {
    private readonly records;
    load(workspaceId: string): Promise<SubscriptionRecord | null>;
    save(record: SubscriptionRecord): Promise<void>;
    saveIfVersion(record: SubscriptionRecord, expectedVersion: number): Promise<boolean>;
}
/**
 * File-per-workspace JSON store. One file per workspace under
 * `<rootDir>/<workspaceId>.json`. Cheap, durable, debuggable — adequate
 * for self-hosted product agents. CAS is implemented via the version
 * field plus a write that re-reads the file under a brief lock window
 * (rename-temp-to-target pattern, atomic on POSIX).
 *
 * Why per-file and not one JSONL: subscriptions are
 * accessed by workspaceId 99% of the time, scanning a JSONL on every
 * request burns I/O. The file-per-workspace pattern keeps reads O(1).
 *
 * The store does NOT use `fs.watch` — webhooks are the only writer in
 * production, and webhooks always go through `applyTransition()` →
 * `saveIfVersion()`, so the CAS catches the race.
 */
declare class FileSystemSubscriptionStore implements SubscriptionStore {
    private readonly rootDir;
    constructor(rootDir: string);
    load(workspaceId: string): Promise<SubscriptionRecord | null>;
    save(record: SubscriptionRecord): Promise<void>;
    saveIfVersion(record: SubscriptionRecord, expectedVersion: number): Promise<boolean>;
    /** Safe filename: workspaceId is restricted to a charset that maps 1:1
     *  to a posix filename. Anything outside is hex-encoded so we can never
     *  escape the rootDir via `../`. */
    private fileName;
}
/** Convenience constructor for the initial record after a checkout
 *  succeeds. The webhook handler for `customer.subscription.created` calls
 *  this — exposed for tests + manual-fix scripts that need to backfill. */
declare function makeSubscriptionRecord(input: {
    workspaceId: string;
    customerId: string;
    subscriptionId: string;
    state: SubscriptionState;
    priceId: string | null;
    currentPeriodEnd: number | null;
    trialEnd?: number | null;
    cancelAtPeriodEnd?: boolean;
    now?: () => number;
}): SubscriptionRecord;

/**
 * Stripe billing error taxonomy.
 *
 * Layered on top of `IntegrationRuntimeError` (the cross-package error
 * contract) so a billing failure surfaces through the same normalization
 * pipeline the rest of the integration runtime uses: `status` maps to
 * an HTTP status, `userAction` carries the recommended next step
 * (connect / reconnect / contact_support), and `code` is stable across
 * versions for product analytics.
 *
 * Why a billing-specific subclass and not bare `IntegrationRuntimeError`:
 * three error classes carry a payload product agents NEED to branch on
 * (subscription state, tenant id, plan id). Encoding them as discriminated
 * subclasses keeps the call site `instanceof BillingError` lookup O(1)
 * and avoids stuffing them into `metadata` where they decay to `unknown`.
 *
 * Code mapping (`BillingErrorCode` → `IntegrationErrorCode`):
 *   subscription_required → action_denied        (403)
 *   subscription_inactive → action_denied        (403)
 *   subscription_past_due → action_denied        (403, w/ warning)
 *   trial_expired         → action_denied        (403)
 *   free_tier_exhausted   → action_denied        (403)
 *   tenant_not_configured → provider_error       (500) — operator bug
 *   webhook_secret_missing→ provider_auth_failed (401) — config bug
 *   webhook_event_unknown → input_invalid        (400)
 *   webhook_replay        → input_invalid        (400)
 */

type BillingErrorCode = 'subscription_required' | 'subscription_inactive' | 'subscription_past_due' | 'trial_expired' | 'free_tier_exhausted' | 'tenant_not_configured' | 'webhook_secret_missing' | 'webhook_event_unknown' | 'webhook_replay';
interface BillingErrorContext {
    workspaceId?: string;
    productId?: string;
    subscriptionId?: string;
    subscriptionState?: SubscriptionState;
    planId?: string;
    eventId?: string;
}
declare class BillingError extends IntegrationRuntimeError {
    readonly billingCode: BillingErrorCode;
    readonly context: BillingErrorContext;
    constructor(input: {
        code: BillingErrorCode;
        message: string;
        context?: BillingErrorContext;
        userAction?: IntegrationUserAction;
    });
}
/** Distinct subclass: operator missed an environment variable. Surfaces
 *  with a different `userAction` (`contact_support`) so the consumer
 *  doesn't render a "connect Stripe" CTA to a customer for what is in
 *  fact a backend deploy bug. */
declare class ConfigError extends BillingError {
    constructor(input: {
        message: string;
        context?: BillingErrorContext;
    });
}

/**
 * Stripe subscription webhook dispatcher.
 *
 * Receives `WebhookEnvelope` rows from `WebhookRouter`'s `deliver()`
 * callback, decodes them into typed `StripeBillingEvent` values, and
 * applies the corresponding state transition to the consumer's
 * `SubscriptionStore`. Emits a typed event the consumer subscribes to.
 *
 * Layering:
 *
 *   Stripe → HTTP → WebhookRouter (verify + idempotency dedup)
 *                          ↓ deliver(envelope)
 *                  StripeBillingDispatcher (this file)
 *                          ↓
 *                          ├─ SubscriptionStore.saveIfVersion(...)
 *                          └─ emit(typed event) → consumer's subscriber
 *
 * Critical guarantees:
 *
 *  1. Idempotency at two layers — the router de-dupes at the event id;
 *     the dispatcher's `saveIfVersion` defends against the second
 *     router instance (multi-region) racing the same event. The
 *     consumer's subscriber sees an event AT MOST ONCE per `eventId`.
 *
 *  2. Order-independence — Stripe doesn't guarantee delivery order.
 *     We process events whose `event.created` timestamp is older than
 *     the stored `updatedAt` only when the resulting state would be a
 *     valid transition; otherwise we drop with `dropped:'out_of_order'`.
 *
 *  3. Explicit unknown handling — events we don't have a handler for
 *     are not dropped silently; we emit them as
 *     `stripe.event_unhandled` so the consumer can log + alert if
 *     they expected coverage that we don't ship.
 *
 *  4. Idempotency of the dispatcher itself — calling `dispatch()` with
 *     an event whose id equals `lastEventId` on the loaded record is a
 *     no-op that emits `stripe.event_replay` instead of advancing state.
 *
 * Events supported (8 critical + 2 lifecycle):
 *
 *   customer.subscription.created
 *   customer.subscription.updated
 *   customer.subscription.deleted
 *   customer.subscription.trial_will_end
 *   customer.subscription.paused
 *   customer.subscription.resumed
 *   invoice.paid
 *   invoice.payment_failed
 */

/** Strongly-typed events the consumer can subscribe to. Each carries
 *  enough context to drive downstream side effects without a second
 *  DB round-trip (audit log row, Slack ping, in-app notification). */
type StripeBillingEvent = {
    kind: 'subscription.created';
    eventId: string;
    record: SubscriptionRecord;
} | {
    kind: 'subscription.updated';
    eventId: string;
    previousState: SubscriptionState;
    record: SubscriptionRecord;
} | {
    kind: 'subscription.deleted';
    eventId: string;
    record: SubscriptionRecord;
} | {
    kind: 'subscription.trial_will_end';
    eventId: string;
    record: SubscriptionRecord;
    trialEndsAt: number;
} | {
    kind: 'subscription.paused';
    eventId: string;
    record: SubscriptionRecord;
} | {
    kind: 'subscription.resumed';
    eventId: string;
    record: SubscriptionRecord;
} | {
    kind: 'invoice.paid';
    eventId: string;
    record: SubscriptionRecord | null;
    invoiceId: string;
    amountPaid: number;
} | {
    kind: 'invoice.payment_failed';
    eventId: string;
    record: SubscriptionRecord | null;
    invoiceId: string;
    amountDue: number;
} | {
    kind: 'event_unhandled';
    eventId: string;
    type: string;
} | {
    kind: 'event_replay';
    eventId: string;
    type: string;
} | {
    kind: 'event_dropped_out_of_order';
    eventId: string;
    type: string;
    reason: string;
};
/** Listener — the product agent wires this to whatever side-effect bus
 *  it owns (audit log, in-process emitter, durable queue). Throws are
 *  caught by `dispatch()` and surfaced through `onError`. */
type StripeBillingListener = (event: StripeBillingEvent) => void | Promise<void>;
interface StripeBillingDispatcherOptions {
    store: SubscriptionStore;
    /** Maps a Stripe `customer.id` → the workspaceId the product uses to
     *  key its `SubscriptionStore`. We default to reading
     *  `subscription.metadata.workspaceId` (agents inject it at checkout
     *  time); supply this override for products that key by customer id
     *  directly or look up a join table. */
    resolveWorkspaceId?(input: {
        customerId: string;
        subscriptionMetadata?: Record<string, string>;
        invoiceMetadata?: Record<string, string>;
    }): Promise<string | null> | string | null;
    /** Single typed listener (most consumers want one — they route inside
     *  it themselves). Compose multiple via `combineListeners(a, b)`. */
    listener?: StripeBillingListener;
    /** Surface unexpected dispatcher errors (validation, store contention
     *  exhausted) without crashing the webhook handler. */
    onError?(err: unknown, context: {
        eventId: string;
        type: string;
    }): void;
    /** Override `Date.now()` for tests. */
    now?(): number;
    /** Max retries on `saveIfVersion` contention. Default 3. */
    maxCasRetries?: number;
}
/**
 * Process a webhook envelope. Safe to call concurrently with itself —
 * the in-store CAS serializes per-workspace updates.
 */
declare class StripeBillingDispatcher {
    private readonly store;
    private readonly resolveWorkspaceId;
    private readonly listener?;
    private readonly onError;
    private readonly now;
    private readonly maxCasRetries;
    constructor(opts: StripeBillingDispatcherOptions);
    /** Drive one envelope through the pipeline. Idempotent w.r.t. the
     *  event id (replays are a no-op + emit `event_replay`). */
    dispatch(envelope: WebhookEnvelope): Promise<void>;
    private handle;
    private handleSubCreated;
    private handleSubUpdated;
    private handleSubDeleted;
    private handleTrialWillEnd;
    private handleSubLifecycle;
    private handleInvoicePaid;
    private handleInvoicePaymentFailed;
    /** Load, apply a transformation, CAS-write. The transformation may
     *  return 'replay' / 'out_of_order' for the dispatcher to emit
     *  diagnostic events instead. Retries on contention up to
     *  `maxCasRetries`; if exhausted, emits via `onError`. */
    private advance;
    private cas;
    private emit;
    private emitNoWorkspace;
}
/** Compose multiple listeners — fan out + collect errors. */
declare function combineListeners(...listeners: StripeBillingListener[]): StripeBillingListener;

/**
 * Per-tenant Stripe configuration routing.
 *
 * Five product agents (legal, tax, gtm, creative, agent-builder) each
 * own a SEPARATE Stripe account. Reasons we pay the multi-account tax
 * rather than billing everyone through a single Tangle Stripe account:
 *
 *  1. Each product is a different LLC/legal entity for tax + dispute
 *     handling. Customer chargebacks land on the product's account.
 *  2. Stripe Tax + Atlas are per-account; we can't share a single
 *     Tax-collection setup across five SaaS products.
 *  3. Each product has its own pricing experiments; sharing one account
 *     would force a shared products/prices namespace and surface
 *     leak risk in the Stripe dashboard.
 *
 * The routing table maps `productId` (a Tangle-internal stable
 * identifier) to:
 *   - Stripe Secret Key  (`sk_live_…` or `rk_live_…`)
 *   - Webhook signing secret (`whsec_…`) — used by the WebhookRouter's
 *     `resolveSecret` callback.
 *   - Optional success/cancel URL defaults the product wants used
 *     unless the caller overrides per-checkout.
 *
 * Env-var convention is `STRIPE_SK_<PRODUCT_UPPER>` and
 * `STRIPE_WHSEC_<PRODUCT_UPPER>` — the resolver below honors that by
 * default. Consumers that store keys in a vault (Doppler, AWS Secrets
 * Manager) inject their own `TenantConfigResolver` instead.
 *
 * Critical invariant: this module NEVER caches resolved keys across
 * `getStripeClient()` calls without the consumer opting in. Stripe
 * encourages key rotation (Atlas docs); a cached `sk_…` outlives the
 * rotation. The default `EnvTenantConfigResolver` re-reads env every
 * call. Consumers that want a memoized cache wrap with
 * `memoizeResolver(resolver, ttlMs)`.
 */
/** Stable product identifiers — kept in sync with the product registry.
 *  Adding a product is a one-line addition; we centralize so a typo at
 *  a call site (`'legal-agent'` vs `'legal'`) is a type error. */
type ProductId = 'legal' | 'tax' | 'gtm' | 'creative' | 'agent-builder';
declare const PRODUCT_IDS: readonly ProductId[];
interface TenantStripeConfig {
    productId: ProductId;
    /** Stripe API secret key. Treat as opaque — do NOT log. */
    secretKey: string;
    /** Webhook signing secret (`whsec_...`). */
    webhookSecret: string;
    /** Optional default URLs the checkout/portal generators fall back to. */
    successUrl?: string;
    cancelUrl?: string;
    /** Free-form metadata threaded through to the product (e.g., the
     *  Connect account id if you later migrate to Connect). */
    metadata?: Record<string, string>;
}
/** Stateless resolver — called per `getStripeClient()` / `resolveSecret()`.
 *  Implementations: read from env (default), read from a vault, read
 *  from a workspace-scoped DB row (per-tenant Connect). */
interface TenantConfigResolver {
    resolve(productId: ProductId): Promise<TenantStripeConfig | null> | TenantStripeConfig | null;
}
/**
 * Reads `STRIPE_SK_<PRODUCT>` + `STRIPE_WHSEC_<PRODUCT>` from
 * `process.env`. Product id is upper-snake-cased (`agent-builder` →
 * `AGENT_BUILDER`).
 *
 * Optional defaults:
 *   STRIPE_SUCCESS_URL_<PRODUCT>
 *   STRIPE_CANCEL_URL_<PRODUCT>
 */
declare class EnvTenantConfigResolver implements TenantConfigResolver {
    private readonly env;
    constructor(env?: NodeJS.ProcessEnv);
    resolve(productId: ProductId): TenantStripeConfig | null;
}
/**
 * Static resolver — pass a hardcoded map, useful for tests and for
 * deployments that pull from a vault at boot.
 */
declare class StaticTenantConfigResolver implements TenantConfigResolver {
    private readonly table;
    constructor(table: Partial<Record<ProductId, TenantStripeConfig>>);
    resolve(productId: ProductId): TenantStripeConfig | null;
}
/**
 * Memoize a resolver with a TTL. Used in production to avoid pounding
 * a remote vault on every webhook. Default 60s — short enough that a
 * key rotation lands within the next minute.
 */
declare function memoizeResolver(inner: TenantConfigResolver, ttlMs?: number): TenantConfigResolver;
/**
 * Thin Stripe HTTP client handle. We do NOT depend on the `stripe`
 * npm package — same rationale as `stripe-pack`: keep the install
 * footprint zero, use `fetch` directly. The handle carries the
 * resolved secret + a Stripe-spec base URL so call sites can issue
 * scoped requests without re-resolving for every operation in a
 * batch.
 *
 * Idempotency-Key forwarding: every mutation MUST include an
 * `idempotency-key` header. Stripe enforces a 24h replay window
 * keyed off it. The `mutate()` helper accepts the key explicitly to
 * make it impossible to forget.
 */
interface StripeClient {
    productId: ProductId;
    config: TenantStripeConfig;
    /** GET request — returns parsed JSON or throws on non-2xx. */
    get<T = unknown>(path: string, query?: Record<string, string>): Promise<T>;
    /** Form-urlencoded POST/DELETE with idempotency. */
    mutate<T = unknown>(method: 'POST' | 'DELETE', path: string, body: Record<string, string | number | boolean | undefined>, idempotencyKey: string): Promise<T>;
}
/**
 * Look up the Stripe client for a product. Throws `ConfigError` if the
 * resolver returns null — the product agent fails its startup health
 * check and the deploy is held back. NEVER silently falls back to a
 * shared key.
 */
declare function getStripeClient(productId: ProductId, resolver: TenantConfigResolver): Promise<StripeClient>;
/** Build a client from an already-resolved config — for callers that
 *  manage resolution themselves (e.g., long-lived workers that
 *  resolved at startup). */
declare function buildStripeClient(config: TenantStripeConfig): StripeClient;
/**
 * `WebhookRouter.resolveSecret` adapter. The router calls this with
 * the provider id and headers; we extract the product id from a
 * header the gateway routes by (`x-tangle-product`) and look it up.
 *
 * Why a header and not the URL path: the router is provider-keyed,
 * not product-keyed, by design. Products inject the header in their
 * gateway layer (Hono middleware in our case). The header is
 * authenticated as part of the gateway's edge auth — Stripe's own
 * signature still has to verify against the secret we return here,
 * so a forged header alone can't bypass anything.
 */
declare function makeStripeSecretResolver(resolver: TenantConfigResolver): (providerId: string, headers: {
    [name: string]: string | string[] | undefined;
}) => Promise<string | null>;

/**
 * Pricing plan scaffold + checkout URL generator.
 *
 * Per task constraint, this module does NOT bake in pricing. The
 * consumer (product agent) supplies the `PricingPlan[]` table at boot.
 * We standardize the SHAPE (id, name, monthly/yearly USD, feature
 * bullets, stripe price ids), the LOOKUP (`findPlan`, `requirePlan`),
 * and the CHECKOUT URL flow (`createCheckoutUrl`).
 *
 * The shape is intentionally USD-only with month/year recurrence.
 * Stripe supports more — multi-currency, week/quarter, usage-based,
 * tiered — but adding columns we don't use creates pressure to fill
 * them with defaults that mislead. When a product needs more, extend
 * the shape; do not work around it in the consumer.
 *
 * `createCheckoutUrl` writes the workspaceId into
 * `subscription_data.metadata.workspaceId` so the dispatcher's default
 * `resolveWorkspaceId` finds it on the first `customer.subscription.created`
 * webhook. THIS IS LOAD-BEARING: drop it and you have to write a
 * customer → workspace join table by hand.
 */

interface PricingPlanFeature {
    /** Short label rendered in pricing table rows. */
    label: string;
    /** Optional richer description for the marketing page. */
    description?: string;
    /** Whether the feature is included in this plan. Many products show
     *  the same feature row across plans with a check/cross. */
    included: boolean;
}
interface PricingPlan {
    /** Stable internal id — used by middleware to gate features, NOT the
     *  Stripe price id. */
    id: string;
    /** Display name in the pricing table. */
    name: string;
    /** Monthly price in whole USD. `null` for plans that are yearly-only
     *  or contact-sales tiers. */
    monthlyUsd: number | null;
    /** Yearly price in whole USD. `null` for plans that don't offer
     *  annual billing. */
    yearlyUsd: number | null;
    /** Marketing feature bullets. */
    features: PricingPlanFeature[];
    /** Stripe `price_…` ids per cadence. At least one must be set if the
     *  matching `*Usd` field is non-null. */
    stripePriceIds: {
        monthly?: string;
        yearly?: string;
    };
    /** Optional trial-day grant. The dispatcher writes `trialEnd` based
     *  on Stripe's response; this field is only the request-time intent. */
    trialDays?: number;
    /** Optional metadata threaded into Stripe Subscription metadata — the
     *  product can use these for analytics or grant-feature lookup. */
    metadata?: Record<string, string>;
}
type BillingCadence = 'monthly' | 'yearly';
/** Find a plan by id. Returns null when not found. */
declare function findPlan(plans: readonly PricingPlan[], id: string): PricingPlan | null;
/** Look up a plan or throw — for code paths where missing is a bug
 *  (e.g., resolving a stored subscription's plan id back to a name). */
declare function requirePlan(plans: readonly PricingPlan[], id: string): PricingPlan;
interface CreateCheckoutUrlInput {
    /** Tenant key — written to subscription metadata for webhook routing. */
    workspaceId: string;
    /** Plan from the consumer's pricing table. */
    plan: PricingPlan;
    /** Which Stripe price id to charge against. */
    billing: BillingCadence;
    /** Optional existing Stripe customer id — pre-fills the checkout. */
    customerId?: string;
    /** Customer email — used by Stripe to pre-fill or to create a new
     *  customer if `customerId` is absent. */
    customerEmail?: string;
    /** Success/cancel URLs. Overrides the per-tenant defaults from
     *  `TenantStripeConfig.successUrl`/`cancelUrl`. */
    successUrl?: string;
    cancelUrl?: string;
    /** Idempotency key — pass a deterministic key (e.g.,
     *  `${workspaceId}:${plan.id}:${billing}`) so the same user clicking
     *  twice gets the same checkout session. */
    idempotencyKey: string;
    /** Trial override — if set, beats `plan.trialDays`. */
    trialDays?: number;
    /** Optional extra metadata mixed into Stripe metadata. */
    metadata?: Record<string, string>;
}
interface CheckoutUrl {
    sessionId: string;
    url: string;
}
/**
 * Create a Stripe checkout session and return its hosted URL. Uses the
 * per-tenant `StripeClient` from `getStripeClient(productId)`.
 *
 * The workspaceId is written into TWO metadata maps:
 *   - `metadata` (on the session itself, surfaces on `checkout.session.*`)
 *   - `subscription_data[metadata]` (carries through to the Subscription
 *     row Stripe creates, which is what `customer.subscription.*`
 *     webhooks carry)
 *
 * Without the second, the dispatcher can't route the first
 * `subscription.created` event to a workspace. We've shipped that bug
 * before — written here once to make it impossible to forget.
 */
declare function createCheckoutUrl(client: StripeClient, input: CreateCheckoutUrlInput): Promise<CheckoutUrl>;
/**
 * Create a Stripe customer-billing-portal session and return its URL.
 * The product calls this when a user clicks "manage billing" — the
 * portal handles cancel / change plan / update card without us
 * implementing those flows.
 */
declare function createBillingPortalUrl(client: StripeClient, input: {
    customerId: string;
    returnUrl: string;
    idempotencyKey: string;
}): Promise<{
    sessionId: string;
    url: string;
}>;

/**
 * Drop-in middleware for product agents.
 *
 * Three primitives consumers wire into their HTTP layer (Hono, Express,
 * raw Workers `fetch` handler — middleware here is framework-neutral,
 * returns a `BillingGate` value the consumer chooses how to respond to).
 *
 *   requireActiveSubscription({ workspaceId, store })
 *     → 'allow' | { allowed: false, error: BillingError }
 *
 *   withTrialAccess({ workspaceId, days, trialStore })
 *     → allow while trial < days expired since workspace creation
 *
 *   getRemainingFreeTier({ workspaceId, freeTierStore })
 *     → { remaining: number, total: number }
 *
 * Frameworks: we don't import Hono / Express. The middleware shape is a
 * pure async function returning a decision. The product wires it into
 * its framework with a 3-line adapter (see `examples/hono.ts`).
 *
 * Past-due policy: by default `requireActiveSubscription` allows
 * `past_due` (the dunning grace window — see `gateAccess` in
 * `subscription-state.ts`). Pass `denyPastDue: true` to override
 * per-route (e.g., legal-agent's "file new petition" gate where
 * irreversible actions justify a stricter rule).
 */

interface RequireActiveSubscriptionInput {
    workspaceId: string;
    store: SubscriptionStore;
    /** Strict mode: reject `past_due`. Default false (allow with warn). */
    denyPastDue?: boolean;
}
type SubscriptionGateResult = {
    allowed: true;
    record: SubscriptionRecord;
    warn?: 'past_due' | 'trial_ending';
} | {
    allowed: false;
    error: BillingError;
};
/**
 * Gate decision for a route that requires an active subscription.
 *
 * Returns `{ allowed: true }` on `active` / `trialing` and on
 * `past_due` (unless `denyPastDue`). Returns `{ allowed: false, error }`
 * with a typed `BillingError` for any other state — the consumer maps
 * the error's `status` to the HTTP response.
 */
declare function requireActiveSubscription(input: RequireActiveSubscriptionInput): Promise<SubscriptionGateResult>;
/** Workspace creation timestamp store — required by `withTrialAccess`. */
interface TrialStore {
    /** Returns workspace creation timestamp (ms epoch), or null if the
     *  workspace doesn't exist yet. */
    getCreatedAt(workspaceId: string): Promise<number | null> | number | null;
}
interface WithTrialAccessInput {
    workspaceId: string;
    /** Trial length in days from workspace creation. */
    days: number;
    trialStore: TrialStore;
    /** Optional `now` override for tests. */
    now?: () => number;
}
interface TrialAccessResult {
    /** Whether the workspace is still inside its free-trial window. */
    inTrial: boolean;
    /** Days remaining (rounded down). Zero when `inTrial` is false. */
    daysRemaining: number;
    /** Trial end timestamp (ms epoch), null when no workspace found. */
    trialEndsAt: number | null;
}
/**
 * Free-trial gate independent of Stripe state. Use BEFORE a workspace
 * has a Stripe subscription (the product's onboarding period). Compose
 * with `requireActiveSubscription`: trial OR active sub passes the gate.
 *
 * Composition pattern:
 *
 *   const trial = await withTrialAccess(...)
 *   if (trial.inTrial) return next()
 *   const sub = await requireActiveSubscription(...)
 *   if (sub.allowed) return next()
 *   return respond(sub.error)
 */
declare function withTrialAccess(input: WithTrialAccessInput): Promise<TrialAccessResult>;
/** Free-tier counter store — abstract over the consumer's metering
 *  pipeline. The interface is read-only; products own counter increment
 *  on usage (e.g., increment on every API call in their own metrics
 *  layer). */
interface FreeTierStore {
    /** Returns `{ used, total }` for the workspace. Implementations
     *  return `{ used: 0, total: <default> }` for unknown workspaces if
     *  the product wants implicit free-tier grant. */
    getUsage(workspaceId: string): Promise<{
        used: number;
        total: number;
    }> | {
        used: number;
        total: number;
    };
}
interface GetRemainingFreeTierInput {
    workspaceId: string;
    freeTierStore: FreeTierStore;
}
interface FreeTierResult {
    /** Units (whatever the product counts: API calls, tokens, generations) still allowed. */
    remaining: number;
    /** Total quota. */
    total: number;
    /** Whether the quota is exhausted. */
    exhausted: boolean;
}
/**
 * Return how much free-tier quota the workspace has left. Pure projection
 * over the store; consumers use the result to decide whether to grant the
 * route or return `BillingError(code: 'free_tier_exhausted')`.
 *
 * Why this isn't a gate function itself: free-tier "exhausted" is rarely
 * a hard deny — most products throttle, queue, or upsell instead. The
 * decision is product-specific; we provide the read and the typed error
 * but stop short of opining on the response shape.
 */
declare function getRemainingFreeTier(input: GetRemainingFreeTierInput): Promise<FreeTierResult>;
interface ComposedGateInput {
    workspaceId: string;
    store: SubscriptionStore;
    trialStore?: TrialStore;
    trialDays?: number;
    denyPastDue?: boolean;
    now?: () => number;
}
/**
 * Compose `withTrialAccess` || `requireActiveSubscription`. Most product
 * routes want this exact combo — passes if EITHER the workspace is
 * inside its free trial OR has an active subscription. Returns the
 * subscription error from `requireActiveSubscription` when both fail
 * (the more actionable of the two — the customer can convert it into
 * a checkout).
 */
declare function gateSubscriptionOrTrial(input: ComposedGateInput): Promise<SubscriptionGateResult & {
    viaTrial?: boolean;
    daysRemaining?: number;
}>;

export { type AccessDecision, type BillingCadence, BillingError, type BillingErrorCode, type BillingErrorContext, type CheckoutUrl, type ComposedGateInput, ConfigError, type CreateCheckoutUrlInput, EnvTenantConfigResolver, FileSystemSubscriptionStore, type FreeTierResult, type FreeTierStore, type GetRemainingFreeTierInput, InMemorySubscriptionStore, PRODUCT_IDS, type PricingPlan, type PricingPlanFeature, type ProductId, type RequireActiveSubscriptionInput, SUBSCRIPTION_STATES, StaticTenantConfigResolver, StripeBillingDispatcher, type StripeBillingDispatcherOptions, type StripeBillingEvent, type StripeBillingListener, type StripeClient, type SubscriptionGateResult, type SubscriptionRecord, type SubscriptionState, type SubscriptionStore, type TenantConfigResolver, type TenantStripeConfig, type TrialAccessResult, type TrialStore, type WithTrialAccessInput, applyTransition, buildStripeClient, combineListeners, createBillingPortalUrl, createCheckoutUrl, findPlan, gateAccess, gateSubscriptionOrTrial, getRemainingFreeTier, getStripeClient, isValidTransition, makeStripeSecretResolver, makeSubscriptionRecord, memoizeResolver, requireActiveSubscription, requirePlan, withTrialAccess };
