Skip to content

@lastshotlabs/slingshot-billing

npm install @lastshotlabs/slingshot-billing

Public contract for slingshot-billing.

Consumers (e.g. an app mapping plans onto its own domain) read the entitlement through the typed capability handle rather than reaching into package state. The package publishes the implementation via capabilities.provides; consumers resolve it with ctx.capabilities.require(BillingEntitlementCap).

Source: packages/slingshot-billing/src/public.ts

Stable identifier for this package; used as the contract/event owner name.

Source: packages/slingshot-billing/src/plugin.ts

Public contract for slingshot-billing.

Consumers (e.g. an app mapping plans onto its own domain) read the entitlement through the typed capability handle rather than reaching into package state. The package publishes the implementation via capabilities.provides; consumers resolve it with ctx.capabilities.require(BillingEntitlementCap).

Source: packages/slingshot-billing/src/public.ts

Trim trailing slashes and validate a mount path prefix.

FieldDescription
donationsOne-time donation configuration.
mountPathURL path prefix for billing routes. Must start with ’/’. Default: /billing.
plansSubscription plans mapped to provider prices. Empty ⇒ no paid plans.
providerPayment provider credentials. Omit to run billing dormant (unconfigured).
urlsCheckout / Portal redirect URLs. Required once a provider is configured.
webhookMaxBodyBytesMaximum body size (bytes) accepted on the Stripe webhook route. Defaults to 1 MiB.

Source: packages/slingshot-billing/src/types/config.ts

Stable identifier for this package; used as the contract/event owner name.

function createBillingPackage(rawConfig: Partial<BillingPackageConfig> = {}, internals: BillingPackageInternals = {},): SlingshotPackageDefinition

Source: packages/slingshot-billing/src/plugin.ts

The narrow storage seam billing’s domain logic runs against.

lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain row types — they never touch an entity adapter directly. The real implementation, createEntityBillingStore, is a thin mapping layer over the framework-resolved entity adapters for billing_customers / billing_subscriptions / billing_payments.

How the adapters are obtained (the runtime wiring, Phase 4/5): package entities registered via definePackage({ entities: [entity({ config })] }) get their adapters published into plugin state during the entity bootstrap; the package resolves them from setupPost onward with maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' }) (precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via wiring: { mode: 'factories', onAdapter } (precedent: slingshot-notifications/src/entities/modules.ts). Either way the adapter only exists inside setup contexts — which is exactly why this seam exists.

function createEntityBillingStore(adapters: BillingEntityAdapters): BillingStore

Source: packages/slingshot-billing/src/lib/store.ts

Stripe implementation of BillingProvider.

The one file allowed to import the Stripe SDK. Everything it returns is billing’s own domain vocabulary (ProviderEvent, HostedSession, …) — no Stripe type crosses the seam. normalizeStripeEvent is exported separately (and typed structurally) so tests can pin the event mapping with minimal fixture objects, no SDK mocking required.

The SDK client is constructed LAZILY on first use: createStripeProvider is only reachable behind the package’s dormant gate (isBillingConfigured), and building the provider object itself performs no I/O and no SDK init.

function createStripeProvider(config: StripeProviderConfig): BillingProvider

Source: packages/slingshot-billing/src/lib/providers/stripe.ts

Pure entitlement derivation: stored subscription rows + configured plans → the single app-agnostic Entitlement answer.

No I/O, no provider knowledge — lib/sync.ts calls this after every subscription mutation, and the Phase 5 capability resolver calls it over the owner’s stored rows.

function deriveEntitlement(rows: readonly BillingSubscriptionRow[], plans: readonly PlanConfig[],): Entitlement

Source: packages/slingshot-billing/src/lib/entitlement.ts

Pure entitlement derivation: stored subscription rows + configured plans → the single app-agnostic Entitlement answer.

No I/O, no provider knowledge — lib/sync.ts calls this after every subscription mutation, and the Phase 5 capability resolver calls it over the owner’s stored rows.

function entitlementEquals(a: Entitlement, b: Entitlement): boolean

Source: packages/slingshot-billing/src/lib/entitlement.ts

Trim trailing slashes and validate a mount path prefix.

function isBillingConfigured(config: BillingPackageConfig): boolean

Source: packages/slingshot-billing/src/types/config.ts

Stripe implementation of BillingProvider.

The one file allowed to import the Stripe SDK. Everything it returns is billing’s own domain vocabulary (ProviderEvent, HostedSession, …) — no Stripe type crosses the seam. normalizeStripeEvent is exported separately (and typed structurally) so tests can pin the event mapping with minimal fixture objects, no SDK mocking required.

The SDK client is constructed LAZILY on first use: createStripeProvider is only reachable behind the package’s dormant gate (isBillingConfigured), and building the provider object itself performs no I/O and no SDK init.

function normalizeStripeEvent(event: StripeEventLike): ProviderEvent

Source: packages/slingshot-billing/src/lib/providers/stripe.ts

Pure entitlement derivation: stored subscription rows + configured plans → the single app-agnostic Entitlement answer.

No I/O, no provider knowledge — lib/sync.ts calls this after every subscription mutation, and the Phase 5 capability resolver calls it over the owner’s stored rows.

function planKeyForPrice(priceId: string | null | undefined, plans: readonly PlanConfig[],): string

Source: packages/slingshot-billing/src/lib/entitlement.ts

Provider-agnostic webhook sync: a normalized ProviderEvent + a BillingStore → idempotent, order-tolerant persistence + a SyncOutcome telling the caller (the Phase 4 webhook route) which event to emit.

Invariants this file owns:

  • Idempotent: replaying the same event is a no-op (changed: false for subscriptions, 'duplicate-payment' for payments). Stripe retries until it sees a 200, so every path here must tolerate a re-run.
  • Order-tolerant: subscription events strictly OLDER than the stored row’s providerEventCreated are DROPPED ('stale-event') — Stripe does not guarantee delivery order. Equal timestamps are applied (Stripe’s created has 1s granularity; an equal-timestamp replay is the idempotent case, not a stale one).
  • Payments never touch entitlement: a donation inserts a billing_payments row and nothing else.
async function syncProviderEvent(event: ProviderEvent, store: BillingStore, plans: readonly PlanConfig[],): Promise<SyncOutcome>

Source: packages/slingshot-billing/src/lib/sync.ts

Source: packages/slingshot-billing/src/entities/customer.ts

Source: packages/slingshot-billing/src/entities/payment.ts

Source: packages/slingshot-billing/src/entities/subscription.ts

Public contract for slingshot-billing.

Consumers (e.g. an app mapping plans onto its own domain) read the entitlement through the typed capability handle rather than reaching into package state. The package publishes the implementation via capabilities.provides; consumers resolve it with ctx.capabilities.require(BillingEntitlementCap).

Source: packages/slingshot-billing/src/public.ts

The narrow storage seam billing’s domain logic runs against.

lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain row types — they never touch an entity adapter directly. The real implementation, createEntityBillingStore, is a thin mapping layer over the framework-resolved entity adapters for billing_customers / billing_subscriptions / billing_payments.

How the adapters are obtained (the runtime wiring, Phase 4/5): package entities registered via definePackage({ entities: [entity({ config })] }) get their adapters published into plugin state during the entity bootstrap; the package resolves them from setupPost onward with maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' }) (precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via wiring: { mode: 'factories', onAdapter } (precedent: slingshot-notifications/src/entities/modules.ts). Either way the adapter only exists inside setup contexts — which is exactly why this seam exists.

Source: packages/slingshot-billing/src/lib/store.ts

Payload for billing:entitlement.changed — an owner’s derived entitlement actually changed after a verified webhook event was synced.

Delivery is fire-and-forget (the bus swallows and logs handler throws), so consumers treat this as a cache-invalidation hint and reconcile via BillingEntitlementCap on read.

Source: packages/slingshot-billing/src/events.ts

The narrow storage seam billing’s domain logic runs against.

lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain row types — they never touch an entity adapter directly. The real implementation, createEntityBillingStore, is a thin mapping layer over the framework-resolved entity adapters for billing_customers / billing_subscriptions / billing_payments.

How the adapters are obtained (the runtime wiring, Phase 4/5): package entities registered via definePackage({ entities: [entity({ config })] }) get their adapters published into plugin state during the entity bootstrap; the package resolves them from setupPost onward with maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' }) (precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via wiring: { mode: 'factories', onAdapter } (precedent: slingshot-notifications/src/entities/modules.ts). Either way the adapter only exists inside setup contexts — which is exactly why this seam exists.

Source: packages/slingshot-billing/src/lib/store.ts

The narrow storage seam billing’s domain logic runs against.

lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain row types — they never touch an entity adapter directly. The real implementation, createEntityBillingStore, is a thin mapping layer over the framework-resolved entity adapters for billing_customers / billing_subscriptions / billing_payments.

How the adapters are obtained (the runtime wiring, Phase 4/5): package entities registered via definePackage({ entities: [entity({ config })] }) get their adapters published into plugin state during the entity bootstrap; the package resolves them from setupPost onward with maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' }) (precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via wiring: { mode: 'factories', onAdapter } (precedent: slingshot-notifications/src/entities/modules.ts). Either way the adapter only exists inside setup contexts — which is exactly why this seam exists.

Source: packages/slingshot-billing/src/lib/store.ts

Stable identifier for this package; used as the contract/event owner name.

Source: packages/slingshot-billing/src/plugin.ts

Payload for billing:entitlement.changed — an owner’s derived entitlement actually changed after a verified webhook event was synced.

Delivery is fire-and-forget (the bus swallows and logs handler throws), so consumers treat this as a cache-invalidation hint and reconcile via BillingEntitlementCap on read.

Source: packages/slingshot-billing/src/events.ts

The narrow storage seam billing’s domain logic runs against.

lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain row types — they never touch an entity adapter directly. The real implementation, createEntityBillingStore, is a thin mapping layer over the framework-resolved entity adapters for billing_customers / billing_subscriptions / billing_payments.

How the adapters are obtained (the runtime wiring, Phase 4/5): package entities registered via definePackage({ entities: [entity({ config })] }) get their adapters published into plugin state during the entity bootstrap; the package resolves them from setupPost onward with maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' }) (precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via wiring: { mode: 'factories', onAdapter } (precedent: slingshot-notifications/src/entities/modules.ts). Either way the adapter only exists inside setup contexts — which is exactly why this seam exists.

Source: packages/slingshot-billing/src/lib/store.ts

Provider abstraction for slingshot-billing.

All billing routes, sync, and entitlement logic speak ONLY this interface plus billing’s own domain types — no provider SDK type ever leaks past this seam. Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a second provider slots in by satisfying BillingProvider with no refactor.

Source: packages/slingshot-billing/src/lib/provider.ts

The narrow storage seam billing’s domain logic runs against.

lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain row types — they never touch an entity adapter directly. The real implementation, createEntityBillingStore, is a thin mapping layer over the framework-resolved entity adapters for billing_customers / billing_subscriptions / billing_payments.

How the adapters are obtained (the runtime wiring, Phase 4/5): package entities registered via definePackage({ entities: [entity({ config })] }) get their adapters published into plugin state during the entity bootstrap; the package resolves them from setupPost onward with maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' }) (precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via wiring: { mode: 'factories', onAdapter } (precedent: slingshot-notifications/src/entities/modules.ts). Either way the adapter only exists inside setup contexts — which is exactly why this seam exists.

Source: packages/slingshot-billing/src/lib/store.ts

The narrow storage seam billing’s domain logic runs against.

lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain row types — they never touch an entity adapter directly. The real implementation, createEntityBillingStore, is a thin mapping layer over the framework-resolved entity adapters for billing_customers / billing_subscriptions / billing_payments.

How the adapters are obtained (the runtime wiring, Phase 4/5): package entities registered via definePackage({ entities: [entity({ config })] }) get their adapters published into plugin state during the entity bootstrap; the package resolves them from setupPost onward with maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' }) (precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via wiring: { mode: 'factories', onAdapter } (precedent: slingshot-notifications/src/entities/modules.ts). Either way the adapter only exists inside setup contexts — which is exactly why this seam exists.

Source: packages/slingshot-billing/src/lib/store.ts

Provider abstraction for slingshot-billing.

All billing routes, sync, and entitlement logic speak ONLY this interface plus billing’s own domain types — no provider SDK type ever leaks past this seam. Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a second provider slots in by satisfying BillingProvider with no refactor.

Source: packages/slingshot-billing/src/lib/provider.ts

Provider abstraction for slingshot-billing.

All billing routes, sync, and entitlement logic speak ONLY this interface plus billing’s own domain types — no provider SDK type ever leaks past this seam. Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a second provider slots in by satisfying BillingProvider with no refactor.

Source: packages/slingshot-billing/src/lib/provider.ts

Public contract for slingshot-billing.

Consumers (e.g. an app mapping plans onto its own domain) read the entitlement through the typed capability handle rather than reaching into package state. The package publishes the implementation via capabilities.provides; consumers resolve it with ctx.capabilities.require(BillingEntitlementCap).

Source: packages/slingshot-billing/src/public.ts

Provider abstraction for slingshot-billing.

All billing routes, sync, and entitlement logic speak ONLY this interface plus billing’s own domain types — no provider SDK type ever leaks past this seam. Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a second provider slots in by satisfying BillingProvider with no refactor.

Source: packages/slingshot-billing/src/lib/provider.ts

Provider abstraction for slingshot-billing.

All billing routes, sync, and entitlement logic speak ONLY this interface plus billing’s own domain types — no provider SDK type ever leaks past this seam. Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a second provider slots in by satisfying BillingProvider with no refactor.

Source: packages/slingshot-billing/src/lib/provider.ts

Provider abstraction for slingshot-billing.

All billing routes, sync, and entitlement logic speak ONLY this interface plus billing’s own domain types — no provider SDK type ever leaks past this seam. Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a second provider slots in by satisfying BillingProvider with no refactor.

Source: packages/slingshot-billing/src/lib/provider.ts

Provider abstraction for slingshot-billing.

All billing routes, sync, and entitlement logic speak ONLY this interface plus billing’s own domain types — no provider SDK type ever leaks past this seam. Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a second provider slots in by satisfying BillingProvider with no refactor.

Source: packages/slingshot-billing/src/lib/provider.ts

Stripe implementation of BillingProvider.

The one file allowed to import the Stripe SDK. Everything it returns is billing’s own domain vocabulary (ProviderEvent, HostedSession, …) — no Stripe type crosses the seam. normalizeStripeEvent is exported separately (and typed structurally) so tests can pin the event mapping with minimal fixture objects, no SDK mocking required.

The SDK client is constructed LAZILY on first use: createStripeProvider is only reachable behind the package’s dormant gate (isBillingConfigured), and building the provider object itself performs no I/O and no SDK init.

Source: packages/slingshot-billing/src/lib/providers/stripe.ts

Provider abstraction for slingshot-billing.

All billing routes, sync, and entitlement logic speak ONLY this interface plus billing’s own domain types — no provider SDK type ever leaks past this seam. Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a second provider slots in by satisfying BillingProvider with no refactor.

Source: packages/slingshot-billing/src/lib/provider.ts

The narrow storage seam billing’s domain logic runs against.

lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain row types — they never touch an entity adapter directly. The real implementation, createEntityBillingStore, is a thin mapping layer over the framework-resolved entity adapters for billing_customers / billing_subscriptions / billing_payments.

How the adapters are obtained (the runtime wiring, Phase 4/5): package entities registered via definePackage({ entities: [entity({ config })] }) get their adapters published into plugin state during the entity bootstrap; the package resolves them from setupPost onward with maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' }) (precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via wiring: { mode: 'factories', onAdapter } (precedent: slingshot-notifications/src/entities/modules.ts). Either way the adapter only exists inside setup contexts — which is exactly why this seam exists.

Source: packages/slingshot-billing/src/lib/store.ts

Trim trailing slashes and validate a mount path prefix.

Source: packages/slingshot-billing/src/types/config.ts

The narrow storage seam billing’s domain logic runs against.

lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain row types — they never touch an entity adapter directly. The real implementation, createEntityBillingStore, is a thin mapping layer over the framework-resolved entity adapters for billing_customers / billing_subscriptions / billing_payments.

How the adapters are obtained (the runtime wiring, Phase 4/5): package entities registered via definePackage({ entities: [entity({ config })] }) get their adapters published into plugin state during the entity bootstrap; the package resolves them from setupPost onward with maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' }) (precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via wiring: { mode: 'factories', onAdapter } (precedent: slingshot-notifications/src/entities/modules.ts). Either way the adapter only exists inside setup contexts — which is exactly why this seam exists.

Source: packages/slingshot-billing/src/lib/store.ts

The narrow storage seam billing’s domain logic runs against.

lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain row types — they never touch an entity adapter directly. The real implementation, createEntityBillingStore, is a thin mapping layer over the framework-resolved entity adapters for billing_customers / billing_subscriptions / billing_payments.

How the adapters are obtained (the runtime wiring, Phase 4/5): package entities registered via definePackage({ entities: [entity({ config })] }) get their adapters published into plugin state during the entity bootstrap; the package resolves them from setupPost onward with maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' }) (precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via wiring: { mode: 'factories', onAdapter } (precedent: slingshot-notifications/src/entities/modules.ts). Either way the adapter only exists inside setup contexts — which is exactly why this seam exists.

Source: packages/slingshot-billing/src/lib/store.ts

The narrow storage seam billing’s domain logic runs against.

lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain row types — they never touch an entity adapter directly. The real implementation, createEntityBillingStore, is a thin mapping layer over the framework-resolved entity adapters for billing_customers / billing_subscriptions / billing_payments.

How the adapters are obtained (the runtime wiring, Phase 4/5): package entities registered via definePackage({ entities: [entity({ config })] }) get their adapters published into plugin state during the entity bootstrap; the package resolves them from setupPost onward with maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' }) (precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via wiring: { mode: 'factories', onAdapter } (precedent: slingshot-notifications/src/entities/modules.ts). Either way the adapter only exists inside setup contexts — which is exactly why this seam exists.

Source: packages/slingshot-billing/src/lib/store.ts

Source: packages/slingshot-billing/src/types/config.ts

Public contract for slingshot-billing.

Consumers (e.g. an app mapping plans onto its own domain) read the entitlement through the typed capability handle rather than reaching into package state. The package publishes the implementation via capabilities.provides; consumers resolve it with ctx.capabilities.require(BillingEntitlementCap).

Source: packages/slingshot-billing/src/public.ts

Source: packages/slingshot-billing/src/types/config.ts

Provider abstraction for slingshot-billing.

All billing routes, sync, and entitlement logic speak ONLY this interface plus billing’s own domain types — no provider SDK type ever leaks past this seam. Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a second provider slots in by satisfying BillingProvider with no refactor.

Source: packages/slingshot-billing/src/lib/provider.ts

Source: packages/slingshot-billing/src/types/config.ts

Provider abstraction for slingshot-billing.

All billing routes, sync, and entitlement logic speak ONLY this interface plus billing’s own domain types — no provider SDK type ever leaks past this seam. Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a second provider slots in by satisfying BillingProvider with no refactor.

Source: packages/slingshot-billing/src/lib/provider.ts

Provider-agnostic webhook sync: a normalized ProviderEvent + a BillingStore → idempotent, order-tolerant persistence + a SyncOutcome telling the caller (the Phase 4 webhook route) which event to emit.

Invariants this file owns:

  • Idempotent: replaying the same event is a no-op (changed: false for subscriptions, 'duplicate-payment' for payments). Stripe retries until it sees a 200, so every path here must tolerate a re-run.
  • Order-tolerant: subscription events strictly OLDER than the stored row’s providerEventCreated are DROPPED ('stale-event') — Stripe does not guarantee delivery order. Equal timestamps are applied (Stripe’s created has 1s granularity; an equal-timestamp replay is the idempotent case, not a stale one).
  • Payments never touch entitlement: a donation inserts a billing_payments row and nothing else.

Source: packages/slingshot-billing/src/lib/sync.ts

Provider-agnostic webhook sync: a normalized ProviderEvent + a BillingStore → idempotent, order-tolerant persistence + a SyncOutcome telling the caller (the Phase 4 webhook route) which event to emit.

Invariants this file owns:

  • Idempotent: replaying the same event is a no-op (changed: false for subscriptions, 'duplicate-payment' for payments). Stripe retries until it sees a 200, so every path here must tolerate a re-run.
  • Order-tolerant: subscription events strictly OLDER than the stored row’s providerEventCreated are DROPPED ('stale-event') — Stripe does not guarantee delivery order. Equal timestamps are applied (Stripe’s created has 1s granularity; an equal-timestamp replay is the idempotent case, not a stale one).
  • Payments never touch entitlement: a donation inserts a billing_payments row and nothing else.

Source: packages/slingshot-billing/src/lib/sync.ts