@tangle-network/agent-integrations 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/stripe/errors.ts","../../src/stripe/subscription-state.ts","../../src/stripe/webhooks.ts","../../src/stripe/pricing.ts","../../src/stripe/tenant-config.ts","../../src/stripe/middleware.ts"],"sourcesContent":["/**\n * Stripe billing error taxonomy.\n *\n * Layered on top of `IntegrationRuntimeError` (the cross-package error\n * contract) so a billing failure surfaces through the same normalization\n * pipeline the rest of the integration runtime uses: `status` maps to\n * an HTTP status, `userAction` carries the recommended next step\n * (connect / reconnect / contact_support), and `code` is stable across\n * versions for product analytics.\n *\n * Why a billing-specific subclass and not bare `IntegrationRuntimeError`:\n * three error classes carry a payload product agents NEED to branch on\n * (subscription state, tenant id, plan id). Encoding them as discriminated\n * subclasses keeps the call site `instanceof BillingError` lookup O(1)\n * and avoids stuffing them into `metadata` where they decay to `unknown`.\n *\n * Code mapping (`BillingErrorCode` → `IntegrationErrorCode`):\n * subscription_required → action_denied (403)\n * subscription_inactive → action_denied (403)\n * subscription_past_due → action_denied (403, w/ warning)\n * trial_expired → action_denied (403)\n * free_tier_exhausted → action_denied (403)\n * tenant_not_configured → provider_error (500) — operator bug\n * webhook_secret_missing→ provider_auth_failed (401) — config bug\n * webhook_event_unknown → input_invalid (400)\n * webhook_replay → input_invalid (400)\n */\n\nimport { IntegrationRuntimeError, type IntegrationUserAction } from '../errors.js'\nimport type { SubscriptionState } from './subscription-state.js'\n\nexport type BillingErrorCode =\n | 'subscription_required'\n | 'subscription_inactive'\n | 'subscription_past_due'\n | 'trial_expired'\n | 'free_tier_exhausted'\n | 'tenant_not_configured'\n | 'webhook_secret_missing'\n | 'webhook_event_unknown'\n | 'webhook_replay'\n\nexport interface BillingErrorContext {\n workspaceId?: string\n productId?: string\n subscriptionId?: string\n subscriptionState?: SubscriptionState\n planId?: string\n eventId?: string\n}\n\nexport class BillingError extends IntegrationRuntimeError {\n readonly billingCode: BillingErrorCode\n readonly context: BillingErrorContext\n\n constructor(input: {\n code: BillingErrorCode\n message: string\n context?: BillingErrorContext\n userAction?: IntegrationUserAction\n }) {\n super({\n code: mapToIntegrationCode(input.code),\n message: input.message,\n status: statusForBillingCode(input.code),\n userAction: input.userAction ?? defaultUserAction(input.code),\n metadata: input.context as Record<string, unknown> | undefined,\n })\n this.name = 'BillingError'\n this.billingCode = input.code\n this.context = input.context ?? {}\n }\n}\n\n/** Distinct subclass: operator missed an environment variable. Surfaces\n * with a different `userAction` (`contact_support`) so the consumer\n * doesn't render a \"connect Stripe\" CTA to a customer for what is in\n * fact a backend deploy bug. */\nexport class ConfigError extends BillingError {\n constructor(input: { message: string; context?: BillingErrorContext }) {\n super({\n code: 'tenant_not_configured',\n message: input.message,\n context: input.context,\n userAction: { type: 'contact_support', label: 'Contact support' },\n })\n this.name = 'ConfigError'\n }\n}\n\nfunction mapToIntegrationCode(code: BillingErrorCode): IntegrationRuntimeError['code'] {\n switch (code) {\n case 'subscription_required':\n case 'subscription_inactive':\n case 'subscription_past_due':\n case 'trial_expired':\n case 'free_tier_exhausted':\n return 'action_denied'\n case 'tenant_not_configured':\n return 'provider_error'\n case 'webhook_secret_missing':\n return 'provider_auth_failed'\n case 'webhook_event_unknown':\n case 'webhook_replay':\n return 'input_invalid'\n }\n}\n\nfunction statusForBillingCode(code: BillingErrorCode): number {\n switch (code) {\n case 'subscription_required':\n case 'subscription_inactive':\n case 'subscription_past_due':\n case 'trial_expired':\n case 'free_tier_exhausted':\n return 403\n case 'tenant_not_configured':\n return 500\n case 'webhook_secret_missing':\n return 401\n case 'webhook_event_unknown':\n case 'webhook_replay':\n return 400\n }\n}\n\nfunction defaultUserAction(code: BillingErrorCode): IntegrationUserAction | undefined {\n switch (code) {\n case 'subscription_required':\n return { type: 'change_request', label: 'Subscribe to continue' }\n case 'subscription_inactive':\n case 'subscription_past_due':\n return { type: 'change_request', label: 'Update billing' }\n case 'trial_expired':\n return { type: 'change_request', label: 'Choose a plan' }\n case 'free_tier_exhausted':\n return { type: 'change_request', label: 'Upgrade for more usage' }\n case 'tenant_not_configured':\n case 'webhook_secret_missing':\n return { type: 'contact_support', label: 'Contact support' }\n case 'webhook_event_unknown':\n case 'webhook_replay':\n return undefined\n }\n}\n","/**\n * Subscription state machine.\n *\n * Stripe ships eight terminal states on `Subscription.status`. We model\n * them verbatim — never normalize, never collapse — so the state\n * persisted in product DBs round-trips Stripe webhooks losslessly.\n *\n * incomplete — first invoice not paid within 23 hours\n * incomplete_expired — first invoice failed, no retry coming\n * trialing — inside a trial window (treat as active)\n * active — paying, current\n * past_due — auto-renewal failed; grace period running\n * canceled — terminal; ended at period boundary or hard\n * unpaid — past_due → unpaid after retries exhausted\n * paused — operator-paused (collection_method=pause_collection)\n *\n * Transition rules below derive STRICTLY from the Stripe state diagram\n * (https://docs.stripe.com/billing/subscriptions/overview#subscription-statuses)\n * — the dispatcher in `webhooks.ts` calls `applyTransition()` which\n * rejects any state pair Stripe never emits. This catches manual-edit\n * bugs (someone POSTing a `force_state` admin endpoint) and tests for\n * the consumer's state store at the same time.\n *\n * Persistence: products pick an adapter (FS, D1, Postgres, in-memory).\n * The interface is intentionally minimal — load(), save(), and a\n * compare-and-set `saveIfVersion()` that defends against duplicate\n * webhook delivery racing the same store. Stripe re-delivers failed\n * webhooks for 3 days; the in-flight one and the retry will both write\n * the same key. `WebhookRouter`'s idempotency hook short-circuits at\n * the event level, but a misconfigured deploy with two routers in\n * different regions both processing the same event needs the second\n * line of defense here.\n *\n * `requireActiveSubscription()` (middleware) calls `gateAccess(state)`\n * to map state → access-decision. `past_due` is intentionally allowed\n * with a warning flag — the dunning period is when products MOST need\n * customers to keep using the product so they remember why they pay,\n * but the UI should render the \"card declined\" banner.\n */\n\nimport { BillingError } from './errors.js'\n\nexport type SubscriptionState =\n | 'incomplete'\n | 'incomplete_expired'\n | 'trialing'\n | 'active'\n | 'past_due'\n | 'canceled'\n | 'unpaid'\n | 'paused'\n\n/** All eight states, exported so tests + consumers can enumerate. */\nexport const SUBSCRIPTION_STATES: readonly SubscriptionState[] = Object.freeze([\n 'incomplete',\n 'incomplete_expired',\n 'trialing',\n 'active',\n 'past_due',\n 'canceled',\n 'unpaid',\n 'paused',\n])\n\n/** Tristate access decision. `warn` means the route runs, but the UI\n * should render a billing banner. */\nexport type AccessDecision =\n | { allowed: true; warn?: 'past_due' | 'trial_ending' }\n | { allowed: false; reason: 'no_subscription' | 'subscription_inactive' | 'subscription_past_due' | 'trial_expired' }\n\nexport interface SubscriptionRecord {\n /** Tenant key the product uses to look up \"is this workspace paying?\" —\n * typically a workspaceId, but products that bill per-user or\n * per-organization can swap in their own scope. */\n workspaceId: string\n /** Stripe customer id (`cus_...`). */\n customerId: string\n /** Stripe subscription id (`sub_...`). */\n subscriptionId: string\n /** Last-known subscription state — updated by webhook handlers. */\n state: SubscriptionState\n /** Stripe price id active on the subscription. Null for canceled. */\n priceId: string | null\n /** Current billing period end (unix seconds). Used by middleware to\n * emit `trial_ending` warning in the last 72h of a trial. */\n currentPeriodEnd: number | null\n /** Trial end (unix seconds), null for non-trial subs. */\n trialEnd: number | null\n /** `cancel_at_period_end` flag — once true, state stays `active` until\n * the period ends, then transitions to `canceled`. */\n cancelAtPeriodEnd: boolean\n /** Monotonic write counter for optimistic concurrency. Incremented on\n * every save; persistence adapters use it for CAS. */\n version: number\n /** Last event id we processed for this subscription — defends against\n * Stripe re-delivering the same event and us racing the dedupe store. */\n lastEventId: string | null\n /** Wall-clock ms of last successful write. */\n updatedAt: number\n}\n\n/** Persistence adapter contract. Three operations — pick the storage\n * layer that matches the product's existing infra. Adapters live below\n * (in-memory + filesystem). D1 / Postgres are one-liners on top. */\nexport interface SubscriptionStore {\n load(workspaceId: string): Promise<SubscriptionRecord | null>\n save(record: SubscriptionRecord): Promise<void>\n /** Compare-and-set on `version`. Returns false if the stored record's\n * version doesn't match `expectedVersion` (someone else wrote first).\n * Implementations MUST return false rather than throw on contention —\n * the caller branches on the bool. */\n saveIfVersion(record: SubscriptionRecord, expectedVersion: number): Promise<boolean>\n}\n\n/** Transitions table — adjacency map of legal {from → to} edges derived\n * from Stripe's subscription status diagram. Used by `applyTransition`. */\nconst TRANSITIONS: Readonly<Record<SubscriptionState, ReadonlySet<SubscriptionState>>> = Object.freeze({\n incomplete: new Set<SubscriptionState>(['active', 'trialing', 'incomplete_expired', 'canceled']),\n incomplete_expired: new Set<SubscriptionState>([]),\n trialing: new Set<SubscriptionState>(['active', 'past_due', 'canceled', 'paused', 'unpaid']),\n active: new Set<SubscriptionState>(['past_due', 'canceled', 'paused', 'unpaid', 'trialing']),\n past_due: new Set<SubscriptionState>(['active', 'canceled', 'unpaid', 'paused']),\n canceled: new Set<SubscriptionState>([]),\n unpaid: new Set<SubscriptionState>(['active', 'canceled', 'past_due']),\n paused: new Set<SubscriptionState>(['active', 'canceled', 'past_due']),\n})\n\n/** Returns true if `to` is reachable from `from` in one Stripe transition.\n * Self-edges are accepted (a webhook can re-emit the current state on\n * any field change). */\nexport function isValidTransition(from: SubscriptionState, to: SubscriptionState): boolean {\n if (from === to) return true\n return TRANSITIONS[from].has(to)\n}\n\n/** Apply a state transition to a record. Throws `BillingError` if Stripe\n * would never emit this edge (defensive — a bad admin tool POSTing a\n * raw state update gets refused). Returns the new record without writing. */\nexport function applyTransition(\n current: SubscriptionRecord,\n next: Partial<SubscriptionRecord> & { state: SubscriptionState },\n options: { eventId?: string; now?: () => number } = {},\n): SubscriptionRecord {\n if (!isValidTransition(current.state, next.state)) {\n throw new BillingError({\n code: 'webhook_event_unknown',\n message: `Illegal subscription transition ${current.state} → ${next.state}`,\n context: {\n workspaceId: current.workspaceId,\n subscriptionId: current.subscriptionId,\n subscriptionState: current.state,\n eventId: options.eventId,\n },\n })\n }\n const now = (options.now ?? Date.now)()\n return {\n ...current,\n ...next,\n version: current.version + 1,\n lastEventId: options.eventId ?? current.lastEventId,\n updatedAt: now,\n }\n}\n\n/**\n * Map a state to an access decision.\n *\n * Rule rationale:\n * active, trialing → allow\n * past_due → allow + warn (dunning grace)\n * paused → deny (operator action; resume restores)\n * canceled, unpaid → deny (terminal financial states)\n * incomplete, incomplete_expired → deny (never paid; first invoice failed)\n *\n * Note `requireActiveSubscription` in `middleware.ts` is the consumer of\n * this — gating is centralized here so the rule lives in one place. The\n * mapping is one assertion in the test suite. Changing the rule for one\n * product (e.g., legal-agent wants past_due to deny) is a per-call\n * `overrides` option on the middleware, NOT a fork of this function.\n */\nexport function gateAccess(state: SubscriptionState): AccessDecision {\n switch (state) {\n case 'active':\n case 'trialing':\n return { allowed: true }\n case 'past_due':\n return { allowed: true, warn: 'past_due' }\n case 'paused':\n return { allowed: false, reason: 'subscription_past_due' }\n case 'canceled':\n case 'unpaid':\n return { allowed: false, reason: 'subscription_inactive' }\n case 'incomplete':\n case 'incomplete_expired':\n return { allowed: false, reason: 'subscription_inactive' }\n }\n}\n\n/* ---------------------------------------------------------------------- */\n/* persistence adapter: in-memory */\n/* ---------------------------------------------------------------------- */\n\n/**\n * Process-local store. Useful for tests; product instances should pick\n * `FileSystemSubscriptionStore` or wire D1 / Postgres. Implements proper\n * CAS — concurrent saves with stale versions are rejected.\n */\nexport class InMemorySubscriptionStore implements SubscriptionStore {\n private readonly records = new Map<string, SubscriptionRecord>()\n\n async load(workspaceId: string): Promise<SubscriptionRecord | null> {\n const r = this.records.get(workspaceId)\n return r ? { ...r } : null\n }\n\n async save(record: SubscriptionRecord): Promise<void> {\n this.records.set(record.workspaceId, { ...record })\n }\n\n async saveIfVersion(record: SubscriptionRecord, expectedVersion: number): Promise<boolean> {\n const current = this.records.get(record.workspaceId)\n if (current && current.version !== expectedVersion) return false\n if (!current && expectedVersion !== 0) return false\n this.records.set(record.workspaceId, { ...record })\n return true\n }\n}\n\n/* ---------------------------------------------------------------------- */\n/* persistence adapter: filesystem (JSONL) */\n/* ---------------------------------------------------------------------- */\n\n/**\n * File-per-workspace JSON store. One file per workspace under\n * `<rootDir>/<workspaceId>.json`. Cheap, durable, debuggable — adequate\n * for self-hosted product agents. CAS is implemented via the version\n * field plus a write that re-reads the file under a brief lock window\n * (rename-temp-to-target pattern, atomic on POSIX).\n *\n * Why per-file and not one JSONL: subscriptions are\n * accessed by workspaceId 99% of the time, scanning a JSONL on every\n * request burns I/O. The file-per-workspace pattern keeps reads O(1).\n *\n * The store does NOT use `fs.watch` — webhooks are the only writer in\n * production, and webhooks always go through `applyTransition()` →\n * `saveIfVersion()`, so the CAS catches the race.\n */\nexport class FileSystemSubscriptionStore implements SubscriptionStore {\n constructor(private readonly rootDir: string) {}\n\n async load(workspaceId: string): Promise<SubscriptionRecord | null> {\n const fs = await import('node:fs/promises')\n const path = await import('node:path')\n const file = path.join(this.rootDir, this.fileName(workspaceId))\n try {\n const raw = await fs.readFile(file, 'utf-8')\n return JSON.parse(raw) as SubscriptionRecord\n } catch (err) {\n if (isNodeENOENT(err)) return null\n throw err\n }\n }\n\n async save(record: SubscriptionRecord): Promise<void> {\n const fs = await import('node:fs/promises')\n const path = await import('node:path')\n await fs.mkdir(this.rootDir, { recursive: true })\n const file = path.join(this.rootDir, this.fileName(record.workspaceId))\n const tmp = `${file}.tmp-${process.pid}-${Date.now()}`\n await fs.writeFile(tmp, JSON.stringify(record), 'utf-8')\n await fs.rename(tmp, file)\n }\n\n async saveIfVersion(record: SubscriptionRecord, expectedVersion: number): Promise<boolean> {\n const existing = await this.load(record.workspaceId)\n if (existing && existing.version !== expectedVersion) return false\n if (!existing && expectedVersion !== 0) return false\n await this.save(record)\n return true\n }\n\n /** Safe filename: workspaceId is restricted to a charset that maps 1:1\n * to a posix filename. Anything outside is hex-encoded so we can never\n * escape the rootDir via `../`. */\n private fileName(workspaceId: string): string {\n if (!/^[A-Za-z0-9_.-]+$/.test(workspaceId)) {\n return `${Buffer.from(workspaceId, 'utf-8').toString('hex')}.json`\n }\n return `${workspaceId}.json`\n }\n}\n\nfunction isNodeENOENT(err: unknown): boolean {\n return !!err && typeof err === 'object' && (err as { code?: string }).code === 'ENOENT'\n}\n\n/* ---------------------------------------------------------------------- */\n/* construction helper */\n/* ---------------------------------------------------------------------- */\n\n/** Convenience constructor for the initial record after a checkout\n * succeeds. The webhook handler for `customer.subscription.created` calls\n * this — exposed for tests + manual-fix scripts that need to backfill. */\nexport function makeSubscriptionRecord(input: {\n workspaceId: string\n customerId: string\n subscriptionId: string\n state: SubscriptionState\n priceId: string | null\n currentPeriodEnd: number | null\n trialEnd?: number | null\n cancelAtPeriodEnd?: boolean\n now?: () => number\n}): SubscriptionRecord {\n const now = (input.now ?? Date.now)()\n return {\n workspaceId: input.workspaceId,\n customerId: input.customerId,\n subscriptionId: input.subscriptionId,\n state: input.state,\n priceId: input.priceId,\n currentPeriodEnd: input.currentPeriodEnd,\n trialEnd: input.trialEnd ?? null,\n cancelAtPeriodEnd: input.cancelAtPeriodEnd ?? false,\n version: 0,\n lastEventId: null,\n updatedAt: now,\n }\n}\n","/**\n * Stripe subscription webhook dispatcher.\n *\n * Receives `WebhookEnvelope` rows from `WebhookRouter`'s `deliver()`\n * callback, decodes them into typed `StripeBillingEvent` values, and\n * applies the corresponding state transition to the consumer's\n * `SubscriptionStore`. Emits a typed event the consumer subscribes to.\n *\n * Layering:\n *\n * Stripe → HTTP → WebhookRouter (verify + idempotency dedup)\n * ↓ deliver(envelope)\n * StripeBillingDispatcher (this file)\n * ↓\n * ├─ SubscriptionStore.saveIfVersion(...)\n * └─ emit(typed event) → consumer's subscriber\n *\n * Critical guarantees:\n *\n * 1. Idempotency at two layers — the router de-dupes at the event id;\n * the dispatcher's `saveIfVersion` defends against the second\n * router instance (multi-region) racing the same event. The\n * consumer's subscriber sees an event AT MOST ONCE per `eventId`.\n *\n * 2. Order-independence — Stripe doesn't guarantee delivery order.\n * We process events whose `event.created` timestamp is older than\n * the stored `updatedAt` only when the resulting state would be a\n * valid transition; otherwise we drop with `dropped:'out_of_order'`.\n *\n * 3. Explicit unknown handling — events we don't have a handler for\n * are not dropped silently; we emit them as\n * `stripe.event_unhandled` so the consumer can log + alert if\n * they expected coverage that we don't ship.\n *\n * 4. Idempotency of the dispatcher itself — calling `dispatch()` with\n * an event whose id equals `lastEventId` on the loaded record is a\n * no-op that emits `stripe.event_replay` instead of advancing state.\n *\n * Events supported (8 critical + 2 lifecycle):\n *\n * customer.subscription.created\n * customer.subscription.updated\n * customer.subscription.deleted\n * customer.subscription.trial_will_end\n * customer.subscription.paused\n * customer.subscription.resumed\n * invoice.paid\n * invoice.payment_failed\n */\n\nimport type { WebhookEnvelope } from '../webhooks/router.js'\nimport { BillingError } from './errors.js'\nimport {\n applyTransition,\n isValidTransition,\n makeSubscriptionRecord,\n type SubscriptionRecord,\n type SubscriptionState,\n type SubscriptionStore,\n} from './subscription-state.js'\n\n/* ---------------------------------------------------------------------- */\n/* Stripe payload type subset */\n/* ---------------------------------------------------------------------- */\n\n/** Subset of Stripe's `Subscription` object we read. Keep narrow — the\n * full object has 70+ fields; we only need the ones that map to our\n * `SubscriptionRecord`. New fields land here on demand. */\ninterface StripeSubscriptionPayload {\n id: string\n status: string\n customer: string\n current_period_end?: number | null\n cancel_at_period_end?: boolean | null\n trial_end?: number | null\n items?: {\n data?: Array<{ price?: { id?: string } }>\n }\n /** Stripe `Subscription.metadata` — agents put their `workspaceId`\n * here at checkout time so we can route subsequent webhooks back to\n * the right tenant without an extra DB lookup. */\n metadata?: Record<string, string>\n}\n\ninterface StripeInvoicePayload {\n id: string\n subscription?: string | null\n customer?: string\n status?: string\n /** Cents. */\n amount_paid?: number\n amount_due?: number\n /** Lined up to subscription metadata at invoice generation. */\n metadata?: Record<string, string>\n}\n\ninterface StripeEvent {\n id: string\n type: string\n created?: number\n data: { object: unknown }\n}\n\n/* ---------------------------------------------------------------------- */\n/* dispatcher contract */\n/* ---------------------------------------------------------------------- */\n\n/** Strongly-typed events the consumer can subscribe to. Each carries\n * enough context to drive downstream side effects without a second\n * DB round-trip (audit log row, Slack ping, in-app notification). */\nexport type StripeBillingEvent =\n | {\n kind: 'subscription.created'\n eventId: string\n record: SubscriptionRecord\n }\n | {\n kind: 'subscription.updated'\n eventId: string\n previousState: SubscriptionState\n record: SubscriptionRecord\n }\n | {\n kind: 'subscription.deleted'\n eventId: string\n record: SubscriptionRecord\n }\n | {\n kind: 'subscription.trial_will_end'\n eventId: string\n record: SubscriptionRecord\n trialEndsAt: number\n }\n | {\n kind: 'subscription.paused'\n eventId: string\n record: SubscriptionRecord\n }\n | {\n kind: 'subscription.resumed'\n eventId: string\n record: SubscriptionRecord\n }\n | {\n kind: 'invoice.paid'\n eventId: string\n record: SubscriptionRecord | null\n invoiceId: string\n amountPaid: number\n }\n | {\n kind: 'invoice.payment_failed'\n eventId: string\n record: SubscriptionRecord | null\n invoiceId: string\n amountDue: number\n }\n | {\n kind: 'event_unhandled'\n eventId: string\n type: string\n }\n | {\n kind: 'event_replay'\n eventId: string\n type: string\n }\n | {\n kind: 'event_dropped_out_of_order'\n eventId: string\n type: string\n reason: string\n }\n\n/** Listener — the product agent wires this to whatever side-effect bus\n * it owns (audit log, in-process emitter, durable queue). Throws are\n * caught by `dispatch()` and surfaced through `onError`. */\nexport type StripeBillingListener = (event: StripeBillingEvent) => void | Promise<void>\n\nexport interface StripeBillingDispatcherOptions {\n store: SubscriptionStore\n /** Maps a Stripe `customer.id` → the workspaceId the product uses to\n * key its `SubscriptionStore`. We default to reading\n * `subscription.metadata.workspaceId` (agents inject it at checkout\n * time); supply this override for products that key by customer id\n * directly or look up a join table. */\n resolveWorkspaceId?(input: {\n customerId: string\n subscriptionMetadata?: Record<string, string>\n invoiceMetadata?: Record<string, string>\n }): Promise<string | null> | string | null\n /** Single typed listener (most consumers want one — they route inside\n * it themselves). Compose multiple via `combineListeners(a, b)`. */\n listener?: StripeBillingListener\n /** Surface unexpected dispatcher errors (validation, store contention\n * exhausted) without crashing the webhook handler. */\n onError?(err: unknown, context: { eventId: string; type: string }): void\n /** Override `Date.now()` for tests. */\n now?(): number\n /** Max retries on `saveIfVersion` contention. Default 3. */\n maxCasRetries?: number\n}\n\n/* ---------------------------------------------------------------------- */\n/* dispatcher */\n/* ---------------------------------------------------------------------- */\n\n/**\n * Process a webhook envelope. Safe to call concurrently with itself —\n * the in-store CAS serializes per-workspace updates.\n */\nexport class StripeBillingDispatcher {\n private readonly store: SubscriptionStore\n private readonly resolveWorkspaceId: NonNullable<StripeBillingDispatcherOptions['resolveWorkspaceId']>\n private readonly listener?: StripeBillingListener\n private readonly onError: NonNullable<StripeBillingDispatcherOptions['onError']>\n private readonly now: () => number\n private readonly maxCasRetries: number\n\n constructor(opts: StripeBillingDispatcherOptions) {\n this.store = opts.store\n this.resolveWorkspaceId = opts.resolveWorkspaceId ?? defaultResolveWorkspaceId\n this.listener = opts.listener\n this.onError = opts.onError ?? defaultOnError\n this.now = opts.now ?? Date.now\n this.maxCasRetries = opts.maxCasRetries ?? 3\n }\n\n /** Drive one envelope through the pipeline. Idempotent w.r.t. the\n * event id (replays are a no-op + emit `event_replay`). */\n async dispatch(envelope: WebhookEnvelope): Promise<void> {\n const evt = envelope.payload as StripeEvent | undefined\n if (!evt || typeof evt !== 'object' || typeof evt.id !== 'string' || typeof evt.type !== 'string') {\n this.onError(new Error('Stripe envelope missing id or type'), {\n eventId: 'unknown',\n type: 'unknown',\n })\n return\n }\n try {\n await this.handle(evt)\n } catch (err) {\n this.onError(err, { eventId: evt.id, type: evt.type })\n }\n }\n\n private async handle(evt: StripeEvent): Promise<void> {\n switch (evt.type) {\n case 'customer.subscription.created':\n return this.handleSubCreated(evt)\n case 'customer.subscription.updated':\n return this.handleSubUpdated(evt)\n case 'customer.subscription.deleted':\n return this.handleSubDeleted(evt)\n case 'customer.subscription.trial_will_end':\n return this.handleTrialWillEnd(evt)\n case 'customer.subscription.paused':\n return this.handleSubLifecycle(evt, 'paused')\n case 'customer.subscription.resumed':\n return this.handleSubLifecycle(evt, 'active')\n case 'invoice.paid':\n return this.handleInvoicePaid(evt)\n case 'invoice.payment_failed':\n return this.handleInvoicePaymentFailed(evt)\n default:\n await this.emit({ kind: 'event_unhandled', eventId: evt.id, type: evt.type })\n return\n }\n }\n\n /* --------------------- subscription event handlers ------------------- */\n\n private async handleSubCreated(evt: StripeEvent): Promise<void> {\n const sub = evt.data.object as StripeSubscriptionPayload\n const workspaceId = await this.resolveWorkspaceId({\n customerId: sub.customer,\n subscriptionMetadata: sub.metadata,\n })\n if (!workspaceId) return this.emitNoWorkspace(evt)\n\n const existing = await this.store.load(workspaceId)\n if (existing && existing.lastEventId === evt.id) {\n return this.emit({ kind: 'event_replay', eventId: evt.id, type: evt.type })\n }\n\n // Create-only: if a record already exists with a non-incomplete state\n // and this is a stale created event, treat as out-of-order.\n if (existing && !canApplyFreshCreate(existing.state)) {\n return this.emit({\n kind: 'event_dropped_out_of_order',\n eventId: evt.id,\n type: evt.type,\n reason: `existing state ${existing.state} cannot accept a fresh 'created'`,\n })\n }\n\n const record = makeSubscriptionRecord({\n workspaceId,\n customerId: sub.customer,\n subscriptionId: sub.id,\n state: parseState(sub.status, evt.id),\n priceId: extractPriceId(sub),\n currentPeriodEnd: sub.current_period_end ?? null,\n trialEnd: sub.trial_end ?? null,\n cancelAtPeriodEnd: sub.cancel_at_period_end ?? false,\n now: this.now,\n })\n const stamped: SubscriptionRecord = { ...record, lastEventId: evt.id }\n const expectedVersion = existing?.version ?? 0\n const written = await this.cas(stamped, expectedVersion)\n if (!written) return\n await this.emit({ kind: 'subscription.created', eventId: evt.id, record: stamped })\n }\n\n private async handleSubUpdated(evt: StripeEvent): Promise<void> {\n const sub = evt.data.object as StripeSubscriptionPayload\n const workspaceId = await this.resolveWorkspaceId({\n customerId: sub.customer,\n subscriptionMetadata: sub.metadata,\n })\n if (!workspaceId) return this.emitNoWorkspace(evt)\n const nextState = parseState(sub.status, evt.id)\n\n await this.advance(evt, workspaceId, (current) => {\n if (current.lastEventId === evt.id) return 'replay'\n if (!isValidTransition(current.state, nextState)) return 'out_of_order'\n const next = applyTransition(\n current,\n {\n state: nextState,\n priceId: extractPriceId(sub) ?? current.priceId,\n currentPeriodEnd: sub.current_period_end ?? current.currentPeriodEnd,\n trialEnd: sub.trial_end ?? current.trialEnd,\n cancelAtPeriodEnd: sub.cancel_at_period_end ?? current.cancelAtPeriodEnd,\n },\n { eventId: evt.id, now: this.now },\n )\n return {\n next,\n emit: { kind: 'subscription.updated', eventId: evt.id, previousState: current.state, record: next },\n }\n })\n }\n\n private async handleSubDeleted(evt: StripeEvent): Promise<void> {\n const sub = evt.data.object as StripeSubscriptionPayload\n const workspaceId = await this.resolveWorkspaceId({\n customerId: sub.customer,\n subscriptionMetadata: sub.metadata,\n })\n if (!workspaceId) return this.emitNoWorkspace(evt)\n\n await this.advance(evt, workspaceId, (current) => {\n if (current.lastEventId === evt.id) return 'replay'\n if (current.state === 'canceled') return 'replay' // terminal — fine to no-op\n const next = applyTransition(\n current,\n { state: 'canceled', priceId: null, currentPeriodEnd: sub.current_period_end ?? current.currentPeriodEnd },\n { eventId: evt.id, now: this.now },\n )\n return {\n next,\n emit: { kind: 'subscription.deleted', eventId: evt.id, record: next },\n }\n })\n }\n\n private async handleTrialWillEnd(evt: StripeEvent): Promise<void> {\n const sub = evt.data.object as StripeSubscriptionPayload\n const workspaceId = await this.resolveWorkspaceId({\n customerId: sub.customer,\n subscriptionMetadata: sub.metadata,\n })\n if (!workspaceId) return this.emitNoWorkspace(evt)\n const current = await this.store.load(workspaceId)\n if (!current) return this.emitNoWorkspace(evt)\n if (current.lastEventId === evt.id) {\n return this.emit({ kind: 'event_replay', eventId: evt.id, type: evt.type })\n }\n // No state transition — trial_will_end is informational. Update\n // lastEventId so a replay is detected.\n const next: SubscriptionRecord = {\n ...current,\n lastEventId: evt.id,\n trialEnd: sub.trial_end ?? current.trialEnd,\n version: current.version + 1,\n updatedAt: this.now(),\n }\n const written = await this.cas(next, current.version)\n if (!written) return\n await this.emit({\n kind: 'subscription.trial_will_end',\n eventId: evt.id,\n record: next,\n trialEndsAt: sub.trial_end ?? next.trialEnd ?? 0,\n })\n }\n\n private async handleSubLifecycle(evt: StripeEvent, target: SubscriptionState): Promise<void> {\n const sub = evt.data.object as StripeSubscriptionPayload\n const workspaceId = await this.resolveWorkspaceId({\n customerId: sub.customer,\n subscriptionMetadata: sub.metadata,\n })\n if (!workspaceId) return this.emitNoWorkspace(evt)\n\n await this.advance(evt, workspaceId, (current) => {\n if (current.lastEventId === evt.id) return 'replay'\n if (!isValidTransition(current.state, target)) return 'out_of_order'\n const next = applyTransition(current, { state: target }, { eventId: evt.id, now: this.now })\n const kind = target === 'paused' ? 'subscription.paused' : 'subscription.resumed'\n return { next, emit: { kind, eventId: evt.id, record: next } }\n })\n }\n\n /* ----------------------- invoice event handlers ---------------------- */\n\n private async handleInvoicePaid(evt: StripeEvent): Promise<void> {\n const inv = evt.data.object as StripeInvoicePayload\n const workspaceId = await this.resolveWorkspaceId({\n customerId: inv.customer ?? '',\n invoiceMetadata: inv.metadata,\n })\n let record: SubscriptionRecord | null = null\n if (workspaceId) record = await this.store.load(workspaceId)\n await this.emit({\n kind: 'invoice.paid',\n eventId: evt.id,\n record,\n invoiceId: inv.id,\n amountPaid: inv.amount_paid ?? 0,\n })\n }\n\n private async handleInvoicePaymentFailed(evt: StripeEvent): Promise<void> {\n const inv = evt.data.object as StripeInvoicePayload\n const workspaceId = await this.resolveWorkspaceId({\n customerId: inv.customer ?? '',\n invoiceMetadata: inv.metadata,\n })\n let record: SubscriptionRecord | null = null\n if (workspaceId) record = await this.store.load(workspaceId)\n await this.emit({\n kind: 'invoice.payment_failed',\n eventId: evt.id,\n record,\n invoiceId: inv.id,\n amountDue: inv.amount_due ?? 0,\n })\n }\n\n /* ------------------------------- core -------------------------------- */\n\n /** Load, apply a transformation, CAS-write. The transformation may\n * return 'replay' / 'out_of_order' for the dispatcher to emit\n * diagnostic events instead. Retries on contention up to\n * `maxCasRetries`; if exhausted, emits via `onError`. */\n private async advance(\n evt: StripeEvent,\n workspaceId: string,\n transform: (current: SubscriptionRecord) => { next: SubscriptionRecord; emit: StripeBillingEvent } | 'replay' | 'out_of_order',\n ): Promise<void> {\n for (let attempt = 0; attempt < this.maxCasRetries; attempt++) {\n const current = await this.store.load(workspaceId)\n if (!current) return this.emitNoWorkspace(evt)\n const result = transform(current)\n if (result === 'replay') {\n return this.emit({ kind: 'event_replay', eventId: evt.id, type: evt.type })\n }\n if (result === 'out_of_order') {\n return this.emit({\n kind: 'event_dropped_out_of_order',\n eventId: evt.id,\n type: evt.type,\n reason: `current=${current.state}`,\n })\n }\n const written = await this.store.saveIfVersion(result.next, current.version)\n if (written) return this.emit(result.emit)\n }\n this.onError(new BillingError({\n code: 'webhook_event_unknown',\n message: `CAS contention exhausted after ${this.maxCasRetries} attempts`,\n context: { workspaceId, eventId: evt.id },\n }), { eventId: evt.id, type: evt.type })\n }\n\n private async cas(record: SubscriptionRecord, expectedVersion: number): Promise<boolean> {\n for (let attempt = 0; attempt < this.maxCasRetries; attempt++) {\n const ok = await this.store.saveIfVersion(record, expectedVersion + attempt)\n if (ok) return true\n }\n return false\n }\n\n private async emit(event: StripeBillingEvent): Promise<void> {\n if (!this.listener) return\n try {\n await this.listener(event)\n } catch (err) {\n this.onError(err, {\n eventId: 'eventId' in event ? event.eventId : 'unknown',\n type: event.kind,\n })\n }\n }\n\n private emitNoWorkspace(evt: StripeEvent): Promise<void> {\n return this.emit({\n kind: 'event_dropped_out_of_order',\n eventId: evt.id,\n type: evt.type,\n reason: 'workspaceId could not be resolved from event payload',\n })\n }\n}\n\n/* ---------------------------------------------------------------------- */\n/* helpers */\n/* ---------------------------------------------------------------------- */\n\n/** Compose multiple listeners — fan out + collect errors. */\nexport function combineListeners(...listeners: StripeBillingListener[]): StripeBillingListener {\n return async (event) => {\n for (const l of listeners) await l(event)\n }\n}\n\nfunction defaultResolveWorkspaceId(input: { subscriptionMetadata?: Record<string, string>; invoiceMetadata?: Record<string, string> }): string | null {\n const sub = input.subscriptionMetadata?.workspaceId\n if (sub) return sub\n const inv = input.invoiceMetadata?.workspaceId\n return inv ?? null\n}\n\nfunction defaultOnError(err: unknown, context: { eventId: string; type: string }): void {\n // eslint-disable-next-line no-console\n console.error('[StripeBillingDispatcher]', context, err)\n}\n\nfunction parseState(status: string, eventId: string): SubscriptionState {\n switch (status) {\n case 'incomplete':\n case 'incomplete_expired':\n case 'trialing':\n case 'active':\n case 'past_due':\n case 'canceled':\n case 'unpaid':\n case 'paused':\n return status\n default:\n throw new BillingError({\n code: 'webhook_event_unknown',\n message: `Unknown Stripe subscription status '${status}'`,\n context: { eventId },\n })\n }\n}\n\nfunction canApplyFreshCreate(state: SubscriptionState): boolean {\n // A 'created' event on a record that already advanced past\n // incomplete means we've already processed the lifecycle and a\n // retried-late 'created' should be dropped.\n return state === 'incomplete' || state === 'incomplete_expired'\n}\n\nfunction extractPriceId(sub: StripeSubscriptionPayload): string | null {\n return sub.items?.data?.[0]?.price?.id ?? null\n}\n","/**\n * Pricing plan scaffold + checkout URL generator.\n *\n * Per task constraint, this module does NOT bake in pricing. The\n * consumer (product agent) supplies the `PricingPlan[]` table at boot.\n * We standardize the SHAPE (id, name, monthly/yearly USD, feature\n * bullets, stripe price ids), the LOOKUP (`findPlan`, `requirePlan`),\n * and the CHECKOUT URL flow (`createCheckoutUrl`).\n *\n * The shape is intentionally USD-only with month/year recurrence.\n * Stripe supports more — multi-currency, week/quarter, usage-based,\n * tiered — but adding columns we don't use creates pressure to fill\n * them with defaults that mislead. When a product needs more, extend\n * the shape; do not work around it in the consumer.\n *\n * `createCheckoutUrl` writes the workspaceId into\n * `subscription_data.metadata.workspaceId` so the dispatcher's default\n * `resolveWorkspaceId` finds it on the first `customer.subscription.created`\n * webhook. THIS IS LOAD-BEARING: drop it and you have to write a\n * customer → workspace join table by hand.\n */\n\nimport type { StripeClient } from './tenant-config.js'\n\nexport interface PricingPlanFeature {\n /** Short label rendered in pricing table rows. */\n label: string\n /** Optional richer description for the marketing page. */\n description?: string\n /** Whether the feature is included in this plan. Many products show\n * the same feature row across plans with a check/cross. */\n included: boolean\n}\n\nexport interface PricingPlan {\n /** Stable internal id — used by middleware to gate features, NOT the\n * Stripe price id. */\n id: string\n /** Display name in the pricing table. */\n name: string\n /** Monthly price in whole USD. `null` for plans that are yearly-only\n * or contact-sales tiers. */\n monthlyUsd: number | null\n /** Yearly price in whole USD. `null` for plans that don't offer\n * annual billing. */\n yearlyUsd: number | null\n /** Marketing feature bullets. */\n features: PricingPlanFeature[]\n /** Stripe `price_…` ids per cadence. At least one must be set if the\n * matching `*Usd` field is non-null. */\n stripePriceIds: {\n monthly?: string\n yearly?: string\n }\n /** Optional trial-day grant. The dispatcher writes `trialEnd` based\n * on Stripe's response; this field is only the request-time intent. */\n trialDays?: number\n /** Optional metadata threaded into Stripe Subscription metadata — the\n * product can use these for analytics or grant-feature lookup. */\n metadata?: Record<string, string>\n}\n\nexport type BillingCadence = 'monthly' | 'yearly'\n\n/** Find a plan by id. Returns null when not found. */\nexport function findPlan(plans: readonly PricingPlan[], id: string): PricingPlan | null {\n return plans.find((p) => p.id === id) ?? null\n}\n\n/** Look up a plan or throw — for code paths where missing is a bug\n * (e.g., resolving a stored subscription's plan id back to a name). */\nexport function requirePlan(plans: readonly PricingPlan[], id: string): PricingPlan {\n const plan = findPlan(plans, id)\n if (!plan) throw new Error(`pricing: unknown plan id '${id}'`)\n return plan\n}\n\nexport interface CreateCheckoutUrlInput {\n /** Tenant key — written to subscription metadata for webhook routing. */\n workspaceId: string\n /** Plan from the consumer's pricing table. */\n plan: PricingPlan\n /** Which Stripe price id to charge against. */\n billing: BillingCadence\n /** Optional existing Stripe customer id — pre-fills the checkout. */\n customerId?: string\n /** Customer email — used by Stripe to pre-fill or to create a new\n * customer if `customerId` is absent. */\n customerEmail?: string\n /** Success/cancel URLs. Overrides the per-tenant defaults from\n * `TenantStripeConfig.successUrl`/`cancelUrl`. */\n successUrl?: string\n cancelUrl?: string\n /** Idempotency key — pass a deterministic key (e.g.,\n * `${workspaceId}:${plan.id}:${billing}`) so the same user clicking\n * twice gets the same checkout session. */\n idempotencyKey: string\n /** Trial override — if set, beats `plan.trialDays`. */\n trialDays?: number\n /** Optional extra metadata mixed into Stripe metadata. */\n metadata?: Record<string, string>\n}\n\nexport interface CheckoutUrl {\n sessionId: string\n url: string\n}\n\n/**\n * Create a Stripe checkout session and return its hosted URL. Uses the\n * per-tenant `StripeClient` from `getStripeClient(productId)`.\n *\n * The workspaceId is written into TWO metadata maps:\n * - `metadata` (on the session itself, surfaces on `checkout.session.*`)\n * - `subscription_data[metadata]` (carries through to the Subscription\n * row Stripe creates, which is what `customer.subscription.*`\n * webhooks carry)\n *\n * Without the second, the dispatcher can't route the first\n * `subscription.created` event to a workspace. We've shipped that bug\n * before — written here once to make it impossible to forget.\n */\nexport async function createCheckoutUrl(\n client: StripeClient,\n input: CreateCheckoutUrlInput,\n): Promise<CheckoutUrl> {\n const priceId = input.plan.stripePriceIds[input.billing]\n if (!priceId) {\n throw new Error(`pricing: plan '${input.plan.id}' has no Stripe price for cadence '${input.billing}'`)\n }\n const successUrl = input.successUrl ?? client.config.successUrl\n const cancelUrl = input.cancelUrl ?? client.config.cancelUrl\n if (!successUrl || !cancelUrl) {\n throw new Error('pricing: successUrl and cancelUrl required (per-call or in TenantStripeConfig)')\n }\n\n const trialDays = input.trialDays ?? input.plan.trialDays\n const body: Record<string, string | number | boolean | undefined> = {\n mode: 'subscription',\n success_url: successUrl,\n cancel_url: cancelUrl,\n 'line_items[0][price]': priceId,\n 'line_items[0][quantity]': 1,\n 'metadata[workspaceId]': input.workspaceId,\n 'metadata[planId]': input.plan.id,\n 'subscription_data[metadata][workspaceId]': input.workspaceId,\n 'subscription_data[metadata][planId]': input.plan.id,\n }\n if (input.customerId) body.customer = input.customerId\n if (input.customerEmail && !input.customerId) body.customer_email = input.customerEmail\n if (trialDays && trialDays > 0) {\n body['subscription_data[trial_period_days]'] = trialDays\n }\n // Mix in plan-defined metadata + caller-supplied metadata.\n const extra = { ...(input.plan.metadata ?? {}), ...(input.metadata ?? {}) }\n for (const [k, v] of Object.entries(extra)) {\n body[`metadata[${k}]`] = v\n body[`subscription_data[metadata][${k}]`] = v\n }\n\n const created = await client.mutate<{ id: string; url: string }>(\n 'POST',\n '/checkout/sessions',\n body,\n input.idempotencyKey,\n )\n if (!created.url) {\n throw new Error('pricing: Stripe checkout response missing url')\n }\n return { sessionId: created.id, url: created.url }\n}\n\n/**\n * Create a Stripe customer-billing-portal session and return its URL.\n * The product calls this when a user clicks \"manage billing\" — the\n * portal handles cancel / change plan / update card without us\n * implementing those flows.\n */\nexport async function createBillingPortalUrl(\n client: StripeClient,\n input: { customerId: string; returnUrl: string; idempotencyKey: string },\n): Promise<{ sessionId: string; url: string }> {\n const created = await client.mutate<{ id: string; url: string }>(\n 'POST',\n '/billing_portal/sessions',\n {\n customer: input.customerId,\n return_url: input.returnUrl,\n },\n input.idempotencyKey,\n )\n return { sessionId: created.id, url: created.url }\n}\n","/**\n * Per-tenant Stripe configuration routing.\n *\n * Five product agents (legal, tax, gtm, creative, agent-builder) each\n * own a SEPARATE Stripe account. Reasons we pay the multi-account tax\n * rather than billing everyone through a single Tangle Stripe account:\n *\n * 1. Each product is a different LLC/legal entity for tax + dispute\n * handling. Customer chargebacks land on the product's account.\n * 2. Stripe Tax + Atlas are per-account; we can't share a single\n * Tax-collection setup across five SaaS products.\n * 3. Each product has its own pricing experiments; sharing one account\n * would force a shared products/prices namespace and surface\n * leak risk in the Stripe dashboard.\n *\n * The routing table maps `productId` (a Tangle-internal stable\n * identifier) to:\n * - Stripe Secret Key (`sk_live_…` or `rk_live_…`)\n * - Webhook signing secret (`whsec_…`) — used by the WebhookRouter's\n * `resolveSecret` callback.\n * - Optional success/cancel URL defaults the product wants used\n * unless the caller overrides per-checkout.\n *\n * Env-var convention is `STRIPE_SK_<PRODUCT_UPPER>` and\n * `STRIPE_WHSEC_<PRODUCT_UPPER>` — the resolver below honors that by\n * default. Consumers that store keys in a vault (Doppler, AWS Secrets\n * Manager) inject their own `TenantConfigResolver` instead.\n *\n * Critical invariant: this module NEVER caches resolved keys across\n * `getStripeClient()` calls without the consumer opting in. Stripe\n * encourages key rotation (Atlas docs); a cached `sk_…` outlives the\n * rotation. The default `EnvTenantConfigResolver` re-reads env every\n * call. Consumers that want a memoized cache wrap with\n * `memoizeResolver(resolver, ttlMs)`.\n */\n\nimport { ConfigError } from './errors.js'\n\n/** Stable product identifiers — kept in sync with the product registry.\n * Adding a product is a one-line addition; we centralize so a typo at\n * a call site (`'legal-agent'` vs `'legal'`) is a type error. */\nexport type ProductId =\n | 'legal'\n | 'tax'\n | 'gtm'\n | 'creative'\n | 'agent-builder'\n\nexport const PRODUCT_IDS: readonly ProductId[] = Object.freeze([\n 'legal',\n 'tax',\n 'gtm',\n 'creative',\n 'agent-builder',\n])\n\nexport interface TenantStripeConfig {\n productId: ProductId\n /** Stripe API secret key. Treat as opaque — do NOT log. */\n secretKey: string\n /** Webhook signing secret (`whsec_...`). */\n webhookSecret: string\n /** Optional default URLs the checkout/portal generators fall back to. */\n successUrl?: string\n cancelUrl?: string\n /** Free-form metadata threaded through to the product (e.g., the\n * Connect account id if you later migrate to Connect). */\n metadata?: Record<string, string>\n}\n\n/** Stateless resolver — called per `getStripeClient()` / `resolveSecret()`.\n * Implementations: read from env (default), read from a vault, read\n * from a workspace-scoped DB row (per-tenant Connect). */\nexport interface TenantConfigResolver {\n resolve(productId: ProductId): Promise<TenantStripeConfig | null> | TenantStripeConfig | null\n}\n\n/* ---------------------------------------------------------------------- */\n/* env resolver */\n/* ---------------------------------------------------------------------- */\n\n/**\n * Reads `STRIPE_SK_<PRODUCT>` + `STRIPE_WHSEC_<PRODUCT>` from\n * `process.env`. Product id is upper-snake-cased (`agent-builder` →\n * `AGENT_BUILDER`).\n *\n * Optional defaults:\n * STRIPE_SUCCESS_URL_<PRODUCT>\n * STRIPE_CANCEL_URL_<PRODUCT>\n */\nexport class EnvTenantConfigResolver implements TenantConfigResolver {\n constructor(private readonly env: NodeJS.ProcessEnv = process.env) {}\n\n resolve(productId: ProductId): TenantStripeConfig | null {\n const key = envKey(productId)\n const sk = this.env[`STRIPE_SK_${key}`]\n const wh = this.env[`STRIPE_WHSEC_${key}`]\n if (!sk || !wh) return null\n return {\n productId,\n secretKey: sk,\n webhookSecret: wh,\n successUrl: this.env[`STRIPE_SUCCESS_URL_${key}`],\n cancelUrl: this.env[`STRIPE_CANCEL_URL_${key}`],\n }\n }\n}\n\n/**\n * Static resolver — pass a hardcoded map, useful for tests and for\n * deployments that pull from a vault at boot.\n */\nexport class StaticTenantConfigResolver implements TenantConfigResolver {\n constructor(private readonly table: Partial<Record<ProductId, TenantStripeConfig>>) {}\n resolve(productId: ProductId): TenantStripeConfig | null {\n return this.table[productId] ?? null\n }\n}\n\n/**\n * Memoize a resolver with a TTL. Used in production to avoid pounding\n * a remote vault on every webhook. Default 60s — short enough that a\n * key rotation lands within the next minute.\n */\nexport function memoizeResolver(inner: TenantConfigResolver, ttlMs = 60_000): TenantConfigResolver {\n const cache = new Map<ProductId, { config: TenantStripeConfig | null; expiresAt: number }>()\n return {\n async resolve(productId: ProductId) {\n const now = Date.now()\n const hit = cache.get(productId)\n if (hit && hit.expiresAt > now) return hit.config\n const config = await inner.resolve(productId)\n cache.set(productId, { config, expiresAt: now + ttlMs })\n return config\n },\n }\n}\n\n/* ---------------------------------------------------------------------- */\n/* the Stripe client handle */\n/* ---------------------------------------------------------------------- */\n\n/**\n * Thin Stripe HTTP client handle. We do NOT depend on the `stripe`\n * npm package — same rationale as `stripe-pack`: keep the install\n * footprint zero, use `fetch` directly. The handle carries the\n * resolved secret + a Stripe-spec base URL so call sites can issue\n * scoped requests without re-resolving for every operation in a\n * batch.\n *\n * Idempotency-Key forwarding: every mutation MUST include an\n * `idempotency-key` header. Stripe enforces a 24h replay window\n * keyed off it. The `mutate()` helper accepts the key explicitly to\n * make it impossible to forget.\n */\nexport interface StripeClient {\n productId: ProductId\n config: TenantStripeConfig\n /** GET request — returns parsed JSON or throws on non-2xx. */\n get<T = unknown>(path: string, query?: Record<string, string>): Promise<T>\n /** Form-urlencoded POST/DELETE with idempotency. */\n mutate<T = unknown>(\n method: 'POST' | 'DELETE',\n path: string,\n body: Record<string, string | number | boolean | undefined>,\n idempotencyKey: string,\n ): Promise<T>\n}\n\nconst STRIPE_API = 'https://api.stripe.com/v1'\n\n/**\n * Look up the Stripe client for a product. Throws `ConfigError` if the\n * resolver returns null — the product agent fails its startup health\n * check and the deploy is held back. NEVER silently falls back to a\n * shared key.\n */\nexport async function getStripeClient(\n productId: ProductId,\n resolver: TenantConfigResolver,\n): Promise<StripeClient> {\n const config = await resolver.resolve(productId)\n if (!config) {\n throw new ConfigError({\n message: `Stripe not configured for product '${productId}'. Set STRIPE_SK_${envKey(productId)} and STRIPE_WHSEC_${envKey(productId)}.`,\n context: { productId },\n })\n }\n return buildClient(config)\n}\n\n/** Build a client from an already-resolved config — for callers that\n * manage resolution themselves (e.g., long-lived workers that\n * resolved at startup). */\nexport function buildStripeClient(config: TenantStripeConfig): StripeClient {\n return buildClient(config)\n}\n\nfunction buildClient(config: TenantStripeConfig): StripeClient {\n const auth = `Bearer ${config.secretKey}`\n return {\n productId: config.productId,\n config,\n async get<T>(path: string, query?: Record<string, string>): Promise<T> {\n const qs = query ? `?${new URLSearchParams(query).toString()}` : ''\n const res = await fetch(`${STRIPE_API}${path}${qs}`, {\n headers: { authorization: auth },\n signal: AbortSignal.timeout(10_000),\n })\n if (!res.ok) {\n const text = await res.text().catch(() => '')\n throw new Error(`stripe ${path} ${res.status}: ${text.slice(0, 200)}`)\n }\n return (await res.json()) as T\n },\n async mutate<T>(\n method: 'POST' | 'DELETE',\n path: string,\n body: Record<string, string | number | boolean | undefined>,\n idempotencyKey: string,\n ): Promise<T> {\n const form = new URLSearchParams()\n for (const [k, v] of Object.entries(body)) {\n if (v === undefined) continue\n form.set(k, String(v))\n }\n const init: RequestInit = {\n method,\n headers: {\n authorization: auth,\n 'idempotency-key': idempotencyKey,\n ...(method === 'POST' ? { 'content-type': 'application/x-www-form-urlencoded' } : {}),\n },\n signal: AbortSignal.timeout(15_000),\n }\n if (method === 'POST') init.body = form\n const res = await fetch(`${STRIPE_API}${path}`, init)\n if (!res.ok) {\n const text = await res.text().catch(() => '')\n throw new Error(`stripe ${path} ${res.status}: ${text.slice(0, 200)}`)\n }\n return (await res.json()) as T\n },\n }\n}\n\n/**\n * `WebhookRouter.resolveSecret` adapter. The router calls this with\n * the provider id and headers; we extract the product id from a\n * header the gateway routes by (`x-tangle-product`) and look it up.\n *\n * Why a header and not the URL path: the router is provider-keyed,\n * not product-keyed, by design. Products inject the header in their\n * gateway layer (Hono middleware in our case). The header is\n * authenticated as part of the gateway's edge auth — Stripe's own\n * signature still has to verify against the secret we return here,\n * so a forged header alone can't bypass anything.\n */\nexport function makeStripeSecretResolver(resolver: TenantConfigResolver) {\n return async function resolveSecret(\n providerId: string,\n headers: { [name: string]: string | string[] | undefined },\n ): Promise<string | null> {\n if (providerId !== 'stripe') return null\n const productHeader = headers['x-tangle-product']\n const productId = Array.isArray(productHeader) ? productHeader[0] : productHeader\n if (!productId || !isProductId(productId)) return null\n const config = await resolver.resolve(productId)\n return config?.webhookSecret ?? null\n }\n}\n\nfunction isProductId(s: string): s is ProductId {\n return (PRODUCT_IDS as readonly string[]).includes(s)\n}\n\nfunction envKey(productId: ProductId): string {\n return productId.toUpperCase().replace(/-/g, '_')\n}\n","/**\n * Drop-in middleware for product agents.\n *\n * Three primitives consumers wire into their HTTP layer (Hono, Express,\n * raw Workers `fetch` handler — middleware here is framework-neutral,\n * returns a `BillingGate` value the consumer chooses how to respond to).\n *\n * requireActiveSubscription({ workspaceId, store })\n * → 'allow' | { allowed: false, error: BillingError }\n *\n * withTrialAccess({ workspaceId, days, trialStore })\n * → allow while trial < days expired since workspace creation\n *\n * getRemainingFreeTier({ workspaceId, freeTierStore })\n * → { remaining: number, total: number }\n *\n * Frameworks: we don't import Hono / Express. The middleware shape is a\n * pure async function returning a decision. The product wires it into\n * its framework with a 3-line adapter (see `examples/hono.ts`).\n *\n * Past-due policy: by default `requireActiveSubscription` allows\n * `past_due` (the dunning grace window — see `gateAccess` in\n * `subscription-state.ts`). Pass `denyPastDue: true` to override\n * per-route (e.g., legal-agent's \"file new petition\" gate where\n * irreversible actions justify a stricter rule).\n */\n\nimport { BillingError } from './errors.js'\nimport {\n gateAccess,\n type SubscriptionRecord,\n type SubscriptionStore,\n} from './subscription-state.js'\n\n/* ---------------------------------------------------------------------- */\n/* requireActiveSubscription */\n/* ---------------------------------------------------------------------- */\n\nexport interface RequireActiveSubscriptionInput {\n workspaceId: string\n store: SubscriptionStore\n /** Strict mode: reject `past_due`. Default false (allow with warn). */\n denyPastDue?: boolean\n}\n\nexport type SubscriptionGateResult =\n | { allowed: true; record: SubscriptionRecord; warn?: 'past_due' | 'trial_ending' }\n | { allowed: false; error: BillingError }\n\n/**\n * Gate decision for a route that requires an active subscription.\n *\n * Returns `{ allowed: true }` on `active` / `trialing` and on\n * `past_due` (unless `denyPastDue`). Returns `{ allowed: false, error }`\n * with a typed `BillingError` for any other state — the consumer maps\n * the error's `status` to the HTTP response.\n */\nexport async function requireActiveSubscription(\n input: RequireActiveSubscriptionInput,\n): Promise<SubscriptionGateResult> {\n const record = await input.store.load(input.workspaceId)\n if (!record) {\n return {\n allowed: false,\n error: new BillingError({\n code: 'subscription_required',\n message: 'This workspace has no Stripe subscription.',\n context: { workspaceId: input.workspaceId },\n }),\n }\n }\n const decision = gateAccess(record.state)\n if (!decision.allowed) {\n return {\n allowed: false,\n error: new BillingError({\n code: decision.reason === 'subscription_inactive'\n ? 'subscription_inactive'\n : decision.reason === 'subscription_past_due'\n ? 'subscription_past_due'\n : 'subscription_required',\n message: `Subscription is ${record.state}.`,\n context: {\n workspaceId: input.workspaceId,\n subscriptionId: record.subscriptionId,\n subscriptionState: record.state,\n },\n }),\n }\n }\n if (decision.warn === 'past_due' && input.denyPastDue) {\n return {\n allowed: false,\n error: new BillingError({\n code: 'subscription_past_due',\n message: 'Subscription is past due — this action requires a current payment method.',\n context: {\n workspaceId: input.workspaceId,\n subscriptionId: record.subscriptionId,\n subscriptionState: record.state,\n },\n }),\n }\n }\n // Surface `trial_ending` warn when within 72h of trial end.\n let warn = decision.warn\n if (!warn && record.state === 'trialing' && record.trialEnd) {\n const TRIAL_WARN_SECONDS = 72 * 60 * 60\n const nowSec = Math.floor(Date.now() / 1000)\n if (record.trialEnd - nowSec < TRIAL_WARN_SECONDS) {\n warn = 'trial_ending'\n }\n }\n return { allowed: true, record, warn }\n}\n\n/* ---------------------------------------------------------------------- */\n/* withTrialAccess */\n/* ---------------------------------------------------------------------- */\n\n/** Workspace creation timestamp store — required by `withTrialAccess`. */\nexport interface TrialStore {\n /** Returns workspace creation timestamp (ms epoch), or null if the\n * workspace doesn't exist yet. */\n getCreatedAt(workspaceId: string): Promise<number | null> | number | null\n}\n\nexport interface WithTrialAccessInput {\n workspaceId: string\n /** Trial length in days from workspace creation. */\n days: number\n trialStore: TrialStore\n /** Optional `now` override for tests. */\n now?: () => number\n}\n\nexport interface TrialAccessResult {\n /** Whether the workspace is still inside its free-trial window. */\n inTrial: boolean\n /** Days remaining (rounded down). Zero when `inTrial` is false. */\n daysRemaining: number\n /** Trial end timestamp (ms epoch), null when no workspace found. */\n trialEndsAt: number | null\n}\n\n/**\n * Free-trial gate independent of Stripe state. Use BEFORE a workspace\n * has a Stripe subscription (the product's onboarding period). Compose\n * with `requireActiveSubscription`: trial OR active sub passes the gate.\n *\n * Composition pattern:\n *\n * const trial = await withTrialAccess(...)\n * if (trial.inTrial) return next()\n * const sub = await requireActiveSubscription(...)\n * if (sub.allowed) return next()\n * return respond(sub.error)\n */\nexport async function withTrialAccess(input: WithTrialAccessInput): Promise<TrialAccessResult> {\n const createdAt = await input.trialStore.getCreatedAt(input.workspaceId)\n if (createdAt === null) {\n return { inTrial: false, daysRemaining: 0, trialEndsAt: null }\n }\n const now = (input.now ?? Date.now)()\n const trialEndsAt = createdAt + input.days * 24 * 60 * 60 * 1000\n const remainingMs = trialEndsAt - now\n if (remainingMs <= 0) {\n return { inTrial: false, daysRemaining: 0, trialEndsAt }\n }\n const daysRemaining = Math.floor(remainingMs / (24 * 60 * 60 * 1000))\n return { inTrial: true, daysRemaining, trialEndsAt }\n}\n\n/* ---------------------------------------------------------------------- */\n/* getRemainingFreeTier */\n/* ---------------------------------------------------------------------- */\n\n/** Free-tier counter store — abstract over the consumer's metering\n * pipeline. The interface is read-only; products own counter increment\n * on usage (e.g., increment on every API call in their own metrics\n * layer). */\nexport interface FreeTierStore {\n /** Returns `{ used, total }` for the workspace. Implementations\n * return `{ used: 0, total: <default> }` for unknown workspaces if\n * the product wants implicit free-tier grant. */\n getUsage(workspaceId: string): Promise<{ used: number; total: number }> | { used: number; total: number }\n}\n\nexport interface GetRemainingFreeTierInput {\n workspaceId: string\n freeTierStore: FreeTierStore\n}\n\nexport interface FreeTierResult {\n /** Units (whatever the product counts: API calls, tokens, generations) still allowed. */\n remaining: number\n /** Total quota. */\n total: number\n /** Whether the quota is exhausted. */\n exhausted: boolean\n}\n\n/**\n * Return how much free-tier quota the workspace has left. Pure projection\n * over the store; consumers use the result to decide whether to grant the\n * route or return `BillingError(code: 'free_tier_exhausted')`.\n *\n * Why this isn't a gate function itself: free-tier \"exhausted\" is rarely\n * a hard deny — most products throttle, queue, or upsell instead. The\n * decision is product-specific; we provide the read and the typed error\n * but stop short of opining on the response shape.\n */\nexport async function getRemainingFreeTier(\n input: GetRemainingFreeTierInput,\n): Promise<FreeTierResult> {\n const { used, total } = await input.freeTierStore.getUsage(input.workspaceId)\n const remaining = Math.max(0, total - used)\n return { remaining, total, exhausted: remaining === 0 }\n}\n\n/* ---------------------------------------------------------------------- */\n/* composed gate (trial + sub) */\n/* ---------------------------------------------------------------------- */\n\nexport interface ComposedGateInput {\n workspaceId: string\n store: SubscriptionStore\n trialStore?: TrialStore\n trialDays?: number\n denyPastDue?: boolean\n now?: () => number\n}\n\n/**\n * Compose `withTrialAccess` || `requireActiveSubscription`. Most product\n * routes want this exact combo — passes if EITHER the workspace is\n * inside its free trial OR has an active subscription. Returns the\n * subscription error from `requireActiveSubscription` when both fail\n * (the more actionable of the two — the customer can convert it into\n * a checkout).\n */\nexport async function gateSubscriptionOrTrial(\n input: ComposedGateInput,\n): Promise<SubscriptionGateResult & { viaTrial?: boolean; daysRemaining?: number }> {\n if (input.trialStore && input.trialDays) {\n const trial = await withTrialAccess({\n workspaceId: input.workspaceId,\n days: input.trialDays,\n trialStore: input.trialStore,\n now: input.now,\n })\n if (trial.inTrial) {\n // Synthesize a record-shaped result so the consumer's downstream\n // code path is uniform — but flag it as via-trial.\n const trialRecord = trialSyntheticRecord(input.workspaceId, trial.trialEndsAt ?? 0)\n return { allowed: true, record: trialRecord, viaTrial: true, daysRemaining: trial.daysRemaining }\n }\n }\n return requireActiveSubscription({\n workspaceId: input.workspaceId,\n store: input.store,\n denyPastDue: input.denyPastDue,\n })\n}\n\nfunction trialSyntheticRecord(workspaceId: string, trialEndsAt: number): SubscriptionRecord {\n return {\n workspaceId,\n customerId: '',\n subscriptionId: '',\n state: 'trialing',\n priceId: null,\n currentPeriodEnd: Math.floor(trialEndsAt / 1000),\n trialEnd: Math.floor(trialEndsAt / 1000),\n cancelAtPeriodEnd: false,\n version: 0,\n lastEventId: null,\n updatedAt: Date.now(),\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,6 @@
1
- export { T as TANGLE_CATALOG_RUNTIME_SIGNATURE_HEADER, y as TangleCatalogAuthResolverOptions, z as TangleCatalogHttpAuthResolverOptions, A as TangleCatalogHttpAuthResolverRequest, B as TangleCatalogInstalledPackageExecutorOptions, C as TangleCatalogRuntimeHandlerOptions, D as TangleCatalogRuntimeHttpRequest, E as TangleCatalogRuntimeHttpResponse, F as TangleCatalogRuntimeInvocation, G as TangleCatalogRuntimeModuleAction, H as TangleCatalogRuntimePackageCoverageOptions, J as TangleCatalogRuntimePackageCoverageRow, K as auditTangleCatalogRuntimePackages, L as createTangleCatalogCredentialAuthResolver, N as createTangleCatalogHttpAuthResolver, O as createTangleCatalogInstalledPackageExecutor, P as createTangleCatalogRuntimeHandler, Q as signTangleCatalogRuntimeRequest, R as tangleCatalogAuthValue, S as verifyTangleCatalogRuntimeSignature } from './registry.js';
1
+ export { T as TANGLE_CATALOG_RUNTIME_SIGNATURE_HEADER, J as TangleCatalogAuthResolverOptions, K as TangleCatalogHttpAuthResolverOptions, N as TangleCatalogHttpAuthResolverRequest, O as TangleCatalogInstalledPackageExecutorOptions, P as TangleCatalogRuntimeHandlerOptions, Q as TangleCatalogRuntimeHttpRequest, S as TangleCatalogRuntimeHttpResponse, U as TangleCatalogRuntimeInvocation, V as TangleCatalogRuntimeModuleAction, W as TangleCatalogRuntimePackageCoverageOptions, X as TangleCatalogRuntimePackageCoverageRow, Y as auditTangleCatalogRuntimePackages, Z as createTangleCatalogCredentialAuthResolver, _ as createTangleCatalogHttpAuthResolver, $ as createTangleCatalogInstalledPackageExecutor, a0 as createTangleCatalogRuntimeHandler, a1 as signTangleCatalogRuntimeRequest, a2 as tangleCatalogAuthValue, a3 as verifyTangleCatalogRuntimeSignature } from './registry.js';
2
2
  import './tangle-id-CTU4kGId.js';
3
+ import './errors-Bg3_rxnQ.js';
3
4
  import './connect/index.js';
4
5
  import './middleware/index.js';
5
6
  import './connectors/index.js';
@@ -8,13 +8,15 @@ import {
8
8
  signTangleCatalogRuntimeRequest,
9
9
  tangleCatalogAuthValue,
10
10
  verifyTangleCatalogRuntimeSignature
11
- } from "./chunk-ICSBYCE2.js";
11
+ } from "./chunk-TUX6MJJ4.js";
12
+ import "./chunk-P24T3MLM.js";
12
13
  import "./chunk-SVQ4PHDZ.js";
14
+ import "./chunk-H4XYLS7T.js";
15
+ import "./chunk-YOKNZY2N.js";
13
16
  import "./chunk-4JQ754PA.js";
14
17
  import "./chunk-376UBTNB.js";
15
18
  import "./chunk-JU25UDN2.js";
16
19
  import "./chunk-2TW2QKGZ.js";
17
- import "./chunk-P24T3MLM.js";
18
20
  import "./chunk-ATYHZXLL.js";
19
21
  export {
20
22
  TANGLE_CATALOG_RUNTIME_SIGNATURE_HEADER,
@@ -1,150 +1,5 @@
1
- /**
2
- * @stable Provider-agnostic inbound webhook router.
3
- *
4
- * Consumer hooks a single HTTP handler at `/webhook/:provider/:event`
5
- * (or whatever pathing they prefer) and forwards the request through
6
- * `WebhookRouter.handle()`. The router:
7
- *
8
- * 1. Resolves the registered provider entry.
9
- * 2. Calls the provider's `verifySignature(rawBody, headers, secrets)`.
10
- * Failure → 401 fast, no downstream work.
11
- * 3. Calls the provider's `parse(rawBody, headers)` to extract zero or
12
- * more normalized events.
13
- * 4. Enqueues each event for async processing via the consumer-supplied
14
- * `deliver(event)` callback (best-effort fire-and-forget — the
15
- * router does NOT block the HTTP response on the consumer's work).
16
- * 5. Returns 200 fast with `{received: events.length}`.
17
- *
18
- * Replay protection: providers that sign timestamps (Stripe, Slack)
19
- * already reject stale signatures inside `verifySignature`. For providers
20
- * that don't (DocuSeal, GDrive push), the router exposes a pluggable
21
- * `idempotency` hook: if `idempotency.seen(providerEventId)` returns
22
- * true, the router 200s without invoking `deliver()`. Consumers wire
23
- * this to a durable kv (D1 / Redis / Postgres unique-index).
24
- *
25
- * Why a router and not a per-provider express app: the runtime contract
26
- * a product cares about is "an inbound event came in, here's the
27
- * normalized envelope". Verification, parsing, and idempotency-dedup
28
- * are mechanical and provider-specific — the router owns them. The
29
- * consumer's `deliver()` is the only place product logic runs.
30
- *
31
- * Stability: `@stable` — additions to `WebhookEnvelope` must be
32
- * additive; the router's HTTP contract (paths, status codes) is frozen
33
- * at 200 (ok), 400 (bad request), 401 (bad signature), 404 (unknown
34
- * provider), 405 (provider has no inbound surface).
35
- */
36
- interface WebhookHeaders {
37
- [name: string]: string | string[] | undefined;
38
- }
39
- /** Normalized inbound event the router emits after parsing. */
40
- interface WebhookEnvelope<TPayload = unknown> {
41
- /** Provider id (matches the `:provider` path segment). */
42
- provider: string;
43
- /** Optional event class — e.g., 'customer.subscription.deleted'. The
44
- * provider's parser decides. Used for routing inside `deliver()`. */
45
- eventType: string;
46
- /** Provider-emitted event id, when present. Used for the idempotency
47
- * short-circuit. */
48
- providerEventId?: string;
49
- /** Wall-clock receive time. */
50
- receivedAt: number;
51
- /** Provider payload, normalized to the provider's documented event
52
- * shape. The router does NOT reshape this — `parse()` is the contract. */
53
- payload: TPayload;
54
- /** Headers passed through for downstream handlers that want them
55
- * (e.g., to extract custom routing metadata). Always lowercased keys. */
56
- headers: Record<string, string>;
57
- }
58
- type SignatureVerification = {
59
- valid: true;
60
- } | {
61
- valid: false;
62
- reason: string;
63
- };
64
- /** Per-provider plug-in. Stateless — the router calls `verifySignature`
65
- * then `parse` on every request. The provider's HTTP-shape concerns
66
- * (e.g., raw body required) are documented per provider. */
67
- interface WebhookProvider {
68
- /** Stable provider id (`stripe`, `docuseal`, `gdrive`, ...). */
69
- id: string;
70
- /** Verify the inbound signature. Receives the EXACT raw body string —
71
- * consumers MUST preserve raw bytes through their HTTP server (do not
72
- * parse JSON before forwarding here). */
73
- verifySignature(input: {
74
- rawBody: string;
75
- headers: WebhookHeaders;
76
- secret: string;
77
- }): SignatureVerification;
78
- /** Parse the validated raw body into zero or more normalized events.
79
- * A single push payload may carry multiple events (e.g., Slack bulk
80
- * delivery). Return [] to ack the push as a no-op. */
81
- parse(input: {
82
- rawBody: string;
83
- headers: WebhookHeaders;
84
- now?: number;
85
- }): WebhookEnvelope[] | Promise<WebhookEnvelope[]>;
86
- }
87
- interface WebhookIdempotencyStore {
88
- /** Returns true if this providerEventId has been processed already.
89
- * Implementations should be O(1) (Redis SETNX, D1 UNIQUE constraint). */
90
- seen(providerEventId: string): Promise<boolean> | boolean;
91
- /** Marks a providerEventId as processed. Called AFTER `deliver()` has
92
- * been invoked. */
93
- remember(providerEventId: string, ttlMs: number): Promise<void> | void;
94
- }
95
- interface WebhookRouterOptions {
96
- /** Provider registry. Pass any number of providers; routing is by id. */
97
- providers: WebhookProvider[];
98
- /** Async callback invoked with every accepted event. Fire-and-forget
99
- * from the router's perspective — the HTTP response is sent before
100
- * this resolves. Throws are caught and reported via `onError`. */
101
- deliver(event: WebhookEnvelope): Promise<void> | void;
102
- /** Resolve the signing secret for a provider id at request time. The
103
- * router never holds secrets — the consumer's vault resolves them. */
104
- resolveSecret(providerId: string, headers: WebhookHeaders): Promise<string | null> | string | null;
105
- /** Optional idempotency-dedup hook. Required for providers that don't
106
- * sign timestamps in their signature scheme (DocuSeal, Drive push). */
107
- idempotency?: WebhookIdempotencyStore;
108
- /** TTL on idempotency entries. Default 7 days — long enough that a
109
- * provider's normal retry-window can't re-deliver. */
110
- idempotencyTtlMs?: number;
111
- /** Surface delivery errors. Default: console.error. */
112
- onError?(err: unknown, context: {
113
- provider: string;
114
- eventType?: string;
115
- providerEventId?: string;
116
- }): void;
117
- /** Override `now()` for tests. */
118
- now?(): number;
119
- }
120
- interface WebhookRouterRequest {
121
- providerId: string;
122
- rawBody: string;
123
- headers: WebhookHeaders;
124
- }
125
- interface WebhookRouterResponse {
126
- status: number;
127
- body: unknown;
128
- headers?: Record<string, string>;
129
- }
130
- /**
131
- * Router instance. Stateless aside from the provider registry — safe to
132
- * share across requests; build once per process.
133
- */
134
- declare class WebhookRouter {
135
- private readonly providers;
136
- private readonly deliver;
137
- private readonly resolveSecret;
138
- private readonly idempotency?;
139
- private readonly idempotencyTtlMs;
140
- private readonly onError;
141
- private readonly nowFn;
142
- constructor(opts: WebhookRouterOptions);
143
- /** Process one inbound webhook request. Pure with respect to side-
144
- * effects on the router instance — safe to call concurrently. */
145
- handle(request: WebhookRouterRequest): Promise<WebhookRouterResponse>;
146
- private deliverEach;
147
- }
1
+ import { W as WebhookProvider } from '../router-BncoovUh.js';
2
+ export { S as SignatureVerification, a as WebhookEnvelope, b as WebhookHeaders, c as WebhookIdempotencyStore, d as WebhookRouter, e as WebhookRouterOptions, f as WebhookRouterRequest, g as WebhookRouterResponse } from '../router-BncoovUh.js';
148
3
 
149
4
  /**
150
5
  * Pre-built `WebhookProvider` implementations for the inbound surfaces
@@ -190,4 +45,4 @@ declare function genericHmacWebhookProvider(options: {
190
45
  parse?: WebhookProvider['parse'];
191
46
  }): WebhookProvider;
192
47
 
193
- export { type SignatureVerification, type WebhookEnvelope, type WebhookHeaders, type WebhookIdempotencyStore, type WebhookProvider, WebhookRouter, type WebhookRouterOptions, type WebhookRouterRequest, type WebhookRouterResponse, docusealWebhookProvider, gdriveWebhookProvider, genericHmacWebhookProvider, gmailWebhookProvider, slackWebhookProvider, stripeWebhookProvider };
48
+ export { WebhookProvider, docusealWebhookProvider, gdriveWebhookProvider, genericHmacWebhookProvider, gmailWebhookProvider, slackWebhookProvider, stripeWebhookProvider };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-integrations",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "description": "Vendor-neutral integration contracts and runtime helpers for sandbox and agent apps.",
5
5
  "homepage": "https://github.com/tangle-network/agent-integrations#readme",
6
6
  "repository": {
@@ -39,6 +39,11 @@
39
39
  "import": "./dist/connect/index.js",
40
40
  "default": "./dist/connect/index.js"
41
41
  },
42
+ "./consumer": {
43
+ "types": "./dist/consumer.d.ts",
44
+ "import": "./dist/consumer.js",
45
+ "default": "./dist/consumer.js"
46
+ },
42
47
  "./middleware": {
43
48
  "types": "./dist/middleware/index.d.ts",
44
49
  "import": "./dist/middleware/index.js",
@@ -64,6 +69,11 @@
64
69
  "import": "./dist/specs.js",
65
70
  "default": "./dist/specs.js"
66
71
  },
72
+ "./stripe": {
73
+ "types": "./dist/stripe/index.d.ts",
74
+ "import": "./dist/stripe/index.js",
75
+ "default": "./dist/stripe/index.js"
76
+ },
67
77
  "./tangle-catalog-runtime": {
68
78
  "types": "./dist/tangle-catalog-runtime.d.ts",
69
79
  "import": "./dist/tangle-catalog-runtime.js",
@@ -83,6 +93,15 @@
83
93
  "publishConfig": {
84
94
  "access": "public"
85
95
  },
96
+ "scripts": {
97
+ "build": "tsup",
98
+ "dev": "tsup --watch",
99
+ "audit:execution": "pnpm build >/dev/null && node scripts/audit-integration-execution.mjs",
100
+ "prepare": "tsup",
101
+ "test": "vitest run",
102
+ "test:watch": "vitest",
103
+ "typecheck": "tsc --noEmit"
104
+ },
86
105
  "devDependencies": {
87
106
  "@types/node": "^25.6.0",
88
107
  "tsup": "^8.0.0",
@@ -93,12 +112,5 @@
93
112
  "node": ">=20"
94
113
  },
95
114
  "license": "MIT",
96
- "scripts": {
97
- "build": "tsup",
98
- "dev": "tsup --watch",
99
- "audit:execution": "pnpm build >/dev/null && node scripts/audit-integration-execution.mjs",
100
- "test": "vitest run",
101
- "test:watch": "vitest",
102
- "typecheck": "tsc --noEmit"
103
- }
104
- }
115
+ "packageManager": "pnpm@10.28.0"
116
+ }