@fonderie/billing 5.1.0 → 5.3.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.
- package/dist/{index-BvYwTSIF.d.ts → index-CBhthuMn.d.ts} +41 -2
- package/dist/{index-CjwZwEVR.d.cts → index-CS1QagwE.d.cts} +41 -2
- package/dist/index.cjs +150 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +149 -7
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/middlewares/index.js.map +1 -1
- package/dist/types.cjs +12 -0
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +6 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/routes.ts","../src/schemas.ts","../src/controllers/plan.controller.ts","../src/services/plans.ts","../src/models/plan.model.ts","../src/dtos/billing.ts","../src/controllers/subscription.controller.ts","../src/services/subscriptions.ts","../src/models/subscription.model.ts","../src/utils.ts","../src/controllers/checkout.controller.ts","../src/controllers/usage.controller.ts","../src/services/usage.ts","../src/models/usage.model.ts","../src/controllers/webhook.controller.ts","../src/middlewares/billing.ts","../src/config.ts","../src/services/policy.ts","../src/backends/memory.ts","../src/backends/db.ts","../src/backends/index.ts","../src/module.ts","../src/providers/stripe.ts","../src/middlewares/require-plan.ts","../src/helpers.ts"],"sourcesContent":["import type { IStoreAdapter } from '@fonderie/store';\nimport type { Middleware } from '@fonderie/core';\nimport { requireAuth, validate } from '@fonderie/core/middlewares';\n\nimport { checkoutSchema, createPlanSchema, recordUsageSchema, updatePlanSchema } from './schemas';\n\nimport type { IBillingConfig } from './config';\nimport { planController } from './controllers/plan.controller';\nimport { subscriptionController } from './controllers/subscription.controller';\nimport { checkoutController } from './controllers/checkout.controller';\nimport { usageController } from './controllers/usage.controller';\nimport { webhookController } from './controllers/webhook.controller';\n\ntype RouteDefinition = [string, string, ...Middleware[]];\n\nexport function buildBillingRoutes(\n\tstore: IStoreAdapter,\n\tconfig: IBillingConfig,\n): RouteDefinition[] {\n\tconst plan = planController(store);\n\tconst subscription = subscriptionController(store);\n\tconst checkout = checkoutController(store, config);\n\tconst usage = usageController(store);\n\tconst webhook = webhookController(store, config);\n\n\treturn [\n\t\t// Plans — public read-only\n\t\t['GET', '/plans', plan.list],\n\t\t['GET', '/plans/:planId', plan.get],\n\n\t\t// Plans — admin write (caller is responsible for authorization)\n\t\t['POST', '/plans', validate(createPlanSchema), plan.create],\n\t\t['PUT', '/plans/:planId', validate(updatePlanSchema), plan.update],\n\t\t['DELETE', '/plans/:planId', plan.delete],\n\n\t\t// Billing — subscriber resolved from X-Workspace-ID header (workspace) or session (user)\n\t\t// Workspace membership is verified automatically by the withBilling global middleware\n\t\t['GET', '/billing/subscription', requireAuth, subscription.get],\n\t\t['POST', '/billing/checkout', requireAuth, validate(checkoutSchema), checkout.createSession],\n\t\t['POST', '/billing/portal', requireAuth, checkout.createPortal],\n\t\t['POST', '/billing/usage', requireAuth, validate(recordUsageSchema), usage.record],\n\t\t['GET', '/billing/usage/:metric', requireAuth, usage.get],\n\n\t\t// Webhook — signature verified inside the handler\n\t\t['POST', '/billing/webhook', webhook.handle],\n\t];\n}\n","import { z } from 'zod';\n\n// Request schemas — the validation contract for billing's body-taking routes\n// (webhook excluded: provider-shaped, signature-verified in the handler).\n// Wired via @fonderie/core's validate(); same pattern as @fonderie/auth.\n\nconst planFields = {\n\tdescription: z.string().max(2000).nullable().optional(),\n\ttier: z.number().int().min(0).optional(),\n\tseats: z.number().int().min(0).nullable().optional(),\n\ttrialDays: z.number().int().min(0).optional(),\n\tmonthlyAmount: z.number().min(0).nullable().optional(),\n\tmonthlyPriceId: z.string().max(200).nullable().optional(),\n\tyearlyAmount: z.number().min(0).nullable().optional(),\n\tyearlyPriceId: z.string().max(200).nullable().optional(),\n\tfeatures: z.unknown().optional(),\n\tmetadata: z.unknown().optional(),\n};\n\nexport const createPlanSchema = z.object({\n\tname: z.string().trim().min(1, 'name is required').max(200),\n\t...planFields,\n});\n\nexport const updatePlanSchema = z\n\t.object({ name: z.string().trim().min(1).max(200).optional(), ...planFields })\n\t.refine((o) => Object.values(o).some((v) => v !== undefined), 'Provide at least one field');\n\nexport const checkoutSchema = z.object({\n\tplan: z.string().min(1, 'plan is required'),\n\tinterval: z.enum(['month', 'year']).optional(),\n});\n\nexport const recordUsageSchema = z.object({\n\tmetric: z.string().min(1, 'metric is required').max(100),\n\tquantity: z.number().min(0).optional(),\n});\n","import { setApiResponse, HTTP, stringOrEmpty, numberOrZero } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { PlanModel } from '../models/plan.model';\nimport { toPlanDTO } from '../dtos/billing';\n\nexport function planController(store: IStoreAdapter) {\n\tconst plans = new PlanModel(store);\n\n\treturn {\n\t\tasync list(_ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst list = await plans.list();\n\t\t\tconst dtos = list.map(toPlanDTO);\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_LIST', `Retrieved ${list.length} workspace plans`, {\n\t\t\t\tplans: dtos,\n\t\t\t});\n\t\t},\n\n\t\tasync get(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst id = params?.['planId'];\n\t\t\tif (!id) return setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Plan ID required');\n\n\t\t\tconst plan = await plans.findById(id);\n\t\t\tif (!plan) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Plan not found');\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_FETCHED', 'Plan retrieved successfully.', {\n\t\t\t\tplan: toPlanDTO(plan),\n\t\t\t});\n\t\t},\n\n\t\tasync create(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst name = stringOrEmpty(body?.['name']);\n\t\t\tif (!name) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'VALIDATION_ERROR', 'name is required');\n\t\t\t}\n\n\t\t\tconst plan = await plans.create({\n\t\t\t\tname,\n\t\t\t\tdescription: body?.['description'] != null ? String(body['description']) : null,\n\t\t\t\ttier: body?.['tier'] != null ? numberOrZero(body['tier']) : 0,\n\t\t\t\tseats: body?.['seats'] != null ? numberOrZero(body['seats']) : null,\n\t\t\t\ttrialDays: body?.['trialDays'] != null ? numberOrZero(body['trialDays']) : 0,\n\t\t\t\tmonthlyAmount: body?.['monthlyAmount'] != null ? numberOrZero(body['monthlyAmount']) : null,\n\t\t\t\tmonthlyPriceId: body?.['monthlyPriceId'] != null ? String(body['monthlyPriceId']) : null,\n\t\t\t\tyearlyAmount: body?.['yearlyAmount'] != null ? numberOrZero(body['yearlyAmount']) : null,\n\t\t\t\tyearlyPriceId: body?.['yearlyPriceId'] != null ? String(body['yearlyPriceId']) : null,\n\t\t\t\tfeatures: body?.['features'],\n\t\t\t\tmetadata: body?.['metadata'],\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.CREATED, 'PLAN_CREATED', 'Plan created successfully.', {\n\t\t\t\tplan: toPlanDTO(plan),\n\t\t\t});\n\t\t},\n\n\t\tasync update(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst id = params?.['planId'];\n\t\t\tif (!id) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Plan ID required');\n\t\t\t}\n\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tif (!body || Object.keys(body).length === 0) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'VALIDATION_ERROR', 'Request body is empty');\n\t\t\t}\n\n\t\t\tconst patch: Record<string, unknown> = {};\n\t\t\tconst allowed = ['name', 'description', 'tier', 'seats', 'trialDays',\n\t\t\t\t'monthlyAmount', 'monthlyPriceId', 'yearlyAmount', 'yearlyPriceId',\n\t\t\t\t'features', 'metadata'];\n\n\t\t\tfor (const key of allowed) {\n\t\t\t\tif (key in body) patch[key] = body[key];\n\t\t\t}\n\n\t\t\tconst plan = await plans.update(id, patch);\n\t\t\tif (!plan) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Plan not found');\n\t\t\t}\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_UPDATED', 'Plan updated successfully.', {\n\t\t\t\tplan: toPlanDTO(plan),\n\t\t\t});\n\t\t},\n\n\t\tasync delete(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst id = params?.['planId'];\n\t\t\tif (!id) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Plan ID required');\n\t\t\t}\n\n\t\t\tconst deleted = await plans.delete(id);\n\t\t\tif (!deleted) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Plan not found');\n\t\t\t}\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_DELETED', 'Plan deleted successfully.');\n\t\t},\n\t};\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPlan } from '../types';\nimport type { IBillingConfig, IBillingPlan } from '../config';\n\nexport function getPlans(config: IBillingConfig): IBillingPlan[] {\n\treturn config.plans;\n}\n\nexport function getPlanByName(name: string, config: IBillingConfig): IBillingPlan | null {\n\treturn config.plans.find((p) => p.name.toLowerCase() === name.toLowerCase()) ?? null;\n}\n\nexport async function syncPlansToDB(config: IBillingConfig, store: IStoreAdapter): Promise<void> {\n\tconst plans = config.plans;\n\tif (plans.length === 0) return;\n\n\tconst values = plans.map((_, i) => {\n\t\tconst b = i * 9;\n\t\treturn `($${b + 1}, $${b + 2}, $${b + 3}, $${b + 4}, $${b + 5}, $${b + 6}, $${b + 7}, $${b + 8}, $${b + 9}::jsonb)`;\n\t});\n\n\tconst params = plans.flatMap((plan) => [\n\t\tplan.name,\n\t\tplan.trialDays ?? 0,\n\t\tplan.monthly?.amount ?? null,\n\t\tplan.monthly?.priceId ?? null,\n\t\tplan.yearly?.amount ?? null,\n\t\tplan.yearly?.priceId ?? null,\n\t\tplan.description ?? null,\n\t\tplan.tier ?? 0,\n\t\tJSON.stringify(plan.metadata ?? {}),\n\t]);\n\n\tawait store.query(\n\t\t`INSERT INTO fonderie_plans\n\t\t\t(name, trial_days,\n\t\t\t monthly_amount, monthly_price_id,\n\t\t\t yearly_amount, yearly_price_id,\n\t\t\t description, tier, metadata)\n\t\tVALUES ${values.join(', ')}\n\t\tON CONFLICT (name) DO UPDATE SET\n\t\t\ttrial_days = EXCLUDED.trial_days,\n\t\t\tmonthly_amount = EXCLUDED.monthly_amount,\n\t\t\tmonthly_price_id = EXCLUDED.monthly_price_id,\n\t\t\tyearly_amount = EXCLUDED.yearly_amount,\n\t\t\tyearly_price_id = EXCLUDED.yearly_price_id,\n\t\t\tdescription = EXCLUDED.description,\n\t\t\ttier = EXCLUDED.tier,\n\t\t\tmetadata = EXCLUDED.metadata`,\n\t\tparams,\n\t);\n}\n\nconst SELECT_PLAN = `\n\tSELECT\n\t\tid,\n\t\tname,\n\t\tseats,\n\t\ttrial_days AS \"trialDays\",\n\t\tmonthly_amount AS \"monthlyAmount\",\n\t\tmonthly_price_id AS \"monthlyPriceId\",\n\t\tyearly_amount AS \"yearlyAmount\",\n\t\tyearly_price_id AS \"yearlyPriceId\",\n\t\tdescription,\n\t\ttier,\n\t\tfeatures,\n\t\tmetadata\n\tFROM fonderie_plans`;\n\nexport async function getDBPlans(store: IStoreAdapter): Promise<IPlan[]> {\n\treturn store.query<IPlan>(\n\t\t`${SELECT_PLAN} WHERE active = true ORDER BY tier ASC, monthly_amount ASC NULLS LAST`,\n\t);\n}\n\nexport async function getPlanById(id: string, store: IStoreAdapter): Promise<IPlan | null> {\n\tconst [row] = await store.query<IPlan>(`${SELECT_PLAN} WHERE id = $1`, [id]);\n\treturn row ?? null;\n}\n\nexport async function createPlan(\n\tdata: {\n\t\tname: string;\n\t\tdescription?: string | null;\n\t\ttier?: number;\n\t\tseats?: number | null;\n\t\ttrialDays?: number;\n\t\tfeatures?: unknown;\n\t\tmetadata?: unknown;\n\t\tmonthlyAmount?: number | null;\n\t\tmonthlyPriceId?: string | null;\n\t\tyearlyAmount?: number | null;\n\t\tyearlyPriceId?: string | null;\n\t},\n\tstore: IStoreAdapter,\n): Promise<IPlan> {\n\tconst [row] = await store.query<IPlan>(\n\t\t`INSERT INTO fonderie_plans\n\t\t\t(name, seats, trial_days, monthly_amount, monthly_price_id,\n\t\t\t yearly_amount, yearly_price_id, description, tier, features, metadata)\n\t\tVALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb)\n\t\tRETURNING\n\t\t\tid, name, seats,\n\t\t\ttrial_days AS \"trialDays\",\n\t\t\tmonthly_amount AS \"monthlyAmount\",\n\t\t\tmonthly_price_id AS \"monthlyPriceId\",\n\t\t\tyearly_amount AS \"yearlyAmount\",\n\t\t\tyearly_price_id AS \"yearlyPriceId\",\n\t\t\tdescription, tier, features, metadata`,\n\t\t[\n\t\t\tdata.name,\n\t\t\tdata.seats ?? null,\n\t\t\tdata.trialDays ?? 0,\n\t\t\tdata.monthlyAmount ?? null,\n\t\t\tdata.monthlyPriceId ?? null,\n\t\t\tdata.yearlyAmount ?? null,\n\t\t\tdata.yearlyPriceId ?? null,\n\t\t\tdata.description ?? null,\n\t\t\tdata.tier ?? 0,\n\t\t\tJSON.stringify(data.features ?? []),\n\t\t\tJSON.stringify(data.metadata ?? {}),\n\t\t],\n\t);\n\tif (!row) throw new Error('Failed to create plan');\n\treturn row;\n}\n\nexport async function updatePlan(\n\tid: string,\n\tdata: Partial<Omit<IPlan, 'id'>>,\n\tstore: IStoreAdapter,\n): Promise<IPlan | null> {\n\tconst fieldMap: Record<string, string> = {\n\t\tname: 'name',\n\t\tseats: 'seats',\n\t\ttrialDays: 'trial_days',\n\t\tmonthlyAmount: 'monthly_amount',\n\t\tmonthlyPriceId: 'monthly_price_id',\n\t\tyearlyAmount: 'yearly_amount',\n\t\tyearlyPriceId: 'yearly_price_id',\n\t\tdescription: 'description',\n\t\ttier: 'tier',\n\t};\n\n\tconst jsonbFields = new Set(['features', 'metadata']);\n\tconst setClauses: string[] = [];\n\tconst params: unknown[] = [id];\n\n\tfor (const [key, col] of Object.entries(fieldMap)) {\n\t\tif (key in data) {\n\t\t\tparams.push((data as Record<string, unknown>)[key]);\n\t\t\tsetClauses.push(`${col} = $${params.length}`);\n\t\t}\n\t}\n\n\tfor (const key of jsonbFields) {\n\t\tif (key in data) {\n\t\t\tparams.push(JSON.stringify((data as Record<string, unknown>)[key]));\n\t\t\tsetClauses.push(`${key} = $${params.length}::jsonb`);\n\t\t}\n\t}\n\n\tif (setClauses.length === 0) return getPlanById(id, store);\n\n\tconst [row] = await store.query<IPlan>(\n\t\t`UPDATE fonderie_plans SET ${setClauses.join(', ')}\n\t\tWHERE id = $1\n\t\tRETURNING\n\t\t\tid, name, seats,\n\t\t\ttrial_days AS \"trialDays\",\n\t\t\tmonthly_amount AS \"monthlyAmount\",\n\t\t\tmonthly_price_id AS \"monthlyPriceId\",\n\t\t\tyearly_amount AS \"yearlyAmount\",\n\t\t\tyearly_price_id AS \"yearlyPriceId\",\n\t\t\tdescription, tier, features, metadata`,\n\t\tparams,\n\t);\n\treturn row ?? null;\n}\n\nexport async function deletePlan(id: string, store: IStoreAdapter): Promise<boolean> {\n\tconst rows = await store.query<{ id: string }>(\n\t\t`DELETE FROM fonderie_plans WHERE id = $1 RETURNING id`,\n\t\t[id],\n\t);\n\treturn rows.length > 0;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPlan } from '../types';\nimport type { IBillingConfig } from '../config';\nimport {\n\tgetDBPlans,\n\tgetPlanById,\n\tcreatePlan,\n\tupdatePlan,\n\tdeletePlan,\n\tgetPlans,\n\tgetPlanByName,\n} from '../services/plans';\n\nexport class PlanModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tlistFromConfig(config: IBillingConfig) {\n\t\treturn getPlans(config);\n\t}\n\n\tfindByNameInConfig(name: string, config: IBillingConfig) {\n\t\treturn getPlanByName(name, config);\n\t}\n\n\tlist(): Promise<IPlan[]> {\n\t\treturn getDBPlans(this.store);\n\t}\n\n\tfindById(id: string): Promise<IPlan | null> {\n\t\treturn getPlanById(id, this.store);\n\t}\n\n\tcreate(data: Parameters<typeof createPlan>[0]): Promise<IPlan> {\n\t\treturn createPlan(data, this.store);\n\t}\n\n\tupdate(id: string, data: Parameters<typeof updatePlan>[1]): Promise<IPlan | null> {\n\t\treturn updatePlan(id, data, this.store);\n\t}\n\n\tdelete(id: string): Promise<boolean> {\n\t\treturn deletePlan(id, this.store);\n\t}\n}\n","import type { IPlan, IPlanFeature, ISubscription, IUsageRecord, SubscriberType } from '../types';\n\nexport interface IPlanDTO {\n\tid: string;\n\tplanId: string;\n\tname: string;\n\tdescription: string;\n\ttier: number;\n\tseats: number | null;\n\ttrialDays: number;\n\tpricing: {\n\t\tmonthly: number; // in cents, e.g. 1999 = $19.99\n\t\tyearly: number; // in cents\n\t\tcurrency: string; // ISO 4217, e.g. 'USD'\n\t};\n\tfeatures: IPlanFeature[];\n\tmetadata: Record<string, unknown>;\n}\n\nexport interface ISubscriptionDTO {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tplan: string;\n\tinterval: string;\n\tstatus: string;\n\tcancelAtPeriodEnd: boolean;\n\tcurrentPeriodStart: string | null;\n\tcurrentPeriodEnd: string | null;\n\ttrialEndsAt: string | null;\n\tcreatedAt: string;\n}\n\nexport interface IUsageRecordDTO {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tmetric: string;\n\tquantity: number;\n\trecordedAt: string;\n}\n\nexport function toPlanDTO(plan: IPlan): IPlanDTO {\n\treturn {\n\t\tid: plan.id,\n\t\tplanId: plan.name.toUpperCase(),\n\t\tname: plan.name,\n\t\tdescription: plan.description ?? '',\n\t\ttier: plan.tier,\n\t\tseats: plan.seats,\n\t\ttrialDays: plan.trialDays,\n\t\tpricing: {\n\t\t\tmonthly: plan.monthlyAmount ?? 0,\n\t\t\tyearly: plan.yearlyAmount ?? 0,\n\t\t\tcurrency: 'USD',\n\t\t},\n\t\tfeatures: Array.isArray(plan.features) ? plan.features : [],\n\t\tmetadata:\n\t\t\tplan.metadata && typeof plan.metadata === 'object'\n\t\t\t\t? (plan.metadata as Record<string, unknown>)\n\t\t\t\t: {},\n\t};\n}\n\nexport function toSubscriptionDTO(sub: ISubscription): ISubscriptionDTO {\n\treturn {\n\t\tid: sub.id,\n\t\tsubscriberType: sub.subscriberType,\n\t\tsubscriberId: sub.subscriberId,\n\t\tplan: sub.plan,\n\t\tinterval: sub.interval,\n\t\tstatus: sub.status,\n\t\tcancelAtPeriodEnd: sub.cancelAtPeriodEnd,\n\t\tcurrentPeriodStart: sub.currentPeriodStart,\n\t\tcurrentPeriodEnd: sub.currentPeriodEnd,\n\t\ttrialEndsAt: sub.trialEndsAt,\n\t\tcreatedAt: sub.createdAt,\n\t};\n}\n\nexport function toUsageRecordDTO(record: IUsageRecord): IUsageRecordDTO {\n\treturn {\n\t\tid: record.id,\n\t\tsubscriberType: record.subscriberType,\n\t\tsubscriberId: record.subscriberId,\n\t\tmetric: record.metric,\n\t\tquantity: record.quantity,\n\t\trecordedAt: record.recordedAt,\n\t};\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { SubscriptionModel } from '../models/subscription.model';\nimport { toSubscriptionDTO } from '../dtos/billing';\nimport { resolveSubscriber } from '../utils';\n\nexport function subscriptionController(store: IStoreAdapter) {\n\tconst subscriptions = new SubscriptionModel(store);\n\n\treturn {\n\t\tasync get(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst subscription = await subscriptions.get(subscriber.type, subscriber.id);\n\t\t\tif (!subscription)\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No active subscription');\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'SUBSCRIPTION_FETCHED',\n\t\t\t\t'Subscription retrieved successfully.',\n\t\t\t\t{\n\t\t\t\t\tsubscription: toSubscriptionDTO(subscription),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t};\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 { IStoreAdapter } from '@fonderie/store';\n\nimport type { ISubscription, SubscriberType } from '../types';\nimport { getSubscription, upsertSubscription } from '../services/subscriptions';\n\nexport class SubscriptionModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tget(subscriberType: SubscriberType, subscriberId: string): Promise<ISubscription | null> {\n\t\treturn getSubscription(subscriberType, subscriberId, this.store);\n\t}\n\n\tupsert(data: Parameters<typeof upsertSubscription>[0]): Promise<void> {\n\t\treturn upsertSubscription(data, this.store);\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 { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport { PlanModel } from '../models/plan.model';\nimport { SubscriptionModel } from '../models/subscription.model';\nimport { resolveSubscriber } from '../utils';\n\nexport function checkoutController(store: IStoreAdapter, config: IBillingConfig) {\n\tconst plans = new PlanModel(store);\n\tconst subscriptions = new SubscriptionModel(store);\n\n\treturn {\n\t\tasync createSession(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst planName = body?.['plan'];\n\t\t\tconst interval = (body?.['interval'] ?? 'month') as 'month' | 'year';\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t\tif (typeof planName !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'plan is required');\n\t\t\t}\n\t\t\tif (interval !== 'month' && interval !== 'year') {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'interval must be month or year',\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst plan = plans.findByNameInConfig(planName, config);\n\t\t\tif (!plan) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', `Unknown plan: ${planName}`);\n\t\t\t}\n\n\t\t\tconst pricing = interval === 'year' ? plan.yearly : plan.monthly;\n\t\t\tif (!pricing?.priceId) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t`Plan ${planName} does not support ${interval} billing`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// If there's already an active subscription: allow upgrades only, and\n\t\t\t// change the subscription in place (proration) rather than opening a\n\t\t\t// second checkout / creating a duplicate subscription.\n\t\t\tconst current = await subscriptions.get(subscriber.type, subscriber.id);\n\t\t\tconst ACTIVE = ['active', 'trialing', 'past_due'];\n\t\t\tif (current && ACTIVE.includes(current.status)) {\n\t\t\t\tconst currentTier = plans.findByNameInConfig(current.plan, config)?.tier ?? -1;\n\t\t\t\tconst targetTier = plan.tier ?? -1;\n\t\t\t\tif (targetTier <= currentTier) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'DOWNGRADE_NOT_ALLOWED',\n\t\t\t\t\t\t`Cannot switch from ${current.plan} to a same-or-lower tier (${planName}) mid-cycle. Upgrades only.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (current.providerSubscriptionId) {\n\t\t\t\t\tconst res = await config.provider.updateSubscription({\n\t\t\t\t\t\tsubscriptionId: current.providerSubscriptionId,\n\t\t\t\t\t\tpriceId: pricing.priceId,\n\t\t\t\t\t});\n\t\t\t\t\tconst upsert: Parameters<typeof subscriptions.upsert>[0] = {\n\t\t\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\t\t\tplan: planName,\n\t\t\t\t\t\tinterval,\n\t\t\t\t\t\tstatus: res.status,\n\t\t\t\t\t\tproviderSubscriptionId: current.providerSubscriptionId,\n\t\t\t\t\t};\n\t\t\t\t\tif (current.providerCustomerId) upsert.providerCustomerId = current.providerCustomerId;\n\t\t\t\t\tif (res.currentPeriodStart) upsert.currentPeriodStart = res.currentPeriodStart;\n\t\t\t\t\tif (res.currentPeriodEnd) upsert.currentPeriodEnd = res.currentPeriodEnd;\n\t\t\t\t\tawait subscriptions.upsert(upsert);\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'SUBSCRIPTION_UPGRADED',\n\t\t\t\t\t\t'Subscription upgraded; the prorated difference was charged.',\n\t\t\t\t\t\t{ upgraded: true, plan: planName },\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { customerId } = await config.provider.createCustomer({\n\t\t\t\temail: ctx.user!.email ?? '',\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tuserId: ctx.user!.id,\n\t\t\t});\n\n\t\t\tconst sessionOpts: Parameters<typeof config.provider.createCheckoutSession>[0] = {\n\t\t\t\tcustomerId,\n\t\t\t\tpriceId: pricing.priceId,\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tsuccessUrl: config.successUrl,\n\t\t\t\tcancelUrl: config.cancelUrl,\n\t\t\t};\n\t\t\tif (plan.trialDays !== undefined) sessionOpts.trialDays = plan.trialDays;\n\n\t\t\tconst { url } = await config.provider.createCheckoutSession(sessionOpts);\n\n\t\t\tawait subscriptions.upsert({\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tplan: planName,\n\t\t\t\tinterval,\n\t\t\t\tstatus: 'incomplete',\n\t\t\t\tproviderCustomerId: customerId,\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'CHECKOUT_URL', 'Checkout session created.', { url });\n\t\t},\n\n\t\tasync createPortal(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst subscription = await subscriptions.get(subscriber.type, subscriber.id);\n\t\t\tif (!subscription?.providerCustomerId) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No active subscription');\n\t\t\t}\n\n\t\t\tconst { url } = await config.provider.createPortalSession({\n\t\t\t\tcustomerId: subscription.providerCustomerId,\n\t\t\t\treturnUrl: config.successUrl,\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PORTAL_URL', 'Portal session created.', { url });\n\t\t},\n\t};\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { UsageModel } from '../models/usage.model';\nimport { resolveSubscriber } from '../utils';\n\nexport function usageController(store: IStoreAdapter) {\n\tconst usage = new UsageModel(store);\n\n\treturn {\n\t\tasync record(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst metric = body?.['metric'];\n\t\t\tconst quantity = body?.['quantity'];\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (typeof metric !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'metric is required');\n\t\t\t}\n\n\t\t\tawait usage.record({\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tmetric,\n\t\t\t\tquantity: typeof quantity === 'number' ? quantity : 1,\n\t\t\t});\n\t\t\treturn setApiResponse(HTTP.OK, 'USAGE_RECORDED', 'Usage recorded successfully.');\n\t\t},\n\n\t\tasync get(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst metric = params?.['metric'];\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t\tif (!subscriber || !metric) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'subscriber and metric are required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst since = new Date();\n\t\t\tsince.setDate(1);\n\t\t\tsince.setHours(0, 0, 0, 0);\n\n\t\t\tconst total = await usage.get(subscriber.type, subscriber.id, metric, since);\n\t\t\treturn setApiResponse(HTTP.OK, 'USAGE_FETCHED', 'Usage retrieved successfully.', {\n\t\t\t\tmetric,\n\t\t\t\ttotal,\n\t\t\t\tsince,\n\t\t\t});\n\t\t},\n\t};\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { SubscriberType } from '../types';\n\nexport async function recordUsage(\n\topts: { subscriberType: SubscriberType; subscriberId: string; metric: string; quantity: number },\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`INSERT INTO fonderie_usage_records (subscriber_type, subscriber_id, metric, quantity)\n\t\tVALUES ($1, $2, $3, $4)`,\n\t\t[opts.subscriberType, opts.subscriberId, opts.metric, opts.quantity],\n\t);\n}\n\nexport async function getUsage(\n\tsubscriberType: SubscriberType,\n\tsubscriberId: string,\n\tmetric: string,\n\tsince: Date,\n\tstore: IStoreAdapter,\n): Promise<number> {\n\tconst rows = await store.query<{ total: string }>(\n\t\t`SELECT COALESCE(SUM(quantity), 0) AS total\n\t\tFROM fonderie_usage_records\n\t\tWHERE subscriber_type = $1\n\t\t\tAND subscriber_id = $2\n\t\t\tAND metric = $3\n\t\t\tAND recorded_at >= $4`,\n\t\t[subscriberType, subscriberId, metric, since],\n\t);\n\treturn parseInt(rows[0]?.total ?? '0', 10);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { SubscriberType } from '../types';\n\nimport { recordUsage, getUsage } from '../services/usage';\n\nexport class UsageModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\trecord(opts: Parameters<typeof recordUsage>[0]): Promise<void> {\n\t\treturn recordUsage(opts, this.store);\n\t}\n\n\tget(\n\t\tsubscriberType: SubscriberType,\n\t\tsubscriberId: string,\n\t\tmetric: string,\n\t\tsince: Date,\n\t): Promise<number> {\n\t\treturn getUsage(subscriberType, subscriberId, metric, since, this.store);\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport { SubscriptionModel } from '../models/subscription.model';\n\nexport function webhookController(store: IStoreAdapter, config: IBillingConfig) {\n\tconst subscriptions = new SubscriptionModel(store);\n\n\treturn {\n\t\tasync handle(ctx: IFonderieContext): Promise<Response> {\n\t\t\tif (!config.webhookSecret) {\n\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Webhook secret not configured');\n\t\t\t}\n\n\t\t\tconst signature =\n\t\t\t\tctx.request.headers.get('stripe-signature') ??\n\t\t\t\tctx.request.headers.get('paypal-auth-algo') ??\n\t\t\t\t'';\n\n\t\t\tif (!signature) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Missing webhook signature');\n\t\t\t}\n\n\t\t\tconst payload = await ctx.request.text();\n\n\t\t\tlet event: Awaited<ReturnType<typeof config.provider.constructEvent>>;\n\t\t\ttry {\n\t\t\t\tevent = await config.provider.constructEvent({\n\t\t\t\t\tpayload,\n\t\t\t\t\tsignature,\n\t\t\t\t\tsecret: config.webhookSecret,\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid webhook signature');\n\t\t\t}\n\n\t\t\tif (event.subscription) {\n\t\t\t\tawait subscriptions.upsert({\n\t\t\t\t\tsubscriberType: event.subscription.subscriberType,\n\t\t\t\t\tsubscriberId: event.subscription.subscriberId,\n\t\t\t\t\tplan: event.subscription.plan,\n\t\t\t\t\tinterval: event.subscription.interval,\n\t\t\t\t\tstatus: event.subscription.status,\n\t\t\t\t\tproviderCustomerId: event.subscription.providerCustomerId,\n\t\t\t\t\tproviderSubscriptionId: event.subscription.providerSubscriptionId,\n\t\t\t\t\tcurrentPeriodStart: event.subscription.currentPeriodStart,\n\t\t\t\t\tcurrentPeriodEnd: event.subscription.currentPeriodEnd,\n\t\t\t\t\tcancelAtPeriodEnd: event.subscription.cancelAtPeriodEnd,\n\t\t\t\t\ttrialEndsAt: event.subscription.trialEndsAt,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn Response.json({ received: true });\n\t\t},\n\t};\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","import type { ICounterBackend } from './types';\n\ninterface Entry {\n\tcount: number;\n\twindowStart: number; // epoch ms — used for windowed expiry\n}\n\nexport class MemoryCounterBackend implements ICounterBackend {\n\tprivate readonly counters = new Map<string, Entry>();\n\n\tasync increment(key: string, windowMs: number | null, quantity = 1): Promise<number> {\n\t\tconst now = Date.now();\n\t\tconst existing = this.counters.get(key);\n\n\t\tif (!existing || (windowMs !== null && now - existing.windowStart >= windowMs)) {\n\t\t\tthis.counters.set(key, { count: quantity, windowStart: now });\n\t\t\treturn quantity;\n\t\t}\n\n\t\texisting.count += quantity;\n\t\treturn existing.count;\n\t}\n\n\tasync get(key: string, windowMs: number | null): Promise<number> {\n\t\tconst now = Date.now();\n\t\tconst existing = this.counters.get(key);\n\t\tif (!existing) return 0;\n\t\tif (windowMs !== null && now - existing.windowStart >= windowMs) return 0;\n\t\treturn existing.count;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { ICounterBackend } from './types';\n\nexport class DBCounterBackend implements ICounterBackend {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tasync increment(key: string, windowMs: number | null, quantity = 1): Promise<number> {\n\t\tconst [subscriberType, subscriberId, ...rest] = key.split(':');\n\t\tconst metric = rest.join(':');\n\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_usage_records (subscriber_type, subscriber_id, metric, quantity)\n\t\t\t VALUES ($1, $2, $3, $4)`,\n\t\t\t[subscriberType, subscriberId, metric, quantity],\n\t\t);\n\n\t\treturn this.get(key, windowMs);\n\t}\n\n\tasync get(key: string, windowMs: number | null): Promise<number> {\n\t\tconst [subscriberType, subscriberId, ...rest] = key.split(':');\n\t\tconst metric = rest.join(':');\n\t\tconst since = windowMs !== null ? new Date(Date.now() - windowMs) : new Date(0);\n\n\t\tconst rows = await this.store.query<{ total: string }>(\n\t\t\t`SELECT COALESCE(SUM(quantity), 0) AS total\n\t\t\t FROM fonderie_usage_records\n\t\t\t WHERE subscriber_type = $1\n\t\t\t AND subscriber_id = $2\n\t\t\t AND metric = $3\n\t\t\t AND recorded_at >= $4`,\n\t\t\t[subscriberType, subscriberId, metric, since],\n\t\t);\n\n\t\treturn parseInt(rows[0]?.total ?? '0', 10);\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { RateLimitBackendConfig } from '../config';\nimport { MemoryCounterBackend } from './memory';\nimport { DBCounterBackend } from './db';\n\nexport function createBackend(config: RateLimitBackendConfig | undefined, store: IStoreAdapter) {\n\tif (!config || config === 'memory') return new MemoryCounterBackend();\n\tif (config === 'db') return new DBCounterBackend(store);\n\treturn config;\n}\n\nexport type { ICounterBackend } from './types';\nexport { MemoryCounterBackend } from './memory';\nexport { DBCounterBackend } from './db';\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from './config';\nimport { buildBillingRoutes } from './routes';\nimport { syncPlansToDB } from './services/plans';\nimport { withBilling } from './middlewares/billing';\nimport { createBackend } from './backends';\n\nexport class BillingModule implements IFonderieModule {\n\treadonly name = '@fonderie/billing';\n\treadonly deps = ['@fonderie/auth'];\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate config: IBillingConfig,\n\t) {}\n\n\tasync install(app: IFonderieApp): Promise<void> {\n\t\tawait syncPlansToDB(this.config, this.store);\n\n\t\tconst backend = createBackend(this.config.rateLimit?.backend, this.store);\n\n\t\t// Global middleware — resolves subscriber + plan, enforces rate limits,\n\t\t// caches IBillingContext on ctx.meta['billing'] for every request.\n\t\t// Runs after auth (ctx.user available), before route handlers.\n\t\tapp.use(withBilling(this.store, this.config, backend));\n\n\t\tconst routes = buildBillingRoutes(this.store, this.config);\n\t\tfor (const [method, path, ...handlers] of routes) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n","import type { IBillingProvider, IBillingEvent, INormalizedSubscription } from './types';\nimport type { SubscriberType } from '../types';\n\ninterface IStripeSubscriptionRaw {\n\tid: string;\n\tstatus: string;\n\tcustomer: string;\n\tmetadata?: Record<string, string>;\n\titems: {\n\t\tdata: Array<{\n\t\t\tprice: { id: string; nickname: string | null; recurring?: { interval: string } };\n\t\t\t// Since Stripe API 2025+, the period lives on the item, not the subscription.\n\t\t\tcurrent_period_start?: number;\n\t\t\tcurrent_period_end?: number;\n\t\t}>;\n\t};\n\t// Older API versions (pre-2025) expose the period on the subscription itself.\n\tcurrent_period_start?: number;\n\tcurrent_period_end?: number;\n\tcancel_at_period_end: boolean;\n\ttrial_end: number | null;\n}\n\ninterface IStripeEventRaw {\n\ttype: string;\n\tdata: { object: unknown };\n}\n\n// Lazy singleton — Stripe SDK is optional\nlet _client: unknown = null;\n\nasync function getClient(secretKey: string): Promise<unknown> {\n\tif (_client) return _client;\n\n\tconst pkg = 'stripe';\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tconst mod: any = await import(pkg).catch(() => {\n\t\tthrow new Error('[billing:stripe] stripe is required: npm install stripe');\n\t});\n\n\tconst Stripe = mod.default ?? mod;\n\t_client = new Stripe(secretKey, { apiVersion: '2024-11-20.acacia' });\n\treturn _client;\n}\n\nfunction normalizeSubscription(sub: IStripeSubscriptionRaw): INormalizedSubscription {\n\tconst item = sub.items.data[0];\n\t// Period moved from the subscription to the item in Stripe API 2025+; read the\n\t// item first, fall back to the subscription-level fields for older versions.\n\tconst periodStart = item?.current_period_start ?? sub.current_period_start;\n\tconst periodEnd = item?.current_period_end ?? sub.current_period_end;\n\treturn {\n\t\tsubscriberType: (sub.metadata?.['subscriberType'] ?? 'workspace') as SubscriberType,\n\t\tsubscriberId: sub.metadata?.['subscriberId'] ?? '',\n\t\tplan: item?.price.nickname ?? 'unknown',\n\t\tstatus: sub.status,\n\t\tproviderCustomerId: sub.customer,\n\t\tproviderSubscriptionId: sub.id,\n\t\tcurrentPeriodStart: periodStart ? new Date(periodStart * 1000) : new Date(),\n\t\tcurrentPeriodEnd: periodEnd ? new Date(periodEnd * 1000) : new Date(),\n\t\tcancelAtPeriodEnd: sub.cancel_at_period_end,\n\t\ttrialEndsAt: sub.trial_end ? new Date(sub.trial_end * 1000) : null,\n\t\tinterval: item?.price.recurring?.interval === 'year' ? 'year' : 'month',\n\t};\n}\n\nexport class StripeProvider implements IBillingProvider {\n\treadonly name = 'stripe';\n\n\tconstructor(\n\t\tprivate secretKey: string,\n\t\tprivate webhookSecret?: string,\n\t) {}\n\n\tprivate async client(): Promise<any> {\n\t\treturn getClient(this.secretKey);\n\t}\n\n\tasync createCustomer(opts: {\n\t\temail: string;\n\t\tsubscriberType: SubscriberType;\n\t\tsubscriberId: string;\n\t\tuserId: string;\n\t}): Promise<{ customerId: string }> {\n\t\tconst stripe = await this.client();\n\t\tconst customer = await stripe.customers.create({\n\t\t\temail: opts.email,\n\t\t\tmetadata: {\n\t\t\t\tsubscriberType: opts.subscriberType,\n\t\t\t\tsubscriberId: opts.subscriberId,\n\t\t\t\tuserId: opts.userId,\n\t\t\t},\n\t\t});\n\t\treturn { customerId: customer.id };\n\t}\n\n\tasync createCheckoutSession(opts: {\n\t\tcustomerId: string;\n\t\tpriceId: string;\n\t\tsubscriberType: SubscriberType;\n\t\tsubscriberId: string;\n\t\ttrialDays?: number;\n\t\tsuccessUrl: string;\n\t\tcancelUrl: string;\n\t}): Promise<{ url: string }> {\n\t\tconst stripe = await this.client();\n\t\tconst session = await stripe.checkout.sessions.create({\n\t\t\tcustomer: opts.customerId,\n\t\t\tmode: 'subscription',\n\t\t\tline_items: [{ price: opts.priceId, quantity: 1 }],\n\t\t\tsuccess_url: opts.successUrl,\n\t\t\tcancel_url: opts.cancelUrl,\n\t\t\tsubscription_data: {\n\t\t\t\tmetadata: {\n\t\t\t\t\tsubscriberType: opts.subscriberType,\n\t\t\t\t\tsubscriberId: opts.subscriberId,\n\t\t\t\t},\n\t\t\t\t...(opts.trialDays && opts.trialDays > 0 ? { trial_period_days: opts.trialDays } : {}),\n\t\t\t},\n\t\t});\n\t\treturn { url: session.url ?? '' };\n\t}\n\n\tasync updateSubscription(opts: {\n\t\tsubscriptionId: string;\n\t\tpriceId: string;\n\t}): Promise<{ status: string; currentPeriodStart: Date | null; currentPeriodEnd: Date | null }> {\n\t\tconst stripe = await this.client();\n\t\tconst sub = await stripe.subscriptions.retrieve(opts.subscriptionId);\n\t\tconst itemId = sub.items.data[0]?.id;\n\t\t// Swap the price on the existing item and invoice the prorated difference\n\t\t// immediately (upgrade → pay the difference now).\n\t\tconst updated = await stripe.subscriptions.update(opts.subscriptionId, {\n\t\t\titems: [{ id: itemId, price: opts.priceId }],\n\t\t\tproration_behavior: 'always_invoice',\n\t\t\tpayment_behavior: 'error_if_incomplete',\n\t\t});\n\t\tconst item = updated.items?.data?.[0];\n\t\tconst cps = item?.current_period_start ?? updated.current_period_start;\n\t\tconst cpe = item?.current_period_end ?? updated.current_period_end;\n\t\treturn {\n\t\t\tstatus: updated.status,\n\t\t\tcurrentPeriodStart: cps ? new Date(cps * 1000) : null,\n\t\t\tcurrentPeriodEnd: cpe ? new Date(cpe * 1000) : null,\n\t\t};\n\t}\n\n\tasync createPortalSession(opts: {\n\t\tcustomerId: string;\n\t\treturnUrl: string;\n\t}): Promise<{ url: string }> {\n\t\tconst stripe = await this.client();\n\t\tconst session = await stripe.billingPortal.sessions.create({\n\t\t\tcustomer: opts.customerId,\n\t\t\treturn_url: opts.returnUrl,\n\t\t});\n\t\treturn { url: session.url };\n\t}\n\n\tasync constructEvent(opts: {\n\t\tpayload: string;\n\t\tsignature: string;\n\t\tsecret: string;\n\t}): Promise<IBillingEvent> {\n\t\tconst stripe = await this.client();\n\n\t\tlet raw: IStripeEventRaw;\n\t\ttry {\n\t\t\traw = stripe.webhooks.constructEvent(opts.payload, opts.signature, opts.secret);\n\t\t} catch {\n\t\t\tthrow new Error('[billing:stripe] Invalid webhook signature');\n\t\t}\n\n\t\tconst isSubscriptionEvent = [\n\t\t\t'customer.subscription.created',\n\t\t\t'customer.subscription.updated',\n\t\t\t'customer.subscription.deleted',\n\t\t].includes(raw.type);\n\n\t\tif (!isSubscriptionEvent) {\n\t\t\treturn { type: raw.type, subscription: null };\n\t\t}\n\n\t\tconst sub = raw.data.object as IStripeSubscriptionRaw;\n\n\t\tif (raw.type === 'customer.subscription.deleted') {\n\t\t\treturn {\n\t\t\t\ttype: raw.type,\n\t\t\t\tsubscription: { ...normalizeSubscription(sub), plan: 'free', status: 'canceled' },\n\t\t\t};\n\t\t}\n\n\t\treturn { type: raw.type, subscription: normalizeSubscription(sub) };\n\t}\n}\n","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 { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IBillingContext, IPolicyStatus } from './types';\n\nfunction getBillingContext(ctx: IFonderieContext): IBillingContext | null {\n\treturn (ctx.meta['billing'] as IBillingContext | undefined) ?? null;\n}\n\n// Returns true if the feature flag is enabled on the subscriber's plan.\n// Returns true when no billing context is present (fail-open when billing not configured).\nexport function hasFeature(ctx: IFonderieContext, key: string): boolean {\n\tconst billing = getBillingContext(ctx);\n\tif (!billing) return true;\n\n\tconst status = billing.statuses[key];\n\tif (!status) return true; // key not declared in policy → allow\n\tif (status.type === 'feature') return status.enabled;\n\treturn true; // counter entry = feature present\n}\n\n// Returns the advertised limit for a counter policy key, or null if unlimited / not configured.\nexport function getPlanLimit(ctx: IFonderieContext, key: string): number | null {\n\tconst billing = getBillingContext(ctx);\n\tif (!billing) return null;\n\n\tconst status = billing.statuses[key];\n\tif (!status || status.type === 'feature') return null;\n\treturn status.limit;\n}\n\n// Returns the full policy status for a key, or null if not configured.\nexport function getLimitStatus(ctx: IFonderieContext, key: string): IPolicyStatus | null {\n\tconst billing = getBillingContext(ctx);\n\tif (!billing) return null;\n\treturn billing.statuses[key] ?? null;\n}\n\n// Middleware — gates a route behind a feature flag.\n// Reads from cached ctx.meta['billing']; no store arg, no async DB call.\n// Fails open if billing context is absent (billing module not registered).\nexport function requireFeature(key: string): Middleware {\n\treturn (ctx, next) => {\n\t\tif (!hasFeature(ctx, key)) {\n\t\t\treturn Promise.resolve(\n\t\t\t\tsetApiResponse(\n\t\t\t\t\tHTTP.PAYMENT_REQUIRED,\n\t\t\t\t\t'FEATURE_UNAVAILABLE',\n\t\t\t\t\t`Feature '${key}' is not available on your current plan`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t};\n}\n"],"mappings":"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","names":["setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP"]}
|
|
1
|
+
{"version":3,"sources":["../src/routes.ts","../src/schemas.ts","../src/services/price-cache.ts","../src/controllers/plan.controller.ts","../src/services/plans.ts","../src/models/plan.model.ts","../src/dtos/billing.ts","../src/controllers/subscription.controller.ts","../src/services/subscriptions.ts","../src/models/subscription.model.ts","../src/utils.ts","../src/controllers/checkout.controller.ts","../src/controllers/usage.controller.ts","../src/services/usage.ts","../src/models/usage.model.ts","../src/controllers/webhook.controller.ts","../src/middlewares/billing.ts","../src/config.ts","../src/services/policy.ts","../src/backends/memory.ts","../src/backends/db.ts","../src/backends/index.ts","../src/module.ts","../src/types.ts","../src/providers/stripe.ts","../src/middlewares/require-plan.ts","../src/helpers.ts"],"sourcesContent":["import type { IStoreAdapter } from '@fonderie/store';\nimport type { Middleware } from '@fonderie/core';\nimport { requireAuth, validate } from '@fonderie/core/middlewares';\n\nimport { checkoutSchema, createPlanSchema, recordUsageSchema, updatePlanSchema } from './schemas';\n\nimport type { IBillingConfig } from './config';\nimport { PriceCache } from './services/price-cache';\nimport { planController } from './controllers/plan.controller';\nimport { subscriptionController } from './controllers/subscription.controller';\nimport { checkoutController } from './controllers/checkout.controller';\nimport { usageController } from './controllers/usage.controller';\nimport { webhookController } from './controllers/webhook.controller';\n\ntype RouteDefinition = [string, string, ...Middleware[]];\n\nexport function buildBillingRoutes(\n\tstore: IStoreAdapter,\n\tconfig: IBillingConfig,\n): RouteDefinition[] {\n\tconst priceCache = new PriceCache({\n\t\tttlMs: config.pricing?.cacheTtlMs,\n\t\tgraceMs: config.pricing?.transferGraceMs,\n\t\tmaxStaleMs: config.pricing?.maxStaleMs,\n\t});\n\tconst plan = planController(store, config, priceCache);\n\tconst subscription = subscriptionController(store);\n\tconst checkout = checkoutController(store, config);\n\tconst usage = usageController(store);\n\tconst webhook = webhookController(store, config, priceCache);\n\n\treturn [\n\t\t// Plans — public read-only\n\t\t['GET', '/plans', plan.list],\n\t\t['GET', '/plans/:planId', plan.get],\n\n\t\t// Plans — admin write (caller is responsible for authorization)\n\t\t['POST', '/plans', validate(createPlanSchema), plan.create],\n\t\t['PUT', '/plans/:planId', validate(updatePlanSchema), plan.update],\n\t\t['DELETE', '/plans/:planId', plan.delete],\n\n\t\t// Billing — subscriber resolved from X-Workspace-ID header (workspace) or session (user)\n\t\t// Workspace membership is verified automatically by the withBilling global middleware\n\t\t['GET', '/billing/subscription', requireAuth, subscription.get],\n\t\t['POST', '/billing/checkout', requireAuth, validate(checkoutSchema), checkout.createSession],\n\t\t['POST', '/billing/portal', requireAuth, checkout.createPortal],\n\t\t['POST', '/billing/usage', requireAuth, validate(recordUsageSchema), usage.record],\n\t\t['GET', '/billing/usage/:metric', requireAuth, usage.get],\n\n\t\t// Webhook — signature verified inside the handler\n\t\t['POST', '/billing/webhook', webhook.handle],\n\t];\n}\n","import { z } from 'zod';\n\n// Request schemas — the validation contract for billing's body-taking routes\n// (webhook excluded: provider-shaped, signature-verified in the handler).\n// Wired via @fonderie/core's validate(); same pattern as @fonderie/auth.\n\nconst planFields = {\n\tdescription: z.string().max(2000).nullable().optional(),\n\ttier: z.number().int().min(0).optional(),\n\tseats: z.number().int().min(0).nullable().optional(),\n\ttrialDays: z.number().int().min(0).optional(),\n\tmonthlyAmount: z.number().min(0).nullable().optional(),\n\tmonthlyPriceId: z.string().max(200).nullable().optional(),\n\tyearlyAmount: z.number().min(0).nullable().optional(),\n\tyearlyPriceId: z.string().max(200).nullable().optional(),\n\tfeatures: z.unknown().optional(),\n\tmetadata: z.unknown().optional(),\n};\n\nexport const createPlanSchema = z.object({\n\tname: z.string().trim().min(1, 'name is required').max(200),\n\t...planFields,\n});\n\nexport const updatePlanSchema = z\n\t.object({ name: z.string().trim().min(1).max(200).optional(), ...planFields })\n\t.refine((o) => Object.values(o).some((v) => v !== undefined), 'Provide at least one field');\n\nexport const checkoutSchema = z.object({\n\tplan: z.string().min(1, 'plan is required'),\n\tinterval: z.enum(['month', 'year']).optional(),\n});\n\nexport const recordUsageSchema = z.object({\n\tmetric: z.string().min(1, 'metric is required').max(100),\n\tquantity: z.number().min(0).optional(),\n});\n","import type { IBillingProvider, IResolvedPrice } from '../providers/types';\n\nexport interface IPriceCacheOptions {\n\tttlMs?: number | undefined; // fresh window — default 5m\n\tgraceMs?: number | undefined; // serve last-cached on transient miss (transfer race) — default 1h\n\tmaxStaleMs?: number | undefined; // serve last-cached during provider outage — default 24h\n}\n\nexport interface IPriceLookup {\n\t/** Resolved price, or null if it could not be resolved at all. */\n\tprice: IResolvedPrice | null;\n\t/** True when the returned price is served past its fresh TTL (transfer grace / outage). */\n\tstale: boolean;\n}\n\n/**\n * Read-through cache over the billing provider's price resolution.\n * - fresh within `ttlMs`\n * - single-flight: concurrent misses for the same id share one provider call (§16.2)\n * - transient miss (provider returns null — e.g. lookup_key transfer window) →\n * serve last-cached within `graceMs`, marked stale (§16.1)\n * - provider throws (outage) → serve last-cached within `maxStaleMs`, marked stale (§16.8)\n */\nexport class PriceCache {\n\tprivate readonly ttl: number;\n\tprivate readonly grace: number;\n\tprivate readonly maxStale: number;\n\tprivate readonly byId = new Map<string, { price: IResolvedPrice; at: number }>();\n\tprivate readonly inflight = new Map<string, Promise<IResolvedPrice | null>>();\n\n\tconstructor(opts: IPriceCacheOptions = {}) {\n\t\tthis.ttl = opts.ttlMs ?? 300_000;\n\t\tthis.grace = opts.graceMs ?? 3_600_000;\n\t\tthis.maxStale = opts.maxStaleMs ?? 86_400_000;\n\t}\n\n\tasync byPriceId(priceId: string, provider: IBillingProvider): Promise<IPriceLookup> {\n\t\tconst now = Date.now();\n\t\tconst hit = this.byId.get(priceId);\n\t\tif (hit && now - hit.at < this.ttl) return { price: hit.price, stale: false };\n\n\t\tlet fresh: IResolvedPrice | null;\n\t\ttry {\n\t\t\tfresh = await this.single(priceId, () => provider.resolvePriceById(priceId));\n\t\t} catch {\n\t\t\t// Provider outage — serve last-cached within maxStale.\n\t\t\tif (hit && now - hit.at < this.maxStale) return { price: hit.price, stale: true };\n\t\t\treturn { price: null, stale: true };\n\t\t}\n\t\tif (fresh) {\n\t\t\tthis.byId.set(priceId, { price: fresh, at: now });\n\t\t\treturn { price: fresh, stale: false };\n\t\t}\n\t\t// Transient miss (e.g. lookup_key transfer window) — serve last-cached within grace.\n\t\tif (hit && now - hit.at < this.grace) return { price: hit.price, stale: true };\n\t\treturn { price: null, stale: true };\n\t}\n\n\tinvalidate(priceId?: string): void {\n\t\tif (priceId) this.byId.delete(priceId);\n\t\telse this.byId.clear();\n\t}\n\n\t/** Warm the cache with prices already resolved elsewhere (e.g. boot guard). */\n\tprime(prices: Iterable<IResolvedPrice>): void {\n\t\tconst now = Date.now();\n\t\tfor (const p of prices) this.byId.set(p.priceId, { price: p, at: now });\n\t}\n\n\tprivate single(key: string, run: () => Promise<IResolvedPrice | null>): Promise<IResolvedPrice | null> {\n\t\tconst existing = this.inflight.get(key);\n\t\tif (existing) return existing;\n\t\tconst p = run().finally(() => this.inflight.delete(key));\n\t\tthis.inflight.set(key, p);\n\t\treturn p;\n\t}\n}\n","import { setApiResponse, HTTP, stringOrEmpty, numberOrZero } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport type { IPlan } from '../types';\nimport type { IPlanDTO } from '../dtos/billing';\nimport { PlanModel } from '../models/plan.model';\nimport { toPlanDTO } from '../dtos/billing';\nimport { PriceCache } from '../services/price-cache';\n\n// Read-through hydration: override the DTO's amount/currency with live Stripe\n// prices (source of truth). Best-effort per plan — on error (incl. currency\n// mismatch, §16.4) keep the fallback amount/currency and flag pricingStale.\nasync function hydratePricing(\n\tdto: IPlanDTO,\n\tplan: IPlan,\n\tconfig: IBillingConfig,\n\tcache: PriceCache,\n): Promise<void> {\n\ttry {\n\t\tlet stale = false;\n\t\tconst resolve = async (priceId: string | null) => {\n\t\t\tif (!priceId) return null;\n\t\t\tconst r = await cache.byPriceId(priceId, config.provider);\n\t\t\tif (r.stale) stale = true;\n\t\t\treturn r.price;\n\t\t};\n\t\tconst [m, y] = await Promise.all([resolve(plan.monthlyPriceId), resolve(plan.yearlyPriceId)]);\n\t\tif (m && y && m.currency !== y.currency) {\n\t\t\tthrow new Error(\n\t\t\t\t`[billing] plan \"${plan.name}\": monthly/yearly currency mismatch (${m.currency} vs ${y.currency})`,\n\t\t\t);\n\t\t}\n\t\tif (m) dto.pricing.monthly = m.unitAmount;\n\t\tif (y) dto.pricing.yearly = y.unitAmount;\n\t\tconst currency = m?.currency ?? y?.currency;\n\t\tif (currency) dto.pricing.currency = currency.toUpperCase();\n\t\tif (stale) dto.pricingStale = true;\n\t} catch (err) {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.error(`[billing] pricing hydration failed for \"${plan.name}\":`, (err as Error).message);\n\t\tdto.pricingStale = true;\n\t}\n}\n\nexport function planController(store: IStoreAdapter, config: IBillingConfig, cache: PriceCache) {\n\tconst plans = new PlanModel(store);\n\tconst hydrate = config.pricing?.hydration === true;\n\n\treturn {\n\t\tasync list(_ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst list = await plans.list();\n\t\t\tconst dtos = list.map(toPlanDTO);\n\t\t\tif (hydrate) {\n\t\t\t\tawait Promise.all(dtos.map((dto, i) => hydratePricing(dto, list[i]!, config, cache)));\n\t\t\t}\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_LIST', `Retrieved ${list.length} workspace plans`, {\n\t\t\t\tplans: dtos,\n\t\t\t});\n\t\t},\n\n\t\tasync get(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst id = params?.['planId'];\n\t\t\tif (!id) return setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Plan ID required');\n\n\t\t\tconst plan = await plans.findById(id);\n\t\t\tif (!plan) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Plan not found');\n\n\t\t\tconst dto = toPlanDTO(plan);\n\t\t\tif (hydrate) await hydratePricing(dto, plan, config, cache);\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_FETCHED', 'Plan retrieved successfully.', {\n\t\t\t\tplan: dto,\n\t\t\t});\n\t\t},\n\n\t\tasync create(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst name = stringOrEmpty(body?.['name']);\n\t\t\tif (!name) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'VALIDATION_ERROR', 'name is required');\n\t\t\t}\n\n\t\t\tconst plan = await plans.create({\n\t\t\t\tname,\n\t\t\t\tdescription: body?.['description'] != null ? String(body['description']) : null,\n\t\t\t\ttier: body?.['tier'] != null ? numberOrZero(body['tier']) : 0,\n\t\t\t\tseats: body?.['seats'] != null ? numberOrZero(body['seats']) : null,\n\t\t\t\ttrialDays: body?.['trialDays'] != null ? numberOrZero(body['trialDays']) : 0,\n\t\t\t\tmonthlyAmount: body?.['monthlyAmount'] != null ? numberOrZero(body['monthlyAmount']) : null,\n\t\t\t\tmonthlyPriceId: body?.['monthlyPriceId'] != null ? String(body['monthlyPriceId']) : null,\n\t\t\t\tyearlyAmount: body?.['yearlyAmount'] != null ? numberOrZero(body['yearlyAmount']) : null,\n\t\t\t\tyearlyPriceId: body?.['yearlyPriceId'] != null ? String(body['yearlyPriceId']) : null,\n\t\t\t\tfeatures: body?.['features'],\n\t\t\t\tmetadata: body?.['metadata'],\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.CREATED, 'PLAN_CREATED', 'Plan created successfully.', {\n\t\t\t\tplan: toPlanDTO(plan),\n\t\t\t});\n\t\t},\n\n\t\tasync update(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst id = params?.['planId'];\n\t\t\tif (!id) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Plan ID required');\n\t\t\t}\n\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tif (!body || Object.keys(body).length === 0) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'VALIDATION_ERROR', 'Request body is empty');\n\t\t\t}\n\n\t\t\tconst patch: Record<string, unknown> = {};\n\t\t\tconst allowed = ['name', 'description', 'tier', 'seats', 'trialDays',\n\t\t\t\t'monthlyAmount', 'monthlyPriceId', 'yearlyAmount', 'yearlyPriceId',\n\t\t\t\t'features', 'metadata'];\n\n\t\t\tfor (const key of allowed) {\n\t\t\t\tif (key in body) patch[key] = body[key];\n\t\t\t}\n\n\t\t\tconst plan = await plans.update(id, patch);\n\t\t\tif (!plan) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Plan not found');\n\t\t\t}\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_UPDATED', 'Plan updated successfully.', {\n\t\t\t\tplan: toPlanDTO(plan),\n\t\t\t});\n\t\t},\n\n\t\tasync delete(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst id = params?.['planId'];\n\t\t\tif (!id) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_PARAMETER', 'Plan ID required');\n\t\t\t}\n\n\t\t\tconst deleted = await plans.delete(id);\n\t\t\tif (!deleted) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Plan not found');\n\t\t\t}\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PLAN_DELETED', 'Plan deleted successfully.');\n\t\t},\n\t};\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPlan } from '../types';\nimport type { IBillingConfig, IBillingPlan } from '../config';\n\nexport function getPlans(config: IBillingConfig): IBillingPlan[] {\n\treturn config.plans;\n}\n\nexport function getPlanByName(name: string, config: IBillingConfig): IBillingPlan | null {\n\treturn config.plans.find((p) => p.name.toLowerCase() === name.toLowerCase()) ?? null;\n}\n\n/**\n * Attribute a subscription to a Fonderie plan by its price, precedence\n * `lookup_key → priceId` (§16.3). Pure — pass the plans list. Returns null so the\n * caller can fall back to the legacy nickname-derived plan.\n */\nexport function resolvePlanNameByPrice(\n\tprice: { lookupKey?: string | null; priceId?: string | null },\n\tplans: IBillingPlan[],\n): string | null {\n\tconst find = (pred: (p?: { lookupKey?: string; priceId?: string }) => boolean) =>\n\t\tplans.find((pl) => pred(pl.monthly) || pred(pl.yearly))?.name ?? null;\n\tif (price.lookupKey) {\n\t\tconst m = find((p) => p?.lookupKey === price.lookupKey);\n\t\tif (m) return m;\n\t}\n\tif (price.priceId) {\n\t\tconst m = find((p) => p?.priceId === price.priceId);\n\t\tif (m) return m;\n\t}\n\treturn null;\n}\n\nexport async function syncPlansToDB(config: IBillingConfig, store: IStoreAdapter): Promise<void> {\n\tconst plans = config.plans;\n\tif (plans.length === 0) return;\n\n\tconst values = plans.map((_, i) => {\n\t\tconst b = i * 9;\n\t\treturn `($${b + 1}, $${b + 2}, $${b + 3}, $${b + 4}, $${b + 5}, $${b + 6}, $${b + 7}, $${b + 8}, $${b + 9}::jsonb)`;\n\t});\n\n\tconst params = plans.flatMap((plan) => [\n\t\tplan.name,\n\t\tplan.trialDays ?? 0,\n\t\tplan.monthly?.amount ?? null,\n\t\tplan.monthly?.priceId ?? null,\n\t\tplan.yearly?.amount ?? null,\n\t\tplan.yearly?.priceId ?? null,\n\t\tplan.description ?? null,\n\t\tplan.tier ?? 0,\n\t\tJSON.stringify(plan.metadata ?? {}),\n\t]);\n\n\tawait store.query(\n\t\t`INSERT INTO fonderie_plans\n\t\t\t(name, trial_days,\n\t\t\t monthly_amount, monthly_price_id,\n\t\t\t yearly_amount, yearly_price_id,\n\t\t\t description, tier, metadata)\n\t\tVALUES ${values.join(', ')}\n\t\tON CONFLICT (name) DO UPDATE SET\n\t\t\ttrial_days = EXCLUDED.trial_days,\n\t\t\tmonthly_amount = EXCLUDED.monthly_amount,\n\t\t\tmonthly_price_id = EXCLUDED.monthly_price_id,\n\t\t\tyearly_amount = EXCLUDED.yearly_amount,\n\t\t\tyearly_price_id = EXCLUDED.yearly_price_id,\n\t\t\tdescription = EXCLUDED.description,\n\t\t\ttier = EXCLUDED.tier,\n\t\t\tmetadata = EXCLUDED.metadata`,\n\t\tparams,\n\t);\n}\n\nconst SELECT_PLAN = `\n\tSELECT\n\t\tid,\n\t\tname,\n\t\tseats,\n\t\ttrial_days AS \"trialDays\",\n\t\tmonthly_amount AS \"monthlyAmount\",\n\t\tmonthly_price_id AS \"monthlyPriceId\",\n\t\tyearly_amount AS \"yearlyAmount\",\n\t\tyearly_price_id AS \"yearlyPriceId\",\n\t\tdescription,\n\t\ttier,\n\t\tfeatures,\n\t\tmetadata\n\tFROM fonderie_plans`;\n\nexport async function getDBPlans(store: IStoreAdapter): Promise<IPlan[]> {\n\treturn store.query<IPlan>(\n\t\t`${SELECT_PLAN} WHERE active = true ORDER BY tier ASC, monthly_amount ASC NULLS LAST`,\n\t);\n}\n\nexport async function getPlanById(id: string, store: IStoreAdapter): Promise<IPlan | null> {\n\tconst [row] = await store.query<IPlan>(`${SELECT_PLAN} WHERE id = $1`, [id]);\n\treturn row ?? null;\n}\n\nexport async function createPlan(\n\tdata: {\n\t\tname: string;\n\t\tdescription?: string | null;\n\t\ttier?: number;\n\t\tseats?: number | null;\n\t\ttrialDays?: number;\n\t\tfeatures?: unknown;\n\t\tmetadata?: unknown;\n\t\tmonthlyAmount?: number | null;\n\t\tmonthlyPriceId?: string | null;\n\t\tyearlyAmount?: number | null;\n\t\tyearlyPriceId?: string | null;\n\t},\n\tstore: IStoreAdapter,\n): Promise<IPlan> {\n\tconst [row] = await store.query<IPlan>(\n\t\t`INSERT INTO fonderie_plans\n\t\t\t(name, seats, trial_days, monthly_amount, monthly_price_id,\n\t\t\t yearly_amount, yearly_price_id, description, tier, features, metadata)\n\t\tVALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb)\n\t\tRETURNING\n\t\t\tid, name, seats,\n\t\t\ttrial_days AS \"trialDays\",\n\t\t\tmonthly_amount AS \"monthlyAmount\",\n\t\t\tmonthly_price_id AS \"monthlyPriceId\",\n\t\t\tyearly_amount AS \"yearlyAmount\",\n\t\t\tyearly_price_id AS \"yearlyPriceId\",\n\t\t\tdescription, tier, features, metadata`,\n\t\t[\n\t\t\tdata.name,\n\t\t\tdata.seats ?? null,\n\t\t\tdata.trialDays ?? 0,\n\t\t\tdata.monthlyAmount ?? null,\n\t\t\tdata.monthlyPriceId ?? null,\n\t\t\tdata.yearlyAmount ?? null,\n\t\t\tdata.yearlyPriceId ?? null,\n\t\t\tdata.description ?? null,\n\t\t\tdata.tier ?? 0,\n\t\t\tJSON.stringify(data.features ?? []),\n\t\t\tJSON.stringify(data.metadata ?? {}),\n\t\t],\n\t);\n\tif (!row) throw new Error('Failed to create plan');\n\treturn row;\n}\n\nexport async function updatePlan(\n\tid: string,\n\tdata: Partial<Omit<IPlan, 'id'>>,\n\tstore: IStoreAdapter,\n): Promise<IPlan | null> {\n\tconst fieldMap: Record<string, string> = {\n\t\tname: 'name',\n\t\tseats: 'seats',\n\t\ttrialDays: 'trial_days',\n\t\tmonthlyAmount: 'monthly_amount',\n\t\tmonthlyPriceId: 'monthly_price_id',\n\t\tyearlyAmount: 'yearly_amount',\n\t\tyearlyPriceId: 'yearly_price_id',\n\t\tdescription: 'description',\n\t\ttier: 'tier',\n\t};\n\n\tconst jsonbFields = new Set(['features', 'metadata']);\n\tconst setClauses: string[] = [];\n\tconst params: unknown[] = [id];\n\n\tfor (const [key, col] of Object.entries(fieldMap)) {\n\t\tif (key in data) {\n\t\t\tparams.push((data as Record<string, unknown>)[key]);\n\t\t\tsetClauses.push(`${col} = $${params.length}`);\n\t\t}\n\t}\n\n\tfor (const key of jsonbFields) {\n\t\tif (key in data) {\n\t\t\tparams.push(JSON.stringify((data as Record<string, unknown>)[key]));\n\t\t\tsetClauses.push(`${key} = $${params.length}::jsonb`);\n\t\t}\n\t}\n\n\tif (setClauses.length === 0) return getPlanById(id, store);\n\n\tconst [row] = await store.query<IPlan>(\n\t\t`UPDATE fonderie_plans SET ${setClauses.join(', ')}\n\t\tWHERE id = $1\n\t\tRETURNING\n\t\t\tid, name, seats,\n\t\t\ttrial_days AS \"trialDays\",\n\t\t\tmonthly_amount AS \"monthlyAmount\",\n\t\t\tmonthly_price_id AS \"monthlyPriceId\",\n\t\t\tyearly_amount AS \"yearlyAmount\",\n\t\t\tyearly_price_id AS \"yearlyPriceId\",\n\t\t\tdescription, tier, features, metadata`,\n\t\tparams,\n\t);\n\treturn row ?? null;\n}\n\nexport async function deletePlan(id: string, store: IStoreAdapter): Promise<boolean> {\n\tconst rows = await store.query<{ id: string }>(\n\t\t`DELETE FROM fonderie_plans WHERE id = $1 RETURNING id`,\n\t\t[id],\n\t);\n\treturn rows.length > 0;\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IPlan } from '../types';\nimport type { IBillingConfig } from '../config';\nimport {\n\tgetDBPlans,\n\tgetPlanById,\n\tcreatePlan,\n\tupdatePlan,\n\tdeletePlan,\n\tgetPlans,\n\tgetPlanByName,\n} from '../services/plans';\n\nexport class PlanModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tlistFromConfig(config: IBillingConfig) {\n\t\treturn getPlans(config);\n\t}\n\n\tfindByNameInConfig(name: string, config: IBillingConfig) {\n\t\treturn getPlanByName(name, config);\n\t}\n\n\tlist(): Promise<IPlan[]> {\n\t\treturn getDBPlans(this.store);\n\t}\n\n\tfindById(id: string): Promise<IPlan | null> {\n\t\treturn getPlanById(id, this.store);\n\t}\n\n\tcreate(data: Parameters<typeof createPlan>[0]): Promise<IPlan> {\n\t\treturn createPlan(data, this.store);\n\t}\n\n\tupdate(id: string, data: Parameters<typeof updatePlan>[1]): Promise<IPlan | null> {\n\t\treturn updatePlan(id, data, this.store);\n\t}\n\n\tdelete(id: string): Promise<boolean> {\n\t\treturn deletePlan(id, this.store);\n\t}\n}\n","import type { IPlan, IPlanFeature, ISubscription, IUsageRecord, SubscriberType } from '../types';\n\nexport interface IPlanDTO {\n\tid: string;\n\tplanId: string;\n\tname: string;\n\tdescription: string;\n\ttier: number;\n\tseats: number | null;\n\ttrialDays: number;\n\tpricing: {\n\t\tmonthly: number; // in cents, e.g. 1999 = $19.99\n\t\tyearly: number; // in cents\n\t\tcurrency: string; // ISO 4217, e.g. 'USD'\n\t};\n\t/** True when pricing was served from stale cache (transfer window / provider outage). */\n\tpricingStale?: boolean;\n\tfeatures: IPlanFeature[];\n\tmetadata: Record<string, unknown>;\n}\n\nexport interface ISubscriptionDTO {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tplan: string;\n\tinterval: string;\n\tstatus: string;\n\tcancelAtPeriodEnd: boolean;\n\tcurrentPeriodStart: string | null;\n\tcurrentPeriodEnd: string | null;\n\ttrialEndsAt: string | null;\n\tcreatedAt: string;\n}\n\nexport interface IUsageRecordDTO {\n\tid: string;\n\tsubscriberType: SubscriberType;\n\tsubscriberId: string;\n\tmetric: string;\n\tquantity: number;\n\trecordedAt: string;\n}\n\nexport function toPlanDTO(plan: IPlan): IPlanDTO {\n\treturn {\n\t\tid: plan.id,\n\t\tplanId: plan.name.toUpperCase(),\n\t\tname: plan.name,\n\t\tdescription: plan.description ?? '',\n\t\ttier: plan.tier,\n\t\tseats: plan.seats,\n\t\ttrialDays: plan.trialDays,\n\t\tpricing: {\n\t\t\tmonthly: plan.monthlyAmount ?? 0,\n\t\t\tyearly: plan.yearlyAmount ?? 0,\n\t\t\tcurrency: 'USD',\n\t\t},\n\t\tfeatures: Array.isArray(plan.features) ? plan.features : [],\n\t\tmetadata:\n\t\t\tplan.metadata && typeof plan.metadata === 'object'\n\t\t\t\t? (plan.metadata as Record<string, unknown>)\n\t\t\t\t: {},\n\t};\n}\n\nexport function toSubscriptionDTO(sub: ISubscription): ISubscriptionDTO {\n\treturn {\n\t\tid: sub.id,\n\t\tsubscriberType: sub.subscriberType,\n\t\tsubscriberId: sub.subscriberId,\n\t\tplan: sub.plan,\n\t\tinterval: sub.interval,\n\t\tstatus: sub.status,\n\t\tcancelAtPeriodEnd: sub.cancelAtPeriodEnd,\n\t\tcurrentPeriodStart: sub.currentPeriodStart,\n\t\tcurrentPeriodEnd: sub.currentPeriodEnd,\n\t\ttrialEndsAt: sub.trialEndsAt,\n\t\tcreatedAt: sub.createdAt,\n\t};\n}\n\nexport function toUsageRecordDTO(record: IUsageRecord): IUsageRecordDTO {\n\treturn {\n\t\tid: record.id,\n\t\tsubscriberType: record.subscriberType,\n\t\tsubscriberId: record.subscriberId,\n\t\tmetric: record.metric,\n\t\tquantity: record.quantity,\n\t\trecordedAt: record.recordedAt,\n\t};\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { SubscriptionModel } from '../models/subscription.model';\nimport { toSubscriptionDTO } from '../dtos/billing';\nimport { resolveSubscriber } from '../utils';\n\nexport function subscriptionController(store: IStoreAdapter) {\n\tconst subscriptions = new SubscriptionModel(store);\n\n\treturn {\n\t\tasync get(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst subscription = await subscriptions.get(subscriber.type, subscriber.id);\n\t\t\tif (!subscription)\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No active subscription');\n\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'SUBSCRIPTION_FETCHED',\n\t\t\t\t'Subscription retrieved successfully.',\n\t\t\t\t{\n\t\t\t\t\tsubscription: toSubscriptionDTO(subscription),\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t};\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 { IStoreAdapter } from '@fonderie/store';\n\nimport type { ISubscription, SubscriberType } from '../types';\nimport { getSubscription, upsertSubscription } from '../services/subscriptions';\n\nexport class SubscriptionModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tget(subscriberType: SubscriberType, subscriberId: string): Promise<ISubscription | null> {\n\t\treturn getSubscription(subscriberType, subscriberId, this.store);\n\t}\n\n\tupsert(data: Parameters<typeof upsertSubscription>[0]): Promise<void> {\n\t\treturn upsertSubscription(data, this.store);\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 { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport { PlanModel } from '../models/plan.model';\nimport { SubscriptionModel } from '../models/subscription.model';\nimport { resolveSubscriber } from '../utils';\n\nexport function checkoutController(store: IStoreAdapter, config: IBillingConfig) {\n\tconst plans = new PlanModel(store);\n\tconst subscriptions = new SubscriptionModel(store);\n\n\treturn {\n\t\tasync createSession(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst planName = body?.['plan'];\n\t\t\tconst interval = (body?.['interval'] ?? 'month') as 'month' | 'year';\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t\tif (typeof planName !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'plan is required');\n\t\t\t}\n\t\t\tif (interval !== 'month' && interval !== 'year') {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'interval must be month or year',\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst plan = plans.findByNameInConfig(planName, config);\n\t\t\tif (!plan) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', `Unknown plan: ${planName}`);\n\t\t\t}\n\n\t\t\tconst pricing = interval === 'year' ? plan.yearly : plan.monthly;\n\t\t\tif (!pricing?.priceId) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t`Plan ${planName} does not support ${interval} billing`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// If there's already an active subscription: allow upgrades only, and\n\t\t\t// change the subscription in place (proration) rather than opening a\n\t\t\t// second checkout / creating a duplicate subscription.\n\t\t\tconst current = await subscriptions.get(subscriber.type, subscriber.id);\n\t\t\tconst ACTIVE = ['active', 'trialing', 'past_due'];\n\t\t\tif (current && ACTIVE.includes(current.status)) {\n\t\t\t\tconst currentTier = plans.findByNameInConfig(current.plan, config)?.tier ?? -1;\n\t\t\t\tconst targetTier = plan.tier ?? -1;\n\t\t\t\tif (targetTier <= currentTier) {\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'DOWNGRADE_NOT_ALLOWED',\n\t\t\t\t\t\t`Cannot switch from ${current.plan} to a same-or-lower tier (${planName}) mid-cycle. Upgrades only.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (current.providerSubscriptionId) {\n\t\t\t\t\tconst res = await config.provider.updateSubscription({\n\t\t\t\t\t\tsubscriptionId: current.providerSubscriptionId,\n\t\t\t\t\t\tpriceId: pricing.priceId,\n\t\t\t\t\t});\n\t\t\t\t\tconst upsert: Parameters<typeof subscriptions.upsert>[0] = {\n\t\t\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\t\t\tplan: planName,\n\t\t\t\t\t\tinterval,\n\t\t\t\t\t\tstatus: res.status,\n\t\t\t\t\t\tproviderSubscriptionId: current.providerSubscriptionId,\n\t\t\t\t\t};\n\t\t\t\t\tif (current.providerCustomerId) upsert.providerCustomerId = current.providerCustomerId;\n\t\t\t\t\tif (res.currentPeriodStart) upsert.currentPeriodStart = res.currentPeriodStart;\n\t\t\t\t\tif (res.currentPeriodEnd) upsert.currentPeriodEnd = res.currentPeriodEnd;\n\t\t\t\t\tawait subscriptions.upsert(upsert);\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.OK,\n\t\t\t\t\t\t'SUBSCRIPTION_UPGRADED',\n\t\t\t\t\t\t'Subscription upgraded; the prorated difference was charged.',\n\t\t\t\t\t\t{ upgraded: true, plan: planName },\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { customerId } = await config.provider.createCustomer({\n\t\t\t\temail: ctx.user!.email ?? '',\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tuserId: ctx.user!.id,\n\t\t\t});\n\n\t\t\tconst sessionOpts: Parameters<typeof config.provider.createCheckoutSession>[0] = {\n\t\t\t\tcustomerId,\n\t\t\t\tpriceId: pricing.priceId,\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tsuccessUrl: config.successUrl,\n\t\t\t\tcancelUrl: config.cancelUrl,\n\t\t\t};\n\t\t\tif (plan.trialDays !== undefined) sessionOpts.trialDays = plan.trialDays;\n\n\t\t\tconst { url } = await config.provider.createCheckoutSession(sessionOpts);\n\n\t\t\tawait subscriptions.upsert({\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tplan: planName,\n\t\t\t\tinterval,\n\t\t\t\tstatus: 'incomplete',\n\t\t\t\tproviderCustomerId: customerId,\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'CHECKOUT_URL', 'Checkout session created.', { url });\n\t\t},\n\n\t\tasync createPortal(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst subscription = await subscriptions.get(subscriber.type, subscriber.id);\n\t\t\tif (!subscription?.providerCustomerId) {\n\t\t\t\treturn setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No active subscription');\n\t\t\t}\n\n\t\t\tconst { url } = await config.provider.createPortalSession({\n\t\t\t\tcustomerId: subscription.providerCustomerId,\n\t\t\t\treturnUrl: config.successUrl,\n\t\t\t});\n\n\t\t\treturn setApiResponse(HTTP.OK, 'PORTAL_URL', 'Portal session created.', { url });\n\t\t},\n\t};\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { UsageModel } from '../models/usage.model';\nimport { resolveSubscriber } from '../utils';\n\nexport function usageController(store: IStoreAdapter) {\n\tconst usage = new UsageModel(store);\n\n\treturn {\n\t\tasync record(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst body = ctx.meta['body'] as Record<string, unknown> | undefined;\n\t\t\tconst metric = body?.['metric'];\n\t\t\tconst quantity = body?.['quantity'];\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t\tif (!subscriber) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.BAD_REQUEST,\n\t\t\t\t\t'SUBSCRIBER_REQUIRED',\n\t\t\t\t\t'Subscriber context required',\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (typeof metric !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'metric is required');\n\t\t\t}\n\n\t\t\tawait usage.record({\n\t\t\t\tsubscriberType: subscriber.type,\n\t\t\t\tsubscriberId: subscriber.id,\n\t\t\t\tmetric,\n\t\t\t\tquantity: typeof quantity === 'number' ? quantity : 1,\n\t\t\t});\n\t\t\treturn setApiResponse(HTTP.OK, 'USAGE_RECORDED', 'Usage recorded successfully.');\n\t\t},\n\n\t\tasync get(ctx: IFonderieContext): Promise<Response> {\n\t\t\tconst params = ctx.meta['params'] as Record<string, string> | undefined;\n\t\t\tconst metric = params?.['metric'];\n\t\t\tconst subscriber = resolveSubscriber(ctx);\n\n\t\t\tif (!subscriber || !metric) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t'INVALID_PARAMETER',\n\t\t\t\t\t'subscriber and metric are required',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst since = new Date();\n\t\t\tsince.setDate(1);\n\t\t\tsince.setHours(0, 0, 0, 0);\n\n\t\t\tconst total = await usage.get(subscriber.type, subscriber.id, metric, since);\n\t\t\treturn setApiResponse(HTTP.OK, 'USAGE_FETCHED', 'Usage retrieved successfully.', {\n\t\t\t\tmetric,\n\t\t\t\ttotal,\n\t\t\t\tsince,\n\t\t\t});\n\t\t},\n\t};\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { SubscriberType } from '../types';\n\nexport async function recordUsage(\n\topts: { subscriberType: SubscriberType; subscriberId: string; metric: string; quantity: number },\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`INSERT INTO fonderie_usage_records (subscriber_type, subscriber_id, metric, quantity)\n\t\tVALUES ($1, $2, $3, $4)`,\n\t\t[opts.subscriberType, opts.subscriberId, opts.metric, opts.quantity],\n\t);\n}\n\nexport async function getUsage(\n\tsubscriberType: SubscriberType,\n\tsubscriberId: string,\n\tmetric: string,\n\tsince: Date,\n\tstore: IStoreAdapter,\n): Promise<number> {\n\tconst rows = await store.query<{ total: string }>(\n\t\t`SELECT COALESCE(SUM(quantity), 0) AS total\n\t\tFROM fonderie_usage_records\n\t\tWHERE subscriber_type = $1\n\t\t\tAND subscriber_id = $2\n\t\t\tAND metric = $3\n\t\t\tAND recorded_at >= $4`,\n\t\t[subscriberType, subscriberId, metric, since],\n\t);\n\treturn parseInt(rows[0]?.total ?? '0', 10);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { SubscriberType } from '../types';\n\nimport { recordUsage, getUsage } from '../services/usage';\n\nexport class UsageModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\trecord(opts: Parameters<typeof recordUsage>[0]): Promise<void> {\n\t\treturn recordUsage(opts, this.store);\n\t}\n\n\tget(\n\t\tsubscriberType: SubscriberType,\n\t\tsubscriberId: string,\n\t\tmetric: string,\n\t\tsince: Date,\n\t): Promise<number> {\n\t\treturn getUsage(subscriberType, subscriberId, metric, since, this.store);\n\t}\n}\n","import { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from '../config';\nimport type { PriceCache } from '../services/price-cache';\nimport { SubscriptionModel } from '../models/subscription.model';\nimport { resolvePlanNameByPrice } from '../services/plans';\n\nexport function webhookController(store: IStoreAdapter, config: IBillingConfig, priceCache?: PriceCache) {\n\tconst subscriptions = new SubscriptionModel(store);\n\n\treturn {\n\t\tasync handle(ctx: IFonderieContext): Promise<Response> {\n\t\t\tif (!config.webhookSecret) {\n\t\t\t\treturn setApiResponse(HTTP.SERVER_ERROR, 'SERVER_ERROR', 'Webhook secret not configured');\n\t\t\t}\n\n\t\t\tconst signature =\n\t\t\t\tctx.request.headers.get('stripe-signature') ??\n\t\t\t\tctx.request.headers.get('paypal-auth-algo') ??\n\t\t\t\t'';\n\n\t\t\tif (!signature) {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Missing webhook signature');\n\t\t\t}\n\n\t\t\tconst payload = await ctx.request.text();\n\n\t\t\tlet event: Awaited<ReturnType<typeof config.provider.constructEvent>>;\n\t\t\ttry {\n\t\t\t\tevent = await config.provider.constructEvent({\n\t\t\t\t\tpayload,\n\t\t\t\t\tsignature,\n\t\t\t\t\tsecret: config.webhookSecret,\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\treturn setApiResponse(HTTP.BAD_REQUEST, 'INVALID_REQUEST', 'Invalid webhook signature');\n\t\t\t}\n\n\t\t\t// §8: keep the price cache honest. Invalidate on any price/product change\n\t\t\t// regardless of arrival order (invalidate-and-refetch is order-safe).\n\t\t\tif (priceCache && (event.type.startsWith('price.') || event.type.startsWith('product.'))) {\n\t\t\t\tpriceCache.invalidate();\n\t\t\t}\n\n\t\t\tif (event.subscription) {\n\t\t\t\t// A deletion resolves to the free/canceled state set by the provider;\n\t\t\t\t// otherwise map the plan from the price (dual-mapping), falling back to\n\t\t\t\t// the nickname-derived value.\n\t\t\t\tconst plan =\n\t\t\t\t\tevent.type === 'customer.subscription.deleted'\n\t\t\t\t\t\t? event.subscription.plan\n\t\t\t\t\t\t: resolvePlanNameByPrice(event.subscription, config.plans) ?? event.subscription.plan;\n\n\t\t\t\tawait subscriptions.upsert({\n\t\t\t\t\tsubscriberType: event.subscription.subscriberType,\n\t\t\t\t\tsubscriberId: event.subscription.subscriberId,\n\t\t\t\t\tplan,\n\t\t\t\t\tinterval: event.subscription.interval,\n\t\t\t\t\tstatus: event.subscription.status,\n\t\t\t\t\tproviderCustomerId: event.subscription.providerCustomerId,\n\t\t\t\t\tproviderSubscriptionId: event.subscription.providerSubscriptionId,\n\t\t\t\t\tcurrentPeriodStart: event.subscription.currentPeriodStart,\n\t\t\t\t\tcurrentPeriodEnd: event.subscription.currentPeriodEnd,\n\t\t\t\t\tcancelAtPeriodEnd: event.subscription.cancelAtPeriodEnd,\n\t\t\t\t\ttrialEndsAt: event.subscription.trialEndsAt,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn Response.json({ received: true });\n\t\t},\n\t};\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 * @deprecated Display amount in cents. When pricing hydration is on and the\n\t * price resolves from Stripe, the live amount wins; this is the fallback only.\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), use the deprecated hardcoded amount/USD path. */\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","import type { ICounterBackend } from './types';\n\ninterface Entry {\n\tcount: number;\n\twindowStart: number; // epoch ms — used for windowed expiry\n}\n\nexport class MemoryCounterBackend implements ICounterBackend {\n\tprivate readonly counters = new Map<string, Entry>();\n\n\tasync increment(key: string, windowMs: number | null, quantity = 1): Promise<number> {\n\t\tconst now = Date.now();\n\t\tconst existing = this.counters.get(key);\n\n\t\tif (!existing || (windowMs !== null && now - existing.windowStart >= windowMs)) {\n\t\t\tthis.counters.set(key, { count: quantity, windowStart: now });\n\t\t\treturn quantity;\n\t\t}\n\n\t\texisting.count += quantity;\n\t\treturn existing.count;\n\t}\n\n\tasync get(key: string, windowMs: number | null): Promise<number> {\n\t\tconst now = Date.now();\n\t\tconst existing = this.counters.get(key);\n\t\tif (!existing) return 0;\n\t\tif (windowMs !== null && now - existing.windowStart >= windowMs) return 0;\n\t\treturn existing.count;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { ICounterBackend } from './types';\n\nexport class DBCounterBackend implements ICounterBackend {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tasync increment(key: string, windowMs: number | null, quantity = 1): Promise<number> {\n\t\tconst [subscriberType, subscriberId, ...rest] = key.split(':');\n\t\tconst metric = rest.join(':');\n\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_usage_records (subscriber_type, subscriber_id, metric, quantity)\n\t\t\t VALUES ($1, $2, $3, $4)`,\n\t\t\t[subscriberType, subscriberId, metric, quantity],\n\t\t);\n\n\t\treturn this.get(key, windowMs);\n\t}\n\n\tasync get(key: string, windowMs: number | null): Promise<number> {\n\t\tconst [subscriberType, subscriberId, ...rest] = key.split(':');\n\t\tconst metric = rest.join(':');\n\t\tconst since = windowMs !== null ? new Date(Date.now() - windowMs) : new Date(0);\n\n\t\tconst rows = await this.store.query<{ total: string }>(\n\t\t\t`SELECT COALESCE(SUM(quantity), 0) AS total\n\t\t\t FROM fonderie_usage_records\n\t\t\t WHERE subscriber_type = $1\n\t\t\t AND subscriber_id = $2\n\t\t\t AND metric = $3\n\t\t\t AND recorded_at >= $4`,\n\t\t\t[subscriberType, subscriberId, metric, since],\n\t\t);\n\n\t\treturn parseInt(rows[0]?.total ?? '0', 10);\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { RateLimitBackendConfig } from '../config';\nimport { MemoryCounterBackend } from './memory';\nimport { DBCounterBackend } from './db';\n\nexport function createBackend(config: RateLimitBackendConfig | undefined, store: IStoreAdapter) {\n\tif (!config || config === 'memory') return new MemoryCounterBackend();\n\tif (config === 'db') return new DBCounterBackend(store);\n\treturn config;\n}\n\nexport type { ICounterBackend } from './types';\nexport { MemoryCounterBackend } from './memory';\nexport { DBCounterBackend } from './db';\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IBillingConfig } from './config';\nimport { buildBillingRoutes } from './routes';\nimport { syncPlansToDB } from './services/plans';\nimport { withBilling } from './middlewares/billing';\nimport { createBackend } from './backends';\n\nexport class BillingModule implements IFonderieModule {\n\treadonly name = '@fonderie/billing';\n\treadonly deps = ['@fonderie/auth'];\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate config: IBillingConfig,\n\t) {}\n\n\tasync install(app: IFonderieApp): Promise<void> {\n\t\tawait syncPlansToDB(this.config, this.store);\n\n\t\tconst backend = createBackend(this.config.rateLimit?.backend, this.store);\n\n\t\t// Global middleware — resolves subscriber + plan, enforces rate limits,\n\t\t// caches IBillingContext on ctx.meta['billing'] for every request.\n\t\t// Runs after auth (ctx.user available), before route handlers.\n\t\tapp.use(withBilling(this.store, this.config, backend));\n\n\t\tconst routes = buildBillingRoutes(this.store, this.config);\n\t\tfor (const [method, path, ...handlers] of routes) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n","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","import type { IBillingProvider, IBillingEvent, INormalizedSubscription, IResolvedPrice } from './types';\nimport { BILLING_INTERVAL } from '../types';\nimport type { SubscriberType } from '../types';\n\ninterface IStripeSubscriptionRaw {\n\tid: string;\n\tstatus: string;\n\tcustomer: string;\n\tmetadata?: Record<string, string>;\n\titems: {\n\t\tdata: Array<{\n\t\t\tprice: { id: string; nickname: string | null; lookup_key?: string | null; recurring?: { interval: string } };\n\t\t\t// Since Stripe API 2025+, the period lives on the item, not the subscription.\n\t\t\tcurrent_period_start?: number;\n\t\t\tcurrent_period_end?: number;\n\t\t}>;\n\t};\n\t// Older API versions (pre-2025) expose the period on the subscription itself.\n\tcurrent_period_start?: number;\n\tcurrent_period_end?: number;\n\tcancel_at_period_end: boolean;\n\ttrial_end: number | null;\n}\n\ninterface IStripeEventRaw {\n\ttype: string;\n\tdata: { object: unknown };\n}\n\n// Lazy singleton — Stripe SDK is optional\nlet _client: unknown = null;\n\nasync function getClient(secretKey: string): Promise<unknown> {\n\tif (_client) return _client;\n\n\tconst pkg = 'stripe';\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tconst mod: any = await import(pkg).catch(() => {\n\t\tthrow new Error('[billing:stripe] stripe is required: npm install stripe');\n\t});\n\n\tconst Stripe = mod.default ?? mod;\n\t_client = new Stripe(secretKey, { apiVersion: '2024-11-20.acacia' });\n\treturn _client;\n}\n\nfunction normalizeSubscription(sub: IStripeSubscriptionRaw): INormalizedSubscription {\n\tconst item = sub.items.data[0];\n\t// Period moved from the subscription to the item in Stripe API 2025+; read the\n\t// item first, fall back to the subscription-level fields for older versions.\n\tconst periodStart = item?.current_period_start ?? sub.current_period_start;\n\tconst periodEnd = item?.current_period_end ?? sub.current_period_end;\n\treturn {\n\t\tsubscriberType: (sub.metadata?.['subscriberType'] ?? 'workspace') as SubscriberType,\n\t\tsubscriberId: sub.metadata?.['subscriberId'] ?? '',\n\t\tplan: item?.price.nickname ?? 'unknown',\n\t\tpriceLookupKey: item?.price.lookup_key ?? null,\n\t\tpriceId: item?.price.id ?? null,\n\t\tstatus: sub.status,\n\t\tproviderCustomerId: sub.customer,\n\t\tproviderSubscriptionId: sub.id,\n\t\tcurrentPeriodStart: periodStart ? new Date(periodStart * 1000) : new Date(),\n\t\tcurrentPeriodEnd: periodEnd ? new Date(periodEnd * 1000) : new Date(),\n\t\tcancelAtPeriodEnd: sub.cancel_at_period_end,\n\t\ttrialEndsAt: sub.trial_end ? new Date(sub.trial_end * 1000) : null,\n\t\tinterval: item?.price.recurring?.interval === BILLING_INTERVAL.YEAR ? BILLING_INTERVAL.YEAR : BILLING_INTERVAL.MONTH,\n\t};\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction toResolvedPrice(p: any): IResolvedPrice {\n\treturn {\n\t\tpriceId: p.id,\n\t\tlookupKey: p.lookup_key ?? null,\n\t\tunitAmount: p.unit_amount ?? 0,\n\t\tcurrency: p.currency,\n\t\tinterval: p.recurring?.interval === BILLING_INTERVAL.YEAR ? BILLING_INTERVAL.YEAR : BILLING_INTERVAL.MONTH,\n\t\tnickname: p.nickname ?? null,\n\t\tproductId: typeof p.product === 'string' ? p.product : (p.product?.id ?? ''),\n\t\tactive: p.active ?? true,\n\t};\n}\n\nexport class StripeProvider implements IBillingProvider {\n\treadonly name = 'stripe';\n\n\tconstructor(\n\t\tprivate secretKey: string,\n\t\tprivate webhookSecret?: string,\n\t) {}\n\n\tprivate async client(): Promise<any> {\n\t\treturn getClient(this.secretKey);\n\t}\n\n\tasync createCustomer(opts: {\n\t\temail: string;\n\t\tsubscriberType: SubscriberType;\n\t\tsubscriberId: string;\n\t\tuserId: string;\n\t}): Promise<{ customerId: string }> {\n\t\tconst stripe = await this.client();\n\t\tconst customer = await stripe.customers.create({\n\t\t\temail: opts.email,\n\t\t\tmetadata: {\n\t\t\t\tsubscriberType: opts.subscriberType,\n\t\t\t\tsubscriberId: opts.subscriberId,\n\t\t\t\tuserId: opts.userId,\n\t\t\t},\n\t\t});\n\t\treturn { customerId: customer.id };\n\t}\n\n\tasync createCheckoutSession(opts: {\n\t\tcustomerId: string;\n\t\tpriceId: string;\n\t\tsubscriberType: SubscriberType;\n\t\tsubscriberId: string;\n\t\ttrialDays?: number;\n\t\tsuccessUrl: string;\n\t\tcancelUrl: string;\n\t}): Promise<{ url: string }> {\n\t\tconst stripe = await this.client();\n\t\tconst session = await stripe.checkout.sessions.create({\n\t\t\tcustomer: opts.customerId,\n\t\t\tmode: 'subscription',\n\t\t\tline_items: [{ price: opts.priceId, quantity: 1 }],\n\t\t\tsuccess_url: opts.successUrl,\n\t\t\tcancel_url: opts.cancelUrl,\n\t\t\tsubscription_data: {\n\t\t\t\tmetadata: {\n\t\t\t\t\tsubscriberType: opts.subscriberType,\n\t\t\t\t\tsubscriberId: opts.subscriberId,\n\t\t\t\t},\n\t\t\t\t...(opts.trialDays && opts.trialDays > 0 ? { trial_period_days: opts.trialDays } : {}),\n\t\t\t},\n\t\t});\n\t\treturn { url: session.url ?? '' };\n\t}\n\n\tasync resolvePriceById(priceId: string): Promise<IResolvedPrice | null> {\n\t\tconst stripe = await this.client();\n\t\ttry {\n\t\t\tconst p = await stripe.prices.retrieve(priceId, { expand: ['product'] });\n\t\t\treturn toResolvedPrice(p);\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tasync resolvePricesByLookupKey(lookupKeys: string[]): Promise<Map<string, IResolvedPrice>> {\n\t\tconst out = new Map<string, IResolvedPrice>();\n\t\tif (lookupKeys.length === 0) return out;\n\t\tconst stripe = await this.client();\n\t\tconst res = await stripe.prices.list({\n\t\t\tlookup_keys: lookupKeys,\n\t\t\tactive: true,\n\t\t\texpand: ['data.product'],\n\t\t\tlimit: 100,\n\t\t});\n\t\tfor (const p of res.data) {\n\t\t\tif (p.lookup_key) out.set(p.lookup_key, toResolvedPrice(p));\n\t\t}\n\t\treturn out;\n\t}\n\n\tasync updateSubscription(opts: {\n\t\tsubscriptionId: string;\n\t\tpriceId: string;\n\t}): Promise<{ status: string; currentPeriodStart: Date | null; currentPeriodEnd: Date | null }> {\n\t\tconst stripe = await this.client();\n\t\tconst sub = await stripe.subscriptions.retrieve(opts.subscriptionId);\n\t\tconst itemId = sub.items.data[0]?.id;\n\t\t// Swap the price on the existing item and invoice the prorated difference\n\t\t// immediately (upgrade → pay the difference now).\n\t\tconst updated = await stripe.subscriptions.update(opts.subscriptionId, {\n\t\t\titems: [{ id: itemId, price: opts.priceId }],\n\t\t\tproration_behavior: 'always_invoice',\n\t\t\tpayment_behavior: 'error_if_incomplete',\n\t\t});\n\t\tconst item = updated.items?.data?.[0];\n\t\tconst cps = item?.current_period_start ?? updated.current_period_start;\n\t\tconst cpe = item?.current_period_end ?? updated.current_period_end;\n\t\treturn {\n\t\t\tstatus: updated.status,\n\t\t\tcurrentPeriodStart: cps ? new Date(cps * 1000) : null,\n\t\t\tcurrentPeriodEnd: cpe ? new Date(cpe * 1000) : null,\n\t\t};\n\t}\n\n\tasync createPortalSession(opts: {\n\t\tcustomerId: string;\n\t\treturnUrl: string;\n\t}): Promise<{ url: string }> {\n\t\tconst stripe = await this.client();\n\t\tconst session = await stripe.billingPortal.sessions.create({\n\t\t\tcustomer: opts.customerId,\n\t\t\treturn_url: opts.returnUrl,\n\t\t});\n\t\treturn { url: session.url };\n\t}\n\n\tasync constructEvent(opts: {\n\t\tpayload: string;\n\t\tsignature: string;\n\t\tsecret: string;\n\t}): Promise<IBillingEvent> {\n\t\tconst stripe = await this.client();\n\n\t\tlet raw: IStripeEventRaw;\n\t\ttry {\n\t\t\traw = stripe.webhooks.constructEvent(opts.payload, opts.signature, opts.secret);\n\t\t} catch {\n\t\t\tthrow new Error('[billing:stripe] Invalid webhook signature');\n\t\t}\n\n\t\tconst isSubscriptionEvent = [\n\t\t\t'customer.subscription.created',\n\t\t\t'customer.subscription.updated',\n\t\t\t'customer.subscription.deleted',\n\t\t].includes(raw.type);\n\n\t\tif (!isSubscriptionEvent) {\n\t\t\treturn { type: raw.type, subscription: null };\n\t\t}\n\n\t\tconst sub = raw.data.object as IStripeSubscriptionRaw;\n\n\t\tif (raw.type === 'customer.subscription.deleted') {\n\t\t\treturn {\n\t\t\t\ttype: raw.type,\n\t\t\t\tsubscription: { ...normalizeSubscription(sub), plan: 'free', status: 'canceled' },\n\t\t\t};\n\t\t}\n\n\t\treturn { type: raw.type, subscription: normalizeSubscription(sub) };\n\t}\n}\n","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 { setApiResponse, HTTP } from '@fonderie/core';\nimport type { IFonderieContext } from '@fonderie/core';\nimport type { Middleware } from '@fonderie/core';\nimport type { IBillingContext, IPolicyStatus } from './types';\n\nfunction getBillingContext(ctx: IFonderieContext): IBillingContext | null {\n\treturn (ctx.meta['billing'] as IBillingContext | undefined) ?? null;\n}\n\n// Returns true if the feature flag is enabled on the subscriber's plan.\n// Returns true when no billing context is present (fail-open when billing not configured).\nexport function hasFeature(ctx: IFonderieContext, key: string): boolean {\n\tconst billing = getBillingContext(ctx);\n\tif (!billing) return true;\n\n\tconst status = billing.statuses[key];\n\tif (!status) return true; // key not declared in policy → allow\n\tif (status.type === 'feature') return status.enabled;\n\treturn true; // counter entry = feature present\n}\n\n// Returns the advertised limit for a counter policy key, or null if unlimited / not configured.\nexport function getPlanLimit(ctx: IFonderieContext, key: string): number | null {\n\tconst billing = getBillingContext(ctx);\n\tif (!billing) return null;\n\n\tconst status = billing.statuses[key];\n\tif (!status || status.type === 'feature') return null;\n\treturn status.limit;\n}\n\n// Returns the full policy status for a key, or null if not configured.\nexport function getLimitStatus(ctx: IFonderieContext, key: string): IPolicyStatus | null {\n\tconst billing = getBillingContext(ctx);\n\tif (!billing) return null;\n\treturn billing.statuses[key] ?? null;\n}\n\n// Middleware — gates a route behind a feature flag.\n// Reads from cached ctx.meta['billing']; no store arg, no async DB call.\n// Fails open if billing context is absent (billing module not registered).\nexport function requireFeature(key: string): Middleware {\n\treturn (ctx, next) => {\n\t\tif (!hasFeature(ctx, key)) {\n\t\t\treturn Promise.resolve(\n\t\t\t\tsetApiResponse(\n\t\t\t\t\tHTTP.PAYMENT_REQUIRED,\n\t\t\t\t\t'FEATURE_UNAVAILABLE',\n\t\t\t\t\t`Feature '${key}' is not available on your current plan`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\treturn next();\n\t};\n}\n"],"mappings":"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","names":["setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP","setApiResponse","HTTP"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/middlewares/index.ts","../../src/middlewares/require-plan.ts","../../src/services/subscriptions.ts","../../src/utils.ts","../../src/middlewares/billing.ts","../../src/config.ts","../../src/services/policy.ts"],"sourcesContent":["export { requirePlan } from './require-plan';\nexport { withBilling } from './billing';\n","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;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAAqC;;;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,iBAAO,4BAAe,iBAAK,cAAc,gBAAgB,cAAc;AAAA,IACxE;AAEA,UAAM,aAAa,kBAAkB,GAAG;AACxC,QAAI,CAAC,YAAY;AAChB,iBAAO,4BAAe,iBAAK,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,iBAAO;AAAA,QACN,iBAAK;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,iBAAO;AAAA,QACN,iBAAK;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,IAAAA,eAAqC;;;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,mBAAO;AAAA,UACN,kBAAK;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":["import_core"]}
|
|
1
|
+
{"version":3,"sources":["../../src/middlewares/index.ts","../../src/middlewares/require-plan.ts","../../src/services/subscriptions.ts","../../src/utils.ts","../../src/middlewares/billing.ts","../../src/config.ts","../../src/services/policy.ts"],"sourcesContent":["export { requirePlan } from './require-plan';\nexport { withBilling } from './billing';\n","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 * @deprecated Display amount in cents. When pricing hydration is on and the\n\t * price resolves from Stripe, the live amount wins; this is the fallback only.\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), use the deprecated hardcoded amount/USD path. */\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":["import_core"]}
|