@fonderie/billing 5.1.0 → 5.3.1

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/middlewares/require-plan.ts","../../src/services/subscriptions.ts","../../src/utils.ts","../../src/middlewares/billing.ts","../../src/config.ts","../../src/services/policy.ts"],"sourcesContent":["import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { getSubscription } from '../services/subscriptions';\nimport { resolveSubscriber } from '../utils';\n\n// Gates a route behind a minimum plan.\n// Works for both user-level and workspace-level subscriptions.\n// Usage: requirePlan(['pro', 'enterprise'], store)\n\nfunction makeHandler(plans: string | string[], store: IStoreAdapter): Middleware {\n\tconst allowed = Array.isArray(plans) ? plans : [plans];\n\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst subscriber = resolveSubscriber(ctx);\n\t\tif (!subscriber) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'SUBSCRIBER_REQUIRED', 'Subscriber context required');\n\t\t}\n\n\t\tconst subscription = await getSubscription(subscriber.type, subscriber.id, store);\n\n\t\tif (!subscription || !allowed.includes(subscription.plan)) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.PAYMENT_REQUIRED,\n\t\t\t\t'PLAN_UPGRADE_REQUIRED',\n\t\t\t\t'Plan upgrade required',\n\t\t\t\t{ required: allowed, current: subscription?.plan ?? 'none' },\n\t\t\t);\n\t\t}\n\n\t\tif (subscription.status !== 'active' && subscription.status !== 'trialing') {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.PAYMENT_REQUIRED,\n\t\t\t\t'SUBSCRIPTION_INACTIVE',\n\t\t\t\t'Subscription is not active',\n\t\t\t\t{ status: subscription.status },\n\t\t\t);\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requirePlan(plans: string | string[], store: IStoreAdapter): Middleware;\nexport function requirePlan(\n\tplans: string | string[],\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requirePlan(\n\tplans: string | string[],\n\tstore: IStoreAdapter,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(plans, store);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ISubscription, SubscriberType } from '../types';\n\nconst SELECT_SUBSCRIPTION = `\n\tSELECT\n\t\tid,\n\t\tsubscriber_type AS \"subscriberType\",\n\t\tsubscriber_id AS \"subscriberId\",\n\t\tplan,\n\t\tinterval,\n\t\tstatus,\n\t\tprovider_customer_id AS \"providerCustomerId\",\n\t\tprovider_subscription_id AS \"providerSubscriptionId\",\n\t\tcurrent_period_start AS \"currentPeriodStart\",\n\t\tcurrent_period_end AS \"currentPeriodEnd\",\n\t\tcancel_at_period_end AS \"cancelAtPeriodEnd\",\n\t\ttrial_ends_at AS \"trialEndsAt\",\n\t\tcreated_at AS \"createdAt\"\n\tFROM fonderie_subscriptions`;\n\nexport async function getSubscription(\n\tsubscriberType: SubscriberType,\n\tsubscriberId: string,\n\tstore: IStoreAdapter,\n): Promise<ISubscription | null> {\n\tconst [row] = await store.query<ISubscription>(\n\t\t`${SELECT_SUBSCRIPTION} WHERE subscriber_type = $1 AND subscriber_id = $2`,\n\t\t[subscriberType, subscriberId],\n\t);\n\treturn row ?? null;\n}\n\nexport async function upsertSubscription(\n\tdata: {\n\t\tsubscriberType: SubscriberType;\n\t\tsubscriberId: string;\n\t\tplan: string;\n\t\tinterval?: 'month' | 'year';\n\t\tstatus: string;\n\t\tproviderCustomerId?: string;\n\t\tproviderSubscriptionId?: string;\n\t\tcurrentPeriodStart?: Date;\n\t\tcurrentPeriodEnd?: Date;\n\t\tcancelAtPeriodEnd?: boolean;\n\t\ttrialEndsAt?: Date | null;\n\t},\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`INSERT INTO fonderie_subscriptions\n\t\t\t(subscriber_type, subscriber_id, plan, interval, status,\n\t\t\t provider_customer_id, provider_subscription_id,\n\t\t\t current_period_start, current_period_end,\n\t\t\t cancel_at_period_end, trial_ends_at)\n\t\t VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)\n\t\t ON CONFLICT (subscriber_type, subscriber_id) DO UPDATE SET\n\t\t\t plan = $3,\n\t\t\t interval = $4,\n\t\t\t status = $5,\n\t\t\t provider_customer_id = COALESCE($6, fonderie_subscriptions.provider_customer_id),\n\t\t\t provider_subscription_id = COALESCE($7, fonderie_subscriptions.provider_subscription_id),\n\t\t\t current_period_start = $8,\n\t\t\t current_period_end = $9,\n\t\t\t cancel_at_period_end = $10,\n\t\t\t trial_ends_at = $11`,\n\t\t[\n\t\t\tdata.subscriberType,\n\t\t\tdata.subscriberId,\n\t\t\tdata.plan,\n\t\t\tdata.interval ?? 'month',\n\t\t\tdata.status,\n\t\t\tdata.providerCustomerId ?? null,\n\t\t\tdata.providerSubscriptionId ?? null,\n\t\t\tdata.currentPeriodStart ?? null,\n\t\t\tdata.currentPeriodEnd ?? null,\n\t\t\tdata.cancelAtPeriodEnd ?? false,\n\t\t\tdata.trialEndsAt ?? null,\n\t\t],\n\t);\n}\n","import type { IFonderieContext } from '@fonderie/core';\n\nimport type { SubscriberType } from './types';\n\nexport interface ISubscriber {\n\ttype: SubscriberType;\n\tid: string;\n}\n\n// Converts window strings like '1d', '30d', '1h' to milliseconds.\nexport function parseWindowMs(window: string): number {\n\tconst n = parseInt(window, 10);\n\tconst unit = window.slice(String(n).length);\n\tswitch (unit) {\n\t\tcase 'h':\n\t\t\treturn n * 3_600_000;\n\t\tcase 'd':\n\t\t\treturn n * 86_400_000;\n\t\tcase 'm':\n\t\t\treturn n * 60_000;\n\t\tdefault:\n\t\t\tthrow new Error(`Unknown window unit: '${unit}' in '${window}'`);\n\t}\n}\n\n// Resolves billing subscriber from request context.\n// Precedence: X-Workspace-ID header → ctx.workspace (set by withWorkspace) → ctx.user\nexport function resolveSubscriber(ctx: IFonderieContext): ISubscriber | null {\n\tconst wsFromHeader = ctx.request.headers.get('x-workspace-id');\n\n\tif (wsFromHeader) {\n\t\treturn {\n\t\t\ttype: 'workspace',\n\t\t\tid: wsFromHeader,\n\t\t};\n\t}\n\n\tif (ctx.workspace?.id) {\n\t\treturn {\n\t\t\ttype: 'workspace',\n\t\t\tid: ctx.workspace.id,\n\t\t};\n\t}\n\n\tif (ctx.user?.id) {\n\t\treturn {\n\t\t\ttype: 'user',\n\t\t\tid: ctx.user.id,\n\t\t};\n\t}\n\n\treturn null;\n}\n","import type { Middleware, ICourierMessage } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport type { ICounterBackend } from '../backends/types';\nimport { MESSAGE_KEYS } from '../config';\nimport { getSubscription } from '../services/subscriptions';\nimport { buildBillingContext } from '../services/policy';\nimport { resolveSubscriber, parseWindowMs } from '../utils';\n\n// In-process de-dup: tracks which threshold notifications have fired this session.\n// Acceptable to lose on restart (may send one duplicate after a redeploy).\nconst notified = new Set<string>();\n\nexport function withBilling(\n\tstore: IStoreAdapter,\n\tconfig: IBillingConfig,\n\tbackend: ICounterBackend,\n): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t// No subscriber (unauthenticated / public route) — skip entirely\n\t\tif (!subscriber) return next();\n\n\t\t// Resolve subscription → plan name (fall back to first plan = free)\n\t\tconst subscription = await getSubscription(subscriber.type, subscriber.id, store);\n\t\tconst planName = subscription?.plan ?? config.plans[0]?.name ?? 'free';\n\t\tconst active =\n\t\t\t!subscription || subscription.status === 'active' || subscription.status === 'trialing';\n\n\t\tconst plan = config.plans.find((p) => p.name === planName) ?? config.plans[0];\n\t\tif (!plan) return next();\n\n\t\t// Increment windowed (rate-limit) counters and read their current totals\n\t\tconst counters: Record<string, number> = {};\n\n\t\tfor (const [key, entry] of Object.entries(plan.policy ?? {})) {\n\t\t\tif ('enabled' in entry || !entry.window) continue;\n\n\t\t\tconst windowMs = parseWindowMs(entry.window);\n\t\t\tconst counterKey = `${subscriber.type}:${subscriber.id}:${key}`;\n\t\t\tcounters[key] = await backend.increment(counterKey, windowMs);\n\t\t}\n\n\t\t// Build and cache billing context on ctx\n\t\tconst billingCtx = buildBillingContext({ subscriber, plan, active, counters });\n\t\tctx.meta['billing'] = billingCtx;\n\n\t\t// Block requests that have hit a hard limit\n\t\tfor (const [key, status] of Object.entries(billingCtx.statuses)) {\n\t\t\tif (status.type === 'counter' && status.status === 'blocked') {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.TOO_MANY_REQUESTS,\n\t\t\t\t\t'RATE_LIMIT_EXCEEDED',\n\t\t\t\t\t`Limit exceeded for: ${key}`,\n\t\t\t\t\t{ key, limit: status.limit, used: status.used, resetsAt: status.resetsAt },\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Fire threshold notifications (once per subscriber per key per session)\n\t\tif (config.notifications) {\n\t\t\tconst toNotify: ICourierMessage[] = [];\n\t\t\tconst recipient = {\n\t\t\t\temail: ctx.user?.email ?? null,\n\t\t\t\tphone: null,\n\t\t\t\tdeviceToken: null,\n\t\t\t};\n\n\t\t\tfor (const [key, status] of Object.entries(billingCtx.statuses)) {\n\t\t\t\tif (status.type !== 'counter' || status.limit === null) continue;\n\n\t\t\t\tconst base = `${subscriber.type}:${subscriber.id}:${key}`;\n\n\t\t\t\tif (config.notifications.softHit && status.status === 'over_limit') {\n\t\t\t\t\tconst nk = `${base}:reached`;\n\t\t\t\t\tif (!notified.has(nk)) {\n\t\t\t\t\t\tnotified.add(nk);\n\t\t\t\t\t\ttoNotify.push({\n\t\t\t\t\t\t\ttype: MESSAGE_KEYS.limitReached,\n\t\t\t\t\t\t\trecipient,\n\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\t\tplan: plan.name,\n\t\t\t\t\t\t\t\tlimit: status.limit,\n\t\t\t\t\t\t\t\tused: status.used,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else if (config.notifications.warnAt && status.status === 'warning') {\n\t\t\t\t\tconst nk = `${base}:warning`;\n\t\t\t\t\tif (!notified.has(nk)) {\n\t\t\t\t\t\tnotified.add(nk);\n\t\t\t\t\t\ttoNotify.push({\n\t\t\t\t\t\t\ttype: MESSAGE_KEYS.limitWarning,\n\t\t\t\t\t\t\trecipient,\n\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\t\tplan: plan.name,\n\t\t\t\t\t\t\t\tlimit: status.limit,\n\t\t\t\t\t\t\t\tused: status.used,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (toNotify.length > 0) {\n\t\t\t\tconst existing = ctx.meta['messages'] as ICourierMessage[] | undefined;\n\t\t\t\tctx.meta['messages'] = [...(existing ?? []), ...toNotify];\n\t\t\t}\n\t\t}\n\n\t\treturn next();\n\t};\n}\n","import type { IBillingProvider } from './providers/types';\nimport type { PolicyEntry } from './types';\nimport type { ICounterBackend } from './backends/types';\n\nexport interface IBillingPlanPrice {\n\tamount: number;\n\tpriceId?: string;\n}\n\nexport interface IBillingPlanDefaults {\n\twarnAt?: number; // default warnAt fraction (0–1) for counter policies\n\tbuffer?: number; // default buffer for counter policies\n}\n\nexport interface IBillingPlan {\n\tname: string;\n\tdescription?: string;\n\ttier?: number;\n\ttrialDays?: number;\n\tmonthly?: IBillingPlanPrice;\n\tyearly?: IBillingPlanPrice;\n\tdefaults?: IBillingPlanDefaults;\n\tpolicy?: Record<string, PolicyEntry>;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport type RateLimitBackendConfig = 'memory' | 'db' | ICounterBackend;\n\nexport interface IBillingNotificationsConfig {\n\twarnAt?: boolean; // fire courier message when warnAt threshold crossed\n\tsoftHit?: boolean; // fire when soft limit crossed\n}\n\nexport interface IBillingConfig {\n\tprovider: IBillingProvider;\n\tplans: IBillingPlan[];\n\tsuccessUrl: string;\n\tcancelUrl: string;\n\twebhookSecret?: string;\n\trateLimit?: { backend?: RateLimitBackendConfig };\n\tnotifications?: IBillingNotificationsConfig;\n}\n\nexport const MESSAGE_KEYS = {\n\tlimitWarning: 'billing.limit-warning',\n\tlimitReached: 'billing.limit-reached',\n\tlimitBlocked: 'billing.limit-blocked',\n} as const;\n\nexport type BillingMessageKey = (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS];\n","import type { IBillingPlan } from '../config';\nimport type { LimitStatus, IPolicyStatus, IBillingContext, SubscriberType } from '../types';\nimport { parseWindowMs } from '../utils';\n\nexport function buildBillingContext(opts: {\n\tsubscriber: { type: SubscriberType; id: string };\n\tplan: IBillingPlan;\n\tactive: boolean;\n\t// Pre-fetched windowed counter values keyed by policy key.\n\t// Non-windowed counter keys are absent (their used count is 0 — app manages those).\n\tcounters: Record<string, number>;\n}): IBillingContext {\n\tconst { subscriber, plan, active, counters } = opts;\n\tconst defaults = plan.defaults ?? {};\n\tconst statuses: Record<string, IPolicyStatus> = {};\n\n\tfor (const [key, entry] of Object.entries(plan.policy ?? {})) {\n\t\tif ('enabled' in entry) {\n\t\t\tstatuses[key] = { type: 'feature', enabled: entry.enabled };\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { limit, buffer = defaults.buffer ?? 0, warnAt = defaults.warnAt ?? 0.8, window } = entry;\n\n\t\tconst used = counters[key] ?? 0;\n\t\tconst hardLimit = limit !== null ? limit + buffer : null;\n\n\t\tlet status: LimitStatus = 'ok';\n\t\tif (hardLimit !== null && used >= hardLimit) status = 'blocked';\n\t\telse if (limit !== null && used >= limit) status = 'over_limit';\n\t\telse if (limit !== null && used >= limit * warnAt) status = 'warning';\n\n\t\tlet resetsAt: string | null = null;\n\t\tif (window) {\n\t\t\tconst windowMs = parseWindowMs(window);\n\t\t\tconst windowStart = Math.floor(Date.now() / windowMs) * windowMs;\n\t\t\tresetsAt = new Date(windowStart + windowMs).toISOString();\n\t\t}\n\n\t\tstatuses[key] = { type: 'counter', limit, used, status, resetsAt };\n\t}\n\n\treturn { subscriber, plan: plan.name, active, statuses };\n}\n"],"mappings":";AAAA,SAAS,gBAAgB,YAAY;;;ACIrC,IAAM,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiB5B,eAAsB,gBACrB,gBACA,cACA,OACgC;AAChC,QAAM,CAAC,GAAG,IAAI,MAAM,MAAM;AAAA,IACzB,GAAG,mBAAmB;AAAA,IACtB,CAAC,gBAAgB,YAAY;AAAA,EAC9B;AACA,SAAO,OAAO;AACf;;;ACrBO,SAAS,cAAc,QAAwB;AACrD,QAAM,IAAI,SAAS,QAAQ,EAAE;AAC7B,QAAM,OAAO,OAAO,MAAM,OAAO,CAAC,EAAE,MAAM;AAC1C,UAAQ,MAAM;AAAA,IACb,KAAK;AACJ,aAAO,IAAI;AAAA,IACZ,KAAK;AACJ,aAAO,IAAI;AAAA,IACZ,KAAK;AACJ,aAAO,IAAI;AAAA,IACZ;AACC,YAAM,IAAI,MAAM,yBAAyB,IAAI,SAAS,MAAM,GAAG;AAAA,EACjE;AACD;AAIO,SAAS,kBAAkB,KAA2C;AAC5E,QAAM,eAAe,IAAI,QAAQ,QAAQ,IAAI,gBAAgB;AAE7D,MAAI,cAAc;AACjB,WAAO;AAAA,MACN,MAAM;AAAA,MACN,IAAI;AAAA,IACL;AAAA,EACD;AAEA,MAAI,IAAI,WAAW,IAAI;AACtB,WAAO;AAAA,MACN,MAAM;AAAA,MACN,IAAI,IAAI,UAAU;AAAA,IACnB;AAAA,EACD;AAEA,MAAI,IAAI,MAAM,IAAI;AACjB,WAAO;AAAA,MACN,MAAM;AAAA,MACN,IAAI,IAAI,KAAK;AAAA,IACd;AAAA,EACD;AAEA,SAAO;AACR;;;AFxCA,SAAS,YAAY,OAA0B,OAAkC;AAChF,QAAM,UAAU,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AAErD,SAAO,OAAO,KAAK,SAAS;AAC3B,QAAI,CAAC,IAAI,MAAM;AACd,aAAO,eAAe,KAAK,cAAc,gBAAgB,cAAc;AAAA,IACxE;AAEA,UAAM,aAAa,kBAAkB,GAAG;AACxC,QAAI,CAAC,YAAY;AAChB,aAAO,eAAe,KAAK,aAAa,uBAAuB,6BAA6B;AAAA,IAC7F;AAEA,UAAM,eAAe,MAAM,gBAAgB,WAAW,MAAM,WAAW,IAAI,KAAK;AAEhF,QAAI,CAAC,gBAAgB,CAAC,QAAQ,SAAS,aAAa,IAAI,GAAG;AAC1D,aAAO;AAAA,QACN,KAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA,EAAE,UAAU,SAAS,SAAS,cAAc,QAAQ,OAAO;AAAA,MAC5D;AAAA,IACD;AAEA,QAAI,aAAa,WAAW,YAAY,aAAa,WAAW,YAAY;AAC3E,aAAO;AAAA,QACN,KAAK;AAAA,QACL;AAAA,QACA;AAAA,QACA,EAAE,QAAQ,aAAa,OAAO;AAAA,MAC/B;AAAA,IACD;AAEA,WAAO,KAAK;AAAA,EACb;AACD;AASO,SAAS,YACf,OACA,OACA,KACA,MACiC;AACjC,QAAM,UAAU,YAAY,OAAO,KAAK;AACxC,MAAI,QAAQ,UAAa,SAAS,OAAW,QAAO,QAAQ,KAAK,IAAI;AACrE,SAAO;AACR;;;AGhEA,SAAS,kBAAAA,iBAAgB,QAAAC,aAAY;;;AC0C9B,IAAM,eAAe;AAAA,EAC3B,cAAc;AAAA,EACd,cAAc;AAAA,EACd,cAAc;AACf;;;AC3CO,SAAS,oBAAoB,MAOhB;AACnB,QAAM,EAAE,YAAY,MAAM,QAAQ,SAAS,IAAI;AAC/C,QAAM,WAAW,KAAK,YAAY,CAAC;AACnC,QAAM,WAA0C,CAAC;AAEjD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC,GAAG;AAC7D,QAAI,aAAa,OAAO;AACvB,eAAS,GAAG,IAAI,EAAE,MAAM,WAAW,SAAS,MAAM,QAAQ;AAC1D;AAAA,IACD;AAEA,UAAM,EAAE,OAAO,SAAS,SAAS,UAAU,GAAG,SAAS,SAAS,UAAU,KAAK,OAAO,IAAI;AAE1F,UAAM,OAAO,SAAS,GAAG,KAAK;AAC9B,UAAM,YAAY,UAAU,OAAO,QAAQ,SAAS;AAEpD,QAAI,SAAsB;AAC1B,QAAI,cAAc,QAAQ,QAAQ,UAAW,UAAS;AAAA,aAC7C,UAAU,QAAQ,QAAQ,MAAO,UAAS;AAAA,aAC1C,UAAU,QAAQ,QAAQ,QAAQ,OAAQ,UAAS;AAE5D,QAAI,WAA0B;AAC9B,QAAI,QAAQ;AACX,YAAM,WAAW,cAAc,MAAM;AACrC,YAAM,cAAc,KAAK,MAAM,KAAK,IAAI,IAAI,QAAQ,IAAI;AACxD,iBAAW,IAAI,KAAK,cAAc,QAAQ,EAAE,YAAY;AAAA,IACzD;AAEA,aAAS,GAAG,IAAI,EAAE,MAAM,WAAW,OAAO,MAAM,QAAQ,SAAS;AAAA,EAClE;AAEA,SAAO,EAAE,YAAY,MAAM,KAAK,MAAM,QAAQ,SAAS;AACxD;;;AF9BA,IAAM,WAAW,oBAAI,IAAY;AAE1B,SAAS,YACf,OACA,QACA,SACa;AACb,SAAO,OAAO,KAAK,SAAS;AAC3B,UAAM,aAAa,kBAAkB,GAAG;AAGxC,QAAI,CAAC,WAAY,QAAO,KAAK;AAG7B,UAAM,eAAe,MAAM,gBAAgB,WAAW,MAAM,WAAW,IAAI,KAAK;AAChF,UAAM,WAAW,cAAc,QAAQ,OAAO,MAAM,CAAC,GAAG,QAAQ;AAChE,UAAM,SACL,CAAC,gBAAgB,aAAa,WAAW,YAAY,aAAa,WAAW;AAE9E,UAAM,OAAO,OAAO,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ,KAAK,OAAO,MAAM,CAAC;AAC5E,QAAI,CAAC,KAAM,QAAO,KAAK;AAGvB,UAAM,WAAmC,CAAC;AAE1C,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC,GAAG;AAC7D,UAAI,aAAa,SAAS,CAAC,MAAM,OAAQ;AAEzC,YAAM,WAAW,cAAc,MAAM,MAAM;AAC3C,YAAM,aAAa,GAAG,WAAW,IAAI,IAAI,WAAW,EAAE,IAAI,GAAG;AAC7D,eAAS,GAAG,IAAI,MAAM,QAAQ,UAAU,YAAY,QAAQ;AAAA,IAC7D;AAGA,UAAM,aAAa,oBAAoB,EAAE,YAAY,MAAM,QAAQ,SAAS,CAAC;AAC7E,QAAI,KAAK,SAAS,IAAI;AAGtB,eAAW,CAAC,KAAK,MAAM,KAAK,OAAO,QAAQ,WAAW,QAAQ,GAAG;AAChE,UAAI,OAAO,SAAS,aAAa,OAAO,WAAW,WAAW;AAC7D,eAAOC;AAAA,UACNC,MAAK;AAAA,UACL;AAAA,UACA,uBAAuB,GAAG;AAAA,UAC1B,EAAE,KAAK,OAAO,OAAO,OAAO,MAAM,OAAO,MAAM,UAAU,OAAO,SAAS;AAAA,QAC1E;AAAA,MACD;AAAA,IACD;AAGA,QAAI,OAAO,eAAe;AACzB,YAAM,WAA8B,CAAC;AACrC,YAAM,YAAY;AAAA,QACjB,OAAO,IAAI,MAAM,SAAS;AAAA,QAC1B,OAAO;AAAA,QACP,aAAa;AAAA,MACd;AAEA,iBAAW,CAAC,KAAK,MAAM,KAAK,OAAO,QAAQ,WAAW,QAAQ,GAAG;AAChE,YAAI,OAAO,SAAS,aAAa,OAAO,UAAU,KAAM;AAExD,cAAM,OAAO,GAAG,WAAW,IAAI,IAAI,WAAW,EAAE,IAAI,GAAG;AAEvD,YAAI,OAAO,cAAc,WAAW,OAAO,WAAW,cAAc;AACnE,gBAAM,KAAK,GAAG,IAAI;AAClB,cAAI,CAAC,SAAS,IAAI,EAAE,GAAG;AACtB,qBAAS,IAAI,EAAE;AACf,qBAAS,KAAK;AAAA,cACb,MAAM,aAAa;AAAA,cACnB;AAAA,cACA,MAAM;AAAA,gBACL;AAAA,gBACA,MAAM,KAAK;AAAA,gBACX,OAAO,OAAO;AAAA,gBACd,MAAM,OAAO;AAAA,cACd;AAAA,YACD,CAAC;AAAA,UACF;AAAA,QACD,WAAW,OAAO,cAAc,UAAU,OAAO,WAAW,WAAW;AACtE,gBAAM,KAAK,GAAG,IAAI;AAClB,cAAI,CAAC,SAAS,IAAI,EAAE,GAAG;AACtB,qBAAS,IAAI,EAAE;AACf,qBAAS,KAAK;AAAA,cACb,MAAM,aAAa;AAAA,cACnB;AAAA,cACA,MAAM;AAAA,gBACL;AAAA,gBACA,MAAM,KAAK;AAAA,gBACX,OAAO,OAAO;AAAA,gBACd,MAAM,OAAO;AAAA,cACd;AAAA,YACD,CAAC;AAAA,UACF;AAAA,QACD;AAAA,MACD;AAEA,UAAI,SAAS,SAAS,GAAG;AACxB,cAAM,WAAW,IAAI,KAAK,UAAU;AACpC,YAAI,KAAK,UAAU,IAAI,CAAC,GAAI,YAAY,CAAC,GAAI,GAAG,QAAQ;AAAA,MACzD;AAAA,IACD;AAEA,WAAO,KAAK;AAAA,EACb;AACD;","names":["setApiResponse","HTTP","setApiResponse","HTTP"]}
1
+ {"version":3,"sources":["../../src/middlewares/require-plan.ts","../../src/services/subscriptions.ts","../../src/utils.ts","../../src/middlewares/billing.ts","../../src/config.ts","../../src/services/policy.ts"],"sourcesContent":["import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { getSubscription } from '../services/subscriptions';\nimport { resolveSubscriber } from '../utils';\n\n// Gates a route behind a minimum plan.\n// Works for both user-level and workspace-level subscriptions.\n// Usage: requirePlan(['pro', 'enterprise'], store)\n\nfunction makeHandler(plans: string | string[], store: IStoreAdapter): Middleware {\n\tconst allowed = Array.isArray(plans) ? plans : [plans];\n\n\treturn async (ctx, next) => {\n\t\tif (!ctx.user) {\n\t\t\treturn setApiResponse(HTTP.UNAUTHORIZED, 'UNAUTHORIZED', 'Unauthorized');\n\t\t}\n\n\t\tconst subscriber = resolveSubscriber(ctx);\n\t\tif (!subscriber) {\n\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'SUBSCRIBER_REQUIRED', 'Subscriber context required');\n\t\t}\n\n\t\tconst subscription = await getSubscription(subscriber.type, subscriber.id, store);\n\n\t\tif (!subscription || !allowed.includes(subscription.plan)) {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.PAYMENT_REQUIRED,\n\t\t\t\t'PLAN_UPGRADE_REQUIRED',\n\t\t\t\t'Plan upgrade required',\n\t\t\t\t{ required: allowed, current: subscription?.plan ?? 'none' },\n\t\t\t);\n\t\t}\n\n\t\tif (subscription.status !== 'active' && subscription.status !== 'trialing') {\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.PAYMENT_REQUIRED,\n\t\t\t\t'SUBSCRIPTION_INACTIVE',\n\t\t\t\t'Subscription is not active',\n\t\t\t\t{ status: subscription.status },\n\t\t\t);\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\nexport function requirePlan(plans: string | string[], store: IStoreAdapter): Middleware;\nexport function requirePlan(\n\tplans: string | string[],\n\tstore: IStoreAdapter,\n\tctx: IFonderieContext,\n\tnext: () => Promise<Response>,\n): Promise<Response>;\nexport function requirePlan(\n\tplans: string | string[],\n\tstore: IStoreAdapter,\n\tctx?: IFonderieContext,\n\tnext?: () => Promise<Response>,\n): Middleware | Promise<Response> {\n\tconst handler = makeHandler(plans, store);\n\tif (ctx !== undefined && next !== undefined) return handler(ctx, next);\n\treturn handler;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ISubscription, SubscriberType } from '../types';\n\nconst SELECT_SUBSCRIPTION = `\n\tSELECT\n\t\tid,\n\t\tsubscriber_type AS \"subscriberType\",\n\t\tsubscriber_id AS \"subscriberId\",\n\t\tplan,\n\t\tinterval,\n\t\tstatus,\n\t\tprovider_customer_id AS \"providerCustomerId\",\n\t\tprovider_subscription_id AS \"providerSubscriptionId\",\n\t\tcurrent_period_start AS \"currentPeriodStart\",\n\t\tcurrent_period_end AS \"currentPeriodEnd\",\n\t\tcancel_at_period_end AS \"cancelAtPeriodEnd\",\n\t\ttrial_ends_at AS \"trialEndsAt\",\n\t\tcreated_at AS \"createdAt\"\n\tFROM fonderie_subscriptions`;\n\nexport async function getSubscription(\n\tsubscriberType: SubscriberType,\n\tsubscriberId: string,\n\tstore: IStoreAdapter,\n): Promise<ISubscription | null> {\n\tconst [row] = await store.query<ISubscription>(\n\t\t`${SELECT_SUBSCRIPTION} WHERE subscriber_type = $1 AND subscriber_id = $2`,\n\t\t[subscriberType, subscriberId],\n\t);\n\treturn row ?? null;\n}\n\nexport async function upsertSubscription(\n\tdata: {\n\t\tsubscriberType: SubscriberType;\n\t\tsubscriberId: string;\n\t\tplan: string;\n\t\tinterval?: 'month' | 'year';\n\t\tstatus: string;\n\t\tproviderCustomerId?: string;\n\t\tproviderSubscriptionId?: string;\n\t\tcurrentPeriodStart?: Date;\n\t\tcurrentPeriodEnd?: Date;\n\t\tcancelAtPeriodEnd?: boolean;\n\t\ttrialEndsAt?: Date | null;\n\t},\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`INSERT INTO fonderie_subscriptions\n\t\t\t(subscriber_type, subscriber_id, plan, interval, status,\n\t\t\t provider_customer_id, provider_subscription_id,\n\t\t\t current_period_start, current_period_end,\n\t\t\t cancel_at_period_end, trial_ends_at)\n\t\t VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)\n\t\t ON CONFLICT (subscriber_type, subscriber_id) DO UPDATE SET\n\t\t\t plan = $3,\n\t\t\t interval = $4,\n\t\t\t status = $5,\n\t\t\t provider_customer_id = COALESCE($6, fonderie_subscriptions.provider_customer_id),\n\t\t\t provider_subscription_id = COALESCE($7, fonderie_subscriptions.provider_subscription_id),\n\t\t\t current_period_start = $8,\n\t\t\t current_period_end = $9,\n\t\t\t cancel_at_period_end = $10,\n\t\t\t trial_ends_at = $11`,\n\t\t[\n\t\t\tdata.subscriberType,\n\t\t\tdata.subscriberId,\n\t\t\tdata.plan,\n\t\t\tdata.interval ?? 'month',\n\t\t\tdata.status,\n\t\t\tdata.providerCustomerId ?? null,\n\t\t\tdata.providerSubscriptionId ?? null,\n\t\t\tdata.currentPeriodStart ?? null,\n\t\t\tdata.currentPeriodEnd ?? null,\n\t\t\tdata.cancelAtPeriodEnd ?? false,\n\t\t\tdata.trialEndsAt ?? null,\n\t\t],\n\t);\n}\n","import type { IFonderieContext } from '@fonderie/core';\n\nimport type { SubscriberType } from './types';\n\nexport interface ISubscriber {\n\ttype: SubscriberType;\n\tid: string;\n}\n\n// Converts window strings like '1d', '30d', '1h' to milliseconds.\nexport function parseWindowMs(window: string): number {\n\tconst n = parseInt(window, 10);\n\tconst unit = window.slice(String(n).length);\n\tswitch (unit) {\n\t\tcase 'h':\n\t\t\treturn n * 3_600_000;\n\t\tcase 'd':\n\t\t\treturn n * 86_400_000;\n\t\tcase 'm':\n\t\t\treturn n * 60_000;\n\t\tdefault:\n\t\t\tthrow new Error(`Unknown window unit: '${unit}' in '${window}'`);\n\t}\n}\n\n// Resolves billing subscriber from request context.\n// Precedence: X-Workspace-ID header → ctx.workspace (set by withWorkspace) → ctx.user\nexport function resolveSubscriber(ctx: IFonderieContext): ISubscriber | null {\n\tconst wsFromHeader = ctx.request.headers.get('x-workspace-id');\n\n\tif (wsFromHeader) {\n\t\treturn {\n\t\t\ttype: 'workspace',\n\t\t\tid: wsFromHeader,\n\t\t};\n\t}\n\n\tif (ctx.workspace?.id) {\n\t\treturn {\n\t\t\ttype: 'workspace',\n\t\t\tid: ctx.workspace.id,\n\t\t};\n\t}\n\n\tif (ctx.user?.id) {\n\t\treturn {\n\t\t\ttype: 'user',\n\t\t\tid: ctx.user.id,\n\t\t};\n\t}\n\n\treturn null;\n}\n","import type { Middleware, ICourierMessage } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport type { ICounterBackend } from '../backends/types';\nimport { MESSAGE_KEYS } from '../config';\nimport { getSubscription } from '../services/subscriptions';\nimport { buildBillingContext } from '../services/policy';\nimport { resolveSubscriber, parseWindowMs } from '../utils';\n\n// In-process de-dup: tracks which threshold notifications have fired this session.\n// Acceptable to lose on restart (may send one duplicate after a redeploy).\nconst notified = new Set<string>();\n\nexport function withBilling(\n\tstore: IStoreAdapter,\n\tconfig: IBillingConfig,\n\tbackend: ICounterBackend,\n): Middleware {\n\treturn async (ctx, next) => {\n\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t// No subscriber (unauthenticated / public route) — skip entirely\n\t\tif (!subscriber) return next();\n\n\t\t// Resolve subscription → plan name (fall back to first plan = free)\n\t\tconst subscription = await getSubscription(subscriber.type, subscriber.id, store);\n\t\tconst planName = subscription?.plan ?? config.plans[0]?.name ?? 'free';\n\t\tconst active =\n\t\t\t!subscription || subscription.status === 'active' || subscription.status === 'trialing';\n\n\t\tconst plan = config.plans.find((p) => p.name === planName) ?? config.plans[0];\n\t\tif (!plan) return next();\n\n\t\t// Increment windowed (rate-limit) counters and read their current totals\n\t\tconst counters: Record<string, number> = {};\n\n\t\tfor (const [key, entry] of Object.entries(plan.policy ?? {})) {\n\t\t\tif ('enabled' in entry || !entry.window) continue;\n\n\t\t\tconst windowMs = parseWindowMs(entry.window);\n\t\t\tconst counterKey = `${subscriber.type}:${subscriber.id}:${key}`;\n\t\t\tcounters[key] = await backend.increment(counterKey, windowMs);\n\t\t}\n\n\t\t// Build and cache billing context on ctx\n\t\tconst billingCtx = buildBillingContext({ subscriber, plan, active, counters });\n\t\tctx.meta['billing'] = billingCtx;\n\n\t\t// Block requests that have hit a hard limit\n\t\tfor (const [key, status] of Object.entries(billingCtx.statuses)) {\n\t\t\tif (status.type === 'counter' && status.status === 'blocked') {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.TOO_MANY_REQUESTS,\n\t\t\t\t\t'RATE_LIMIT_EXCEEDED',\n\t\t\t\t\t`Limit exceeded for: ${key}`,\n\t\t\t\t\t{ key, limit: status.limit, used: status.used, resetsAt: status.resetsAt },\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Fire threshold notifications (once per subscriber per key per session)\n\t\tif (config.notifications) {\n\t\t\tconst toNotify: ICourierMessage[] = [];\n\t\t\tconst recipient = {\n\t\t\t\temail: ctx.user?.email ?? null,\n\t\t\t\tphone: null,\n\t\t\t\tdeviceToken: null,\n\t\t\t};\n\n\t\t\tfor (const [key, status] of Object.entries(billingCtx.statuses)) {\n\t\t\t\tif (status.type !== 'counter' || status.limit === null) continue;\n\n\t\t\t\tconst base = `${subscriber.type}:${subscriber.id}:${key}`;\n\n\t\t\t\tif (config.notifications.softHit && status.status === 'over_limit') {\n\t\t\t\t\tconst nk = `${base}:reached`;\n\t\t\t\t\tif (!notified.has(nk)) {\n\t\t\t\t\t\tnotified.add(nk);\n\t\t\t\t\t\ttoNotify.push({\n\t\t\t\t\t\t\ttype: MESSAGE_KEYS.limitReached,\n\t\t\t\t\t\t\trecipient,\n\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\t\tplan: plan.name,\n\t\t\t\t\t\t\t\tlimit: status.limit,\n\t\t\t\t\t\t\t\tused: status.used,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else if (config.notifications.warnAt && status.status === 'warning') {\n\t\t\t\t\tconst nk = `${base}:warning`;\n\t\t\t\t\tif (!notified.has(nk)) {\n\t\t\t\t\t\tnotified.add(nk);\n\t\t\t\t\t\ttoNotify.push({\n\t\t\t\t\t\t\ttype: MESSAGE_KEYS.limitWarning,\n\t\t\t\t\t\t\trecipient,\n\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\t\tplan: plan.name,\n\t\t\t\t\t\t\t\tlimit: status.limit,\n\t\t\t\t\t\t\t\tused: status.used,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (toNotify.length > 0) {\n\t\t\t\tconst existing = ctx.meta['messages'] as ICourierMessage[] | undefined;\n\t\t\t\tctx.meta['messages'] = [...(existing ?? []), ...toNotify];\n\t\t\t}\n\t\t}\n\n\t\treturn next();\n\t};\n}\n","import type { IBillingProvider } from './providers/types';\nimport type { PolicyEntry } from './types';\nimport type { ICounterBackend } from './backends/types';\n\nexport interface IBillingPlanPrice {\n\t/** Stable Stripe Price lookup_key, e.g. \"pro_monthly\". Preferred reference. */\n\tlookupKey?: string;\n\t/** Stripe price id. Used for hydration and as a lookup_key fallback. */\n\tpriceId?: string;\n\t/**\n\t * Display amount in cents — the seed value written to fonderie_plans and the\n\t * fallback shown (flagged pricingStale) when hydration is off or Stripe is\n\t * unreachable. When hydration resolves a live price, the live amount wins.\n\t */\n\tamount?: number;\n}\n\n/**\n * Read-through pricing: amount/currency come from Stripe (source of truth) rather\n * than the duplicated `amount` above. Gated by `hydration` (kill-switch, §16.9).\n * See packages/billing/docs/pricing-hydration.md.\n */\nexport interface IBillingPricingConfig {\n\t/** Kill-switch. When false (default), serve the configured amount/USD directly. */\n\thydration?: boolean;\n\t/** Fresh-cache TTL. Default 300_000 (5m). */\n\tcacheTtlMs?: number;\n\t/** Serve last-cached price on a transient resolution miss (lookup_key transfer). Default 3_600_000 (1h). */\n\ttransferGraceMs?: number;\n\t/** Max age to serve stale price during a Stripe outage before giving up. Default 86_400_000 (24h). */\n\tmaxStaleMs?: number;\n}\n\nexport interface IBillingPlanDefaults {\n\twarnAt?: number; // default warnAt fraction (0–1) for counter policies\n\tbuffer?: number; // default buffer for counter policies\n}\n\nexport interface IBillingPlan {\n\tname: string;\n\tdescription?: string;\n\ttier?: number;\n\ttrialDays?: number;\n\tmonthly?: IBillingPlanPrice;\n\tyearly?: IBillingPlanPrice;\n\tdefaults?: IBillingPlanDefaults;\n\tpolicy?: Record<string, PolicyEntry>;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport type RateLimitBackendConfig = 'memory' | 'db' | ICounterBackend;\n\nexport interface IBillingNotificationsConfig {\n\twarnAt?: boolean; // fire courier message when warnAt threshold crossed\n\tsoftHit?: boolean; // fire when soft limit crossed\n}\n\nexport interface IBillingConfig {\n\tprovider: IBillingProvider;\n\tplans: IBillingPlan[];\n\tsuccessUrl: string;\n\tcancelUrl: string;\n\twebhookSecret?: string;\n\trateLimit?: { backend?: RateLimitBackendConfig };\n\tnotifications?: IBillingNotificationsConfig;\n\tpricing?: IBillingPricingConfig;\n}\n\nexport const MESSAGE_KEYS = {\n\tlimitWarning: 'billing.limit-warning',\n\tlimitReached: 'billing.limit-reached',\n\tlimitBlocked: 'billing.limit-blocked',\n} as const;\n\nexport type BillingMessageKey = (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS];\n","import type { IBillingPlan } from '../config';\nimport type { LimitStatus, IPolicyStatus, IBillingContext, SubscriberType } from '../types';\nimport { parseWindowMs } from '../utils';\n\nexport function buildBillingContext(opts: {\n\tsubscriber: { type: SubscriberType; id: string };\n\tplan: IBillingPlan;\n\tactive: boolean;\n\t// Pre-fetched windowed counter values keyed by policy key.\n\t// Non-windowed counter keys are absent (their used count is 0 — app manages those).\n\tcounters: Record<string, number>;\n}): IBillingContext {\n\tconst { subscriber, plan, active, counters } = opts;\n\tconst defaults = plan.defaults ?? {};\n\tconst statuses: Record<string, IPolicyStatus> = {};\n\n\tfor (const [key, entry] of Object.entries(plan.policy ?? {})) {\n\t\tif ('enabled' in entry) {\n\t\t\tstatuses[key] = { type: 'feature', enabled: entry.enabled };\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { limit, buffer = defaults.buffer ?? 0, warnAt = defaults.warnAt ?? 0.8, window } = entry;\n\n\t\tconst used = counters[key] ?? 0;\n\t\tconst hardLimit = limit !== null ? limit + buffer : null;\n\n\t\tlet status: LimitStatus = 'ok';\n\t\tif (hardLimit !== null && used >= hardLimit) status = 'blocked';\n\t\telse if (limit !== null && used >= limit) status = 'over_limit';\n\t\telse if (limit !== null && used >= limit * warnAt) status = 'warning';\n\n\t\tlet resetsAt: string | null = null;\n\t\tif (window) {\n\t\t\tconst windowMs = parseWindowMs(window);\n\t\t\tconst windowStart = Math.floor(Date.now() / windowMs) * windowMs;\n\t\t\tresetsAt = new Date(windowStart + windowMs).toISOString();\n\t\t}\n\n\t\tstatuses[key] = { type: 'counter', limit, used, status, resetsAt };\n\t}\n\n\treturn { subscriber, plan: plan.name, active, statuses };\n}\n"],"mappings":"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","names":["setApiResponse","HTTP","setApiResponse","HTTP"]}
package/dist/types.cjs CHANGED
@@ -3,6 +3,10 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
6
10
  var __copyProps = (to, from, except, desc) => {
7
11
  if (from && typeof from === "object" || typeof from === "function") {
8
12
  for (let key of __getOwnPropNames(from))
@@ -15,5 +19,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
15
19
 
16
20
  // src/types.ts
17
21
  var types_exports = {};
22
+ __export(types_exports, {
23
+ BILLING_INTERVAL: () => BILLING_INTERVAL
24
+ });
18
25
  module.exports = __toCommonJS(types_exports);
26
+ var BILLING_INTERVAL = { MONTH: "month", YEAR: "year" };
27
+ // Annotate the CommonJS export names for ESM import in node:
28
+ 0 && (module.exports = {
29
+ BILLING_INTERVAL
30
+ });
19
31
  //# sourceMappingURL=types.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["export type SubscriberType = 'user' | 'workspace';\n\n// ── Policy ────────────────────────────────────────────────────────\n\nexport type PolicyEntry =\n\t| { enabled: boolean }\n\t| {\n\t\t\tlimit: number | null; // advertised ceiling; null = unlimited\n\t\t\tbuffer?: number; // unadvertised grace on top of limit\n\t\t\twarnAt?: number; // fraction of limit to trigger warning (0–1)\n\t\t\twindow?: string; // '1d' | '30d' | '1h' — if set, auto rate-limited\n\t\t\tunit?: string; // display only, e.g. 'mb', 'requests'\n\t };\n\nexport type LimitStatus = 'ok' | 'warning' | 'over_limit' | 'blocked';\n\nexport type IPolicyStatus =\n\t| { type: 'feature'; enabled: boolean }\n\t| {\n\t\t\ttype: 'counter';\n\t\t\tlimit: number | null; // advertised — safe to send to client\n\t\t\tused: number;\n\t\t\tstatus: LimitStatus;\n\t\t\tresetsAt: string | null; // ISO string for windowed counters, null otherwise\n\t };\n\nexport interface IBillingContext {\n\tsubscriber: { type: SubscriberType; id: string };\n\tplan: string;\n\tactive: boolean; // subscription is active or trialing\n\tstatuses: Record<string, IPolicyStatus>;\n}\n\n// ── Subscription ──────────────────────────────────────────────────\n\nexport interface ISubscription {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tplan: string;\n\tinterval: 'month' | 'year';\n\tstatus: SubscriptionStatus;\n\tproviderCustomerId: string | null;\n\tproviderSubscriptionId: string | null;\n\tcurrentPeriodStart: string | null;\n\tcurrentPeriodEnd: string | null;\n\tcancelAtPeriodEnd: boolean;\n\ttrialEndsAt: string | null;\n\tcreatedAt: string;\n}\n\nexport type SubscriptionStatus =\n\t| 'trialing'\n\t| 'active'\n\t| 'past_due'\n\t| 'canceled'\n\t| 'incomplete'\n\t| 'paused';\n\n// ── DB plan (read from fonderie_plans table) ──────────────────────\n\nexport interface IPlan {\n\tid: string;\n\tname: string;\n\tseats: number | null;\n\ttrialDays: number;\n\tmonthlyAmount: number | null;\n\tmonthlyPriceId: string | null;\n\tyearlyAmount: number | null;\n\tyearlyPriceId: string | null;\n\tdescription: string | null;\n\ttier: number;\n\tfeatures: IPlanFeature[];\n\tmetadata: Record<string, unknown>;\n}\n\nexport interface IPlanFeature {\n\tname: string;\n\tdescription: string;\n\tenabled: boolean;\n\tlimit?: number;\n}\n\n// ── Usage ─────────────────────────────────────────────────────────\n\nexport interface IUsageRecord {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tmetric: string;\n\tquantity: number;\n\trecordedAt: string;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["export type SubscriberType = 'user' | 'workspace';\n\n// Billing interval — one source for the 'month' | 'year' literals.\nexport const BILLING_INTERVAL = { MONTH: 'month', YEAR: 'year' } as const;\nexport type BillingInterval = (typeof BILLING_INTERVAL)[keyof typeof BILLING_INTERVAL];\n\n// ── Policy ────────────────────────────────────────────────────────\n\nexport type PolicyEntry =\n\t| { enabled: boolean }\n\t| {\n\t\t\tlimit: number | null; // advertised ceiling; null = unlimited\n\t\t\tbuffer?: number; // unadvertised grace on top of limit\n\t\t\twarnAt?: number; // fraction of limit to trigger warning (0–1)\n\t\t\twindow?: string; // '1d' | '30d' | '1h' — if set, auto rate-limited\n\t\t\tunit?: string; // display only, e.g. 'mb', 'requests'\n\t };\n\nexport type LimitStatus = 'ok' | 'warning' | 'over_limit' | 'blocked';\n\nexport type IPolicyStatus =\n\t| { type: 'feature'; enabled: boolean }\n\t| {\n\t\t\ttype: 'counter';\n\t\t\tlimit: number | null; // advertised — safe to send to client\n\t\t\tused: number;\n\t\t\tstatus: LimitStatus;\n\t\t\tresetsAt: string | null; // ISO string for windowed counters, null otherwise\n\t };\n\nexport interface IBillingContext {\n\tsubscriber: { type: SubscriberType; id: string };\n\tplan: string;\n\tactive: boolean; // subscription is active or trialing\n\tstatuses: Record<string, IPolicyStatus>;\n}\n\n// ── Subscription ──────────────────────────────────────────────────\n\nexport interface ISubscription {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tplan: string;\n\tinterval: 'month' | 'year';\n\tstatus: SubscriptionStatus;\n\tproviderCustomerId: string | null;\n\tproviderSubscriptionId: string | null;\n\tcurrentPeriodStart: string | null;\n\tcurrentPeriodEnd: string | null;\n\tcancelAtPeriodEnd: boolean;\n\ttrialEndsAt: string | null;\n\tcreatedAt: string;\n}\n\nexport type SubscriptionStatus =\n\t| 'trialing'\n\t| 'active'\n\t| 'past_due'\n\t| 'canceled'\n\t| 'incomplete'\n\t| 'paused';\n\n// ── DB plan (read from fonderie_plans table) ──────────────────────\n\nexport interface IPlan {\n\tid: string;\n\tname: string;\n\tseats: number | null;\n\ttrialDays: number;\n\tmonthlyAmount: number | null;\n\tmonthlyPriceId: string | null;\n\tyearlyAmount: number | null;\n\tyearlyPriceId: string | null;\n\tdescription: string | null;\n\ttier: number;\n\tfeatures: IPlanFeature[];\n\tmetadata: Record<string, unknown>;\n}\n\nexport interface IPlanFeature {\n\tname: string;\n\tdescription: string;\n\tenabled: boolean;\n\tlimit?: number;\n}\n\n// ── Usage ─────────────────────────────────────────────────────────\n\nexport interface IUsageRecord {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tmetric: string;\n\tquantity: number;\n\trecordedAt: string;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGO,IAAM,mBAAmB,EAAE,OAAO,SAAS,MAAM,OAAO;","names":[]}
package/dist/types.d.cts CHANGED
@@ -1,4 +1,9 @@
1
1
  type SubscriberType = 'user' | 'workspace';
2
+ declare const BILLING_INTERVAL: {
3
+ readonly MONTH: "month";
4
+ readonly YEAR: "year";
5
+ };
6
+ type BillingInterval = (typeof BILLING_INTERVAL)[keyof typeof BILLING_INTERVAL];
2
7
  type PolicyEntry = {
3
8
  enabled: boolean;
4
9
  } | {
@@ -73,4 +78,4 @@ interface IUsageRecord {
73
78
  recordedAt: string;
74
79
  }
75
80
 
76
- export type { IBillingContext, IPlan, IPlanFeature, IPolicyStatus, ISubscription, IUsageRecord, LimitStatus, PolicyEntry, SubscriberType, SubscriptionStatus };
81
+ export { BILLING_INTERVAL, type BillingInterval, type IBillingContext, type IPlan, type IPlanFeature, type IPolicyStatus, type ISubscription, type IUsageRecord, type LimitStatus, type PolicyEntry, type SubscriberType, type SubscriptionStatus };
package/dist/types.d.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  type SubscriberType = 'user' | 'workspace';
2
+ declare const BILLING_INTERVAL: {
3
+ readonly MONTH: "month";
4
+ readonly YEAR: "year";
5
+ };
6
+ type BillingInterval = (typeof BILLING_INTERVAL)[keyof typeof BILLING_INTERVAL];
2
7
  type PolicyEntry = {
3
8
  enabled: boolean;
4
9
  } | {
@@ -73,4 +78,4 @@ interface IUsageRecord {
73
78
  recordedAt: string;
74
79
  }
75
80
 
76
- export type { IBillingContext, IPlan, IPlanFeature, IPolicyStatus, ISubscription, IUsageRecord, LimitStatus, PolicyEntry, SubscriberType, SubscriptionStatus };
81
+ export { BILLING_INTERVAL, type BillingInterval, type IBillingContext, type IPlan, type IPlanFeature, type IPolicyStatus, type ISubscription, type IUsageRecord, type LimitStatus, type PolicyEntry, type SubscriberType, type SubscriptionStatus };
package/dist/types.js CHANGED
@@ -1 +1,6 @@
1
+ // src/types.ts
2
+ var BILLING_INTERVAL = { MONTH: "month", YEAR: "year" };
3
+ export {
4
+ BILLING_INTERVAL
5
+ };
1
6
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["export type SubscriberType = 'user' | 'workspace';\n\n// Billing interval — one source for the 'month' | 'year' literals.\nexport const BILLING_INTERVAL = { MONTH: 'month', YEAR: 'year' } as const;\nexport type BillingInterval = (typeof BILLING_INTERVAL)[keyof typeof BILLING_INTERVAL];\n\n// ── Policy ────────────────────────────────────────────────────────\n\nexport type PolicyEntry =\n\t| { enabled: boolean }\n\t| {\n\t\t\tlimit: number | null; // advertised ceiling; null = unlimited\n\t\t\tbuffer?: number; // unadvertised grace on top of limit\n\t\t\twarnAt?: number; // fraction of limit to trigger warning (0–1)\n\t\t\twindow?: string; // '1d' | '30d' | '1h' — if set, auto rate-limited\n\t\t\tunit?: string; // display only, e.g. 'mb', 'requests'\n\t };\n\nexport type LimitStatus = 'ok' | 'warning' | 'over_limit' | 'blocked';\n\nexport type IPolicyStatus =\n\t| { type: 'feature'; enabled: boolean }\n\t| {\n\t\t\ttype: 'counter';\n\t\t\tlimit: number | null; // advertised — safe to send to client\n\t\t\tused: number;\n\t\t\tstatus: LimitStatus;\n\t\t\tresetsAt: string | null; // ISO string for windowed counters, null otherwise\n\t };\n\nexport interface IBillingContext {\n\tsubscriber: { type: SubscriberType; id: string };\n\tplan: string;\n\tactive: boolean; // subscription is active or trialing\n\tstatuses: Record<string, IPolicyStatus>;\n}\n\n// ── Subscription ──────────────────────────────────────────────────\n\nexport interface ISubscription {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tplan: string;\n\tinterval: 'month' | 'year';\n\tstatus: SubscriptionStatus;\n\tproviderCustomerId: string | null;\n\tproviderSubscriptionId: string | null;\n\tcurrentPeriodStart: string | null;\n\tcurrentPeriodEnd: string | null;\n\tcancelAtPeriodEnd: boolean;\n\ttrialEndsAt: string | null;\n\tcreatedAt: string;\n}\n\nexport type SubscriptionStatus =\n\t| 'trialing'\n\t| 'active'\n\t| 'past_due'\n\t| 'canceled'\n\t| 'incomplete'\n\t| 'paused';\n\n// ── DB plan (read from fonderie_plans table) ──────────────────────\n\nexport interface IPlan {\n\tid: string;\n\tname: string;\n\tseats: number | null;\n\ttrialDays: number;\n\tmonthlyAmount: number | null;\n\tmonthlyPriceId: string | null;\n\tyearlyAmount: number | null;\n\tyearlyPriceId: string | null;\n\tdescription: string | null;\n\ttier: number;\n\tfeatures: IPlanFeature[];\n\tmetadata: Record<string, unknown>;\n}\n\nexport interface IPlanFeature {\n\tname: string;\n\tdescription: string;\n\tenabled: boolean;\n\tlimit?: number;\n}\n\n// ── Usage ─────────────────────────────────────────────────────────\n\nexport interface IUsageRecord {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tmetric: string;\n\tquantity: number;\n\trecordedAt: string;\n}\n"],"mappings":";AAGO,IAAM,mBAAmB,EAAE,OAAO,SAAS,MAAM,OAAO;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonderie/billing",
3
- "version": "5.1.0",
3
+ "version": "5.3.1",
4
4
  "description": "SaaS billing in one module — config-driven plan catalogue, Stripe subscriptions, polymorphic user and workspace billing surfaces, usage metering, and webhook handling.",
5
5
  "keywords": [
6
6
  "fonderiejs",