@fonderie/billing 1.0.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.cjs ADDED
@@ -0,0 +1,1170 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BillingModule: () => BillingModule,
24
+ DBCounterBackend: () => DBCounterBackend,
25
+ MESSAGE_KEYS: () => MESSAGE_KEYS,
26
+ MemoryCounterBackend: () => MemoryCounterBackend,
27
+ StripeProvider: () => StripeProvider,
28
+ createPlan: () => createPlan,
29
+ deletePlan: () => deletePlan,
30
+ getDBPlans: () => getDBPlans,
31
+ getLimitStatus: () => getLimitStatus,
32
+ getPlanById: () => getPlanById,
33
+ getPlanByName: () => getPlanByName,
34
+ getPlanLimit: () => getPlanLimit,
35
+ getPlans: () => getPlans,
36
+ getSubscription: () => getSubscription,
37
+ getUsage: () => getUsage,
38
+ hasFeature: () => hasFeature,
39
+ recordUsage: () => recordUsage,
40
+ requireFeature: () => requireFeature,
41
+ requirePlan: () => requirePlan,
42
+ toPlanDTO: () => toPlanDTO,
43
+ toSubscriptionDTO: () => toSubscriptionDTO,
44
+ toUsageRecordDTO: () => toUsageRecordDTO,
45
+ updatePlan: () => updatePlan,
46
+ withBilling: () => withBilling
47
+ });
48
+ module.exports = __toCommonJS(index_exports);
49
+
50
+ // src/routes.ts
51
+ var import_middlewares = require("@fonderie/core/middlewares");
52
+
53
+ // src/controllers/plan.controller.ts
54
+ var import_core = require("@fonderie/core");
55
+
56
+ // src/services/plans.ts
57
+ function getPlans(config) {
58
+ return config.plans;
59
+ }
60
+ function getPlanByName(name, config) {
61
+ return config.plans.find((p) => p.name.toLowerCase() === name.toLowerCase()) ?? null;
62
+ }
63
+ async function syncPlansToDB(config, store) {
64
+ const plans = config.plans;
65
+ if (plans.length === 0) return;
66
+ const values = plans.map((_, i) => {
67
+ const b = i * 9;
68
+ return `($${b + 1}, $${b + 2}, $${b + 3}, $${b + 4}, $${b + 5}, $${b + 6}, $${b + 7}, $${b + 8}, $${b + 9}::jsonb)`;
69
+ });
70
+ const params = plans.flatMap((plan) => [
71
+ plan.name,
72
+ plan.trialDays ?? 0,
73
+ plan.monthly?.amount ?? null,
74
+ plan.monthly?.priceId ?? null,
75
+ plan.yearly?.amount ?? null,
76
+ plan.yearly?.priceId ?? null,
77
+ plan.description ?? null,
78
+ plan.tier ?? 0,
79
+ JSON.stringify(plan.metadata ?? {})
80
+ ]);
81
+ await store.query(
82
+ `INSERT INTO fonderie_plans
83
+ (name, trial_days,
84
+ monthly_amount, monthly_price_id,
85
+ yearly_amount, yearly_price_id,
86
+ description, tier, metadata)
87
+ VALUES ${values.join(", ")}
88
+ ON CONFLICT (name) DO UPDATE SET
89
+ trial_days = EXCLUDED.trial_days,
90
+ monthly_amount = EXCLUDED.monthly_amount,
91
+ monthly_price_id = EXCLUDED.monthly_price_id,
92
+ yearly_amount = EXCLUDED.yearly_amount,
93
+ yearly_price_id = EXCLUDED.yearly_price_id,
94
+ description = EXCLUDED.description,
95
+ tier = EXCLUDED.tier,
96
+ metadata = EXCLUDED.metadata`,
97
+ params
98
+ );
99
+ }
100
+ var SELECT_PLAN = `
101
+ SELECT
102
+ id,
103
+ name,
104
+ seats,
105
+ trial_days AS "trialDays",
106
+ monthly_amount AS "monthlyAmount",
107
+ monthly_price_id AS "monthlyPriceId",
108
+ yearly_amount AS "yearlyAmount",
109
+ yearly_price_id AS "yearlyPriceId",
110
+ description,
111
+ tier,
112
+ features,
113
+ metadata
114
+ FROM fonderie_plans`;
115
+ async function getDBPlans(store) {
116
+ return store.query(
117
+ `${SELECT_PLAN} WHERE active = true ORDER BY tier ASC, monthly_amount ASC NULLS LAST`
118
+ );
119
+ }
120
+ async function getPlanById(id, store) {
121
+ const [row] = await store.query(`${SELECT_PLAN} WHERE id = $1`, [id]);
122
+ return row ?? null;
123
+ }
124
+ async function createPlan(data, store) {
125
+ const [row] = await store.query(
126
+ `INSERT INTO fonderie_plans
127
+ (name, seats, trial_days, monthly_amount, monthly_price_id,
128
+ yearly_amount, yearly_price_id, description, tier, features, metadata)
129
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb)
130
+ RETURNING
131
+ id, name, seats,
132
+ trial_days AS "trialDays",
133
+ monthly_amount AS "monthlyAmount",
134
+ monthly_price_id AS "monthlyPriceId",
135
+ yearly_amount AS "yearlyAmount",
136
+ yearly_price_id AS "yearlyPriceId",
137
+ description, tier, features, metadata`,
138
+ [
139
+ data.name,
140
+ data.seats ?? null,
141
+ data.trialDays ?? 0,
142
+ data.monthlyAmount ?? null,
143
+ data.monthlyPriceId ?? null,
144
+ data.yearlyAmount ?? null,
145
+ data.yearlyPriceId ?? null,
146
+ data.description ?? null,
147
+ data.tier ?? 0,
148
+ JSON.stringify(data.features ?? []),
149
+ JSON.stringify(data.metadata ?? {})
150
+ ]
151
+ );
152
+ if (!row) throw new Error("Failed to create plan");
153
+ return row;
154
+ }
155
+ async function updatePlan(id, data, store) {
156
+ const fieldMap = {
157
+ name: "name",
158
+ seats: "seats",
159
+ trialDays: "trial_days",
160
+ monthlyAmount: "monthly_amount",
161
+ monthlyPriceId: "monthly_price_id",
162
+ yearlyAmount: "yearly_amount",
163
+ yearlyPriceId: "yearly_price_id",
164
+ description: "description",
165
+ tier: "tier"
166
+ };
167
+ const jsonbFields = /* @__PURE__ */ new Set(["features", "metadata"]);
168
+ const setClauses = [];
169
+ const params = [id];
170
+ for (const [key, col] of Object.entries(fieldMap)) {
171
+ if (key in data) {
172
+ params.push(data[key]);
173
+ setClauses.push(`${col} = $${params.length}`);
174
+ }
175
+ }
176
+ for (const key of jsonbFields) {
177
+ if (key in data) {
178
+ params.push(JSON.stringify(data[key]));
179
+ setClauses.push(`${key} = $${params.length}::jsonb`);
180
+ }
181
+ }
182
+ if (setClauses.length === 0) return getPlanById(id, store);
183
+ const [row] = await store.query(
184
+ `UPDATE fonderie_plans SET ${setClauses.join(", ")}
185
+ WHERE id = $1
186
+ RETURNING
187
+ id, name, seats,
188
+ trial_days AS "trialDays",
189
+ monthly_amount AS "monthlyAmount",
190
+ monthly_price_id AS "monthlyPriceId",
191
+ yearly_amount AS "yearlyAmount",
192
+ yearly_price_id AS "yearlyPriceId",
193
+ description, tier, features, metadata`,
194
+ params
195
+ );
196
+ return row ?? null;
197
+ }
198
+ async function deletePlan(id, store) {
199
+ const rows = await store.query(
200
+ `DELETE FROM fonderie_plans WHERE id = $1 RETURNING id`,
201
+ [id]
202
+ );
203
+ return rows.length > 0;
204
+ }
205
+
206
+ // src/models/plan.model.ts
207
+ var PlanModel = class {
208
+ constructor(store) {
209
+ this.store = store;
210
+ }
211
+ store;
212
+ listFromConfig(config) {
213
+ return getPlans(config);
214
+ }
215
+ findByNameInConfig(name, config) {
216
+ return getPlanByName(name, config);
217
+ }
218
+ list() {
219
+ return getDBPlans(this.store);
220
+ }
221
+ findById(id) {
222
+ return getPlanById(id, this.store);
223
+ }
224
+ create(data) {
225
+ return createPlan(data, this.store);
226
+ }
227
+ update(id, data) {
228
+ return updatePlan(id, data, this.store);
229
+ }
230
+ delete(id) {
231
+ return deletePlan(id, this.store);
232
+ }
233
+ };
234
+
235
+ // src/dtos/billing.ts
236
+ function toPlanDTO(plan) {
237
+ return {
238
+ id: plan.id,
239
+ planId: plan.name.toUpperCase(),
240
+ name: plan.name,
241
+ description: plan.description ?? "",
242
+ tier: plan.tier,
243
+ seats: plan.seats,
244
+ trialDays: plan.trialDays,
245
+ pricing: {
246
+ monthly: plan.monthlyAmount ?? 0,
247
+ yearly: plan.yearlyAmount ?? 0,
248
+ currency: "USD"
249
+ },
250
+ features: Array.isArray(plan.features) ? plan.features : [],
251
+ metadata: plan.metadata && typeof plan.metadata === "object" ? plan.metadata : {}
252
+ };
253
+ }
254
+ function toSubscriptionDTO(sub) {
255
+ return {
256
+ id: sub.id,
257
+ subscriberType: sub.subscriberType,
258
+ subscriberId: sub.subscriberId,
259
+ plan: sub.plan,
260
+ interval: sub.interval,
261
+ status: sub.status,
262
+ cancelAtPeriodEnd: sub.cancelAtPeriodEnd,
263
+ currentPeriodStart: sub.currentPeriodStart,
264
+ currentPeriodEnd: sub.currentPeriodEnd,
265
+ trialEndsAt: sub.trialEndsAt,
266
+ createdAt: sub.createdAt
267
+ };
268
+ }
269
+ function toUsageRecordDTO(record) {
270
+ return {
271
+ id: record.id,
272
+ subscriberType: record.subscriberType,
273
+ subscriberId: record.subscriberId,
274
+ metric: record.metric,
275
+ quantity: record.quantity,
276
+ recordedAt: record.recordedAt
277
+ };
278
+ }
279
+
280
+ // src/controllers/plan.controller.ts
281
+ function planController(store) {
282
+ const plans = new PlanModel(store);
283
+ return {
284
+ async list(_ctx) {
285
+ const list = await plans.list();
286
+ const dtos = list.map(toPlanDTO);
287
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "PLAN_LIST", `Retrieved ${list.length} workspace plans`, {
288
+ plans: dtos
289
+ });
290
+ },
291
+ async get(ctx) {
292
+ const params = ctx.meta["params"];
293
+ const id = params?.["planId"];
294
+ if (!id) return (0, import_core.setApiResponse)(import_core.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Plan ID required");
295
+ const plan = await plans.findById(id);
296
+ if (!plan) return (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "Plan not found");
297
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "PLAN_FETCHED", "Plan retrieved successfully.", {
298
+ plan: toPlanDTO(plan)
299
+ });
300
+ },
301
+ async create(ctx) {
302
+ const body = ctx.meta["body"];
303
+ const name = (0, import_core.stringOrEmpty)(body?.["name"]);
304
+ if (!name) {
305
+ return (0, import_core.setApiResponse)(import_core.HTTP.UNPROCESSABLE, "VALIDATION_ERROR", "name is required");
306
+ }
307
+ const plan = await plans.create({
308
+ name,
309
+ description: body?.["description"] != null ? String(body["description"]) : null,
310
+ tier: body?.["tier"] != null ? (0, import_core.numberOrZero)(body["tier"]) : 0,
311
+ seats: body?.["seats"] != null ? (0, import_core.numberOrZero)(body["seats"]) : null,
312
+ trialDays: body?.["trialDays"] != null ? (0, import_core.numberOrZero)(body["trialDays"]) : 0,
313
+ monthlyAmount: body?.["monthlyAmount"] != null ? (0, import_core.numberOrZero)(body["monthlyAmount"]) : null,
314
+ monthlyPriceId: body?.["monthlyPriceId"] != null ? String(body["monthlyPriceId"]) : null,
315
+ yearlyAmount: body?.["yearlyAmount"] != null ? (0, import_core.numberOrZero)(body["yearlyAmount"]) : null,
316
+ yearlyPriceId: body?.["yearlyPriceId"] != null ? String(body["yearlyPriceId"]) : null,
317
+ features: body?.["features"],
318
+ metadata: body?.["metadata"]
319
+ });
320
+ return (0, import_core.setApiResponse)(import_core.HTTP.CREATED, "PLAN_CREATED", "Plan created successfully.", {
321
+ plan: toPlanDTO(plan)
322
+ });
323
+ },
324
+ async update(ctx) {
325
+ const params = ctx.meta["params"];
326
+ const id = params?.["planId"];
327
+ if (!id) {
328
+ return (0, import_core.setApiResponse)(import_core.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Plan ID required");
329
+ }
330
+ const body = ctx.meta["body"];
331
+ if (!body || Object.keys(body).length === 0) {
332
+ return (0, import_core.setApiResponse)(import_core.HTTP.UNPROCESSABLE, "VALIDATION_ERROR", "Request body is empty");
333
+ }
334
+ const patch = {};
335
+ const allowed = [
336
+ "name",
337
+ "description",
338
+ "tier",
339
+ "seats",
340
+ "trialDays",
341
+ "monthlyAmount",
342
+ "monthlyPriceId",
343
+ "yearlyAmount",
344
+ "yearlyPriceId",
345
+ "features",
346
+ "metadata"
347
+ ];
348
+ for (const key of allowed) {
349
+ if (key in body) patch[key] = body[key];
350
+ }
351
+ const plan = await plans.update(id, patch);
352
+ if (!plan) {
353
+ return (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "Plan not found");
354
+ }
355
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "PLAN_UPDATED", "Plan updated successfully.", {
356
+ plan: toPlanDTO(plan)
357
+ });
358
+ },
359
+ async delete(ctx) {
360
+ const params = ctx.meta["params"];
361
+ const id = params?.["planId"];
362
+ if (!id) {
363
+ return (0, import_core.setApiResponse)(import_core.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Plan ID required");
364
+ }
365
+ const deleted = await plans.delete(id);
366
+ if (!deleted) {
367
+ return (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "Plan not found");
368
+ }
369
+ return (0, import_core.setApiResponse)(import_core.HTTP.OK, "PLAN_DELETED", "Plan deleted successfully.");
370
+ }
371
+ };
372
+ }
373
+
374
+ // src/controllers/subscription.controller.ts
375
+ var import_core2 = require("@fonderie/core");
376
+
377
+ // src/services/subscriptions.ts
378
+ var SELECT_SUBSCRIPTION = `
379
+ SELECT
380
+ id,
381
+ subscriber_type AS "subscriberType",
382
+ subscriber_id AS "subscriberId",
383
+ plan,
384
+ interval,
385
+ status,
386
+ provider_customer_id AS "providerCustomerId",
387
+ provider_subscription_id AS "providerSubscriptionId",
388
+ current_period_start AS "currentPeriodStart",
389
+ current_period_end AS "currentPeriodEnd",
390
+ cancel_at_period_end AS "cancelAtPeriodEnd",
391
+ trial_ends_at AS "trialEndsAt",
392
+ created_at AS "createdAt"
393
+ FROM fonderie_subscriptions`;
394
+ async function getSubscription(subscriberType, subscriberId, store) {
395
+ const [row] = await store.query(
396
+ `${SELECT_SUBSCRIPTION} WHERE subscriber_type = $1 AND subscriber_id = $2`,
397
+ [subscriberType, subscriberId]
398
+ );
399
+ return row ?? null;
400
+ }
401
+ async function upsertSubscription(data, store) {
402
+ await store.query(
403
+ `INSERT INTO fonderie_subscriptions
404
+ (subscriber_type, subscriber_id, plan, interval, status,
405
+ provider_customer_id, provider_subscription_id,
406
+ current_period_start, current_period_end,
407
+ cancel_at_period_end, trial_ends_at)
408
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
409
+ ON CONFLICT (subscriber_type, subscriber_id) DO UPDATE SET
410
+ plan = $3,
411
+ interval = $4,
412
+ status = $5,
413
+ provider_customer_id = COALESCE($6, fonderie_subscriptions.provider_customer_id),
414
+ provider_subscription_id = COALESCE($7, fonderie_subscriptions.provider_subscription_id),
415
+ current_period_start = $8,
416
+ current_period_end = $9,
417
+ cancel_at_period_end = $10,
418
+ trial_ends_at = $11`,
419
+ [
420
+ data.subscriberType,
421
+ data.subscriberId,
422
+ data.plan,
423
+ data.interval ?? "month",
424
+ data.status,
425
+ data.providerCustomerId ?? null,
426
+ data.providerSubscriptionId ?? null,
427
+ data.currentPeriodStart ?? null,
428
+ data.currentPeriodEnd ?? null,
429
+ data.cancelAtPeriodEnd ?? false,
430
+ data.trialEndsAt ?? null
431
+ ]
432
+ );
433
+ }
434
+
435
+ // src/models/subscription.model.ts
436
+ var SubscriptionModel = class {
437
+ constructor(store) {
438
+ this.store = store;
439
+ }
440
+ store;
441
+ get(subscriberType, subscriberId) {
442
+ return getSubscription(subscriberType, subscriberId, this.store);
443
+ }
444
+ upsert(data) {
445
+ return upsertSubscription(data, this.store);
446
+ }
447
+ };
448
+
449
+ // src/utils.ts
450
+ function parseWindowMs(window) {
451
+ const n = parseInt(window, 10);
452
+ const unit = window.slice(String(n).length);
453
+ switch (unit) {
454
+ case "h":
455
+ return n * 36e5;
456
+ case "d":
457
+ return n * 864e5;
458
+ case "m":
459
+ return n * 6e4;
460
+ default:
461
+ throw new Error(`Unknown window unit: '${unit}' in '${window}'`);
462
+ }
463
+ }
464
+ function resolveSubscriber(ctx) {
465
+ const wsFromHeader = ctx.request.headers.get("x-workspace-id");
466
+ if (wsFromHeader) {
467
+ return {
468
+ type: "workspace",
469
+ id: wsFromHeader
470
+ };
471
+ }
472
+ if (ctx.workspace?.id) {
473
+ return {
474
+ type: "workspace",
475
+ id: ctx.workspace.id
476
+ };
477
+ }
478
+ if (ctx.user?.id) {
479
+ return {
480
+ type: "user",
481
+ id: ctx.user.id
482
+ };
483
+ }
484
+ return null;
485
+ }
486
+
487
+ // src/controllers/subscription.controller.ts
488
+ function subscriptionController(store) {
489
+ const subscriptions = new SubscriptionModel(store);
490
+ return {
491
+ async get(ctx) {
492
+ const subscriber = resolveSubscriber(ctx);
493
+ if (!subscriber) {
494
+ return (0, import_core2.setApiResponse)(
495
+ import_core2.HTTP.BAD_REQUEST,
496
+ "SUBSCRIBER_REQUIRED",
497
+ "Subscriber context required"
498
+ );
499
+ }
500
+ const subscription = await subscriptions.get(subscriber.type, subscriber.id);
501
+ if (!subscription)
502
+ return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "No active subscription");
503
+ return (0, import_core2.setApiResponse)(
504
+ import_core2.HTTP.OK,
505
+ "SUBSCRIPTION_FETCHED",
506
+ "Subscription retrieved successfully.",
507
+ {
508
+ subscription: toSubscriptionDTO(subscription)
509
+ }
510
+ );
511
+ }
512
+ };
513
+ }
514
+
515
+ // src/controllers/checkout.controller.ts
516
+ var import_core3 = require("@fonderie/core");
517
+ function checkoutController(store, config) {
518
+ const plans = new PlanModel(store);
519
+ const subscriptions = new SubscriptionModel(store);
520
+ return {
521
+ async createSession(ctx) {
522
+ const body = ctx.meta["body"];
523
+ const planName = body?.["plan"];
524
+ const interval = body?.["interval"] ?? "month";
525
+ const subscriber = resolveSubscriber(ctx);
526
+ if (typeof planName !== "string") {
527
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "plan is required");
528
+ }
529
+ if (interval !== "month" && interval !== "year") {
530
+ return (0, import_core3.setApiResponse)(
531
+ import_core3.HTTP.UNPROCESSABLE,
532
+ "INVALID_PARAMETER",
533
+ "interval must be month or year"
534
+ );
535
+ }
536
+ if (!subscriber) {
537
+ return (0, import_core3.setApiResponse)(
538
+ import_core3.HTTP.BAD_REQUEST,
539
+ "SUBSCRIBER_REQUIRED",
540
+ "Subscriber context required"
541
+ );
542
+ }
543
+ const plan = plans.findByNameInConfig(planName, config);
544
+ if (!plan) {
545
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", `Unknown plan: ${planName}`);
546
+ }
547
+ const pricing = interval === "year" ? plan.yearly : plan.monthly;
548
+ if (!pricing?.priceId) {
549
+ return (0, import_core3.setApiResponse)(
550
+ import_core3.HTTP.UNPROCESSABLE,
551
+ "INVALID_PARAMETER",
552
+ `Plan ${planName} does not support ${interval} billing`
553
+ );
554
+ }
555
+ const { customerId } = await config.provider.createCustomer({
556
+ email: ctx.user.email ?? "",
557
+ subscriberType: subscriber.type,
558
+ subscriberId: subscriber.id,
559
+ userId: ctx.user.id
560
+ });
561
+ const sessionOpts = {
562
+ customerId,
563
+ priceId: pricing.priceId,
564
+ subscriberType: subscriber.type,
565
+ subscriberId: subscriber.id,
566
+ successUrl: config.successUrl,
567
+ cancelUrl: config.cancelUrl
568
+ };
569
+ if (plan.trialDays !== void 0) sessionOpts.trialDays = plan.trialDays;
570
+ const { url } = await config.provider.createCheckoutSession(sessionOpts);
571
+ await subscriptions.upsert({
572
+ subscriberType: subscriber.type,
573
+ subscriberId: subscriber.id,
574
+ plan: planName,
575
+ interval,
576
+ status: "incomplete",
577
+ providerCustomerId: customerId
578
+ });
579
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "CHECKOUT_URL", "Checkout session created.", { url });
580
+ },
581
+ async createPortal(ctx) {
582
+ const subscriber = resolveSubscriber(ctx);
583
+ if (!subscriber) {
584
+ return (0, import_core3.setApiResponse)(
585
+ import_core3.HTTP.BAD_REQUEST,
586
+ "SUBSCRIBER_REQUIRED",
587
+ "Subscriber context required"
588
+ );
589
+ }
590
+ const subscription = await subscriptions.get(subscriber.type, subscriber.id);
591
+ if (!subscription?.providerCustomerId) {
592
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "No active subscription");
593
+ }
594
+ const { url } = await config.provider.createPortalSession({
595
+ customerId: subscription.providerCustomerId,
596
+ returnUrl: config.successUrl
597
+ });
598
+ return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "PORTAL_URL", "Portal session created.", { url });
599
+ }
600
+ };
601
+ }
602
+
603
+ // src/controllers/usage.controller.ts
604
+ var import_core4 = require("@fonderie/core");
605
+
606
+ // src/services/usage.ts
607
+ async function recordUsage(opts, store) {
608
+ await store.query(
609
+ `INSERT INTO fonderie_usage_records (subscriber_type, subscriber_id, metric, quantity)
610
+ VALUES ($1, $2, $3, $4)`,
611
+ [opts.subscriberType, opts.subscriberId, opts.metric, opts.quantity]
612
+ );
613
+ }
614
+ async function getUsage(subscriberType, subscriberId, metric, since, store) {
615
+ const rows = await store.query(
616
+ `SELECT COALESCE(SUM(quantity), 0) AS total
617
+ FROM fonderie_usage_records
618
+ WHERE subscriber_type = $1
619
+ AND subscriber_id = $2
620
+ AND metric = $3
621
+ AND recorded_at >= $4`,
622
+ [subscriberType, subscriberId, metric, since]
623
+ );
624
+ return parseInt(rows[0]?.total ?? "0", 10);
625
+ }
626
+
627
+ // src/models/usage.model.ts
628
+ var UsageModel = class {
629
+ constructor(store) {
630
+ this.store = store;
631
+ }
632
+ store;
633
+ record(opts) {
634
+ return recordUsage(opts, this.store);
635
+ }
636
+ get(subscriberType, subscriberId, metric, since) {
637
+ return getUsage(subscriberType, subscriberId, metric, since, this.store);
638
+ }
639
+ };
640
+
641
+ // src/controllers/usage.controller.ts
642
+ function usageController(store) {
643
+ const usage = new UsageModel(store);
644
+ return {
645
+ async record(ctx) {
646
+ const body = ctx.meta["body"];
647
+ const metric = body?.["metric"];
648
+ const quantity = body?.["quantity"];
649
+ const subscriber = resolveSubscriber(ctx);
650
+ if (!subscriber) {
651
+ return (0, import_core4.setApiResponse)(
652
+ import_core4.HTTP.BAD_REQUEST,
653
+ "SUBSCRIBER_REQUIRED",
654
+ "Subscriber context required"
655
+ );
656
+ }
657
+ if (typeof metric !== "string") {
658
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "metric is required");
659
+ }
660
+ await usage.record({
661
+ subscriberType: subscriber.type,
662
+ subscriberId: subscriber.id,
663
+ metric,
664
+ quantity: typeof quantity === "number" ? quantity : 1
665
+ });
666
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "USAGE_RECORDED", "Usage recorded successfully.");
667
+ },
668
+ async get(ctx) {
669
+ const params = ctx.meta["params"];
670
+ const metric = params?.["metric"];
671
+ const subscriber = resolveSubscriber(ctx);
672
+ if (!subscriber || !metric) {
673
+ return (0, import_core4.setApiResponse)(
674
+ import_core4.HTTP.UNPROCESSABLE,
675
+ "INVALID_PARAMETER",
676
+ "subscriber and metric are required"
677
+ );
678
+ }
679
+ const since = /* @__PURE__ */ new Date();
680
+ since.setDate(1);
681
+ since.setHours(0, 0, 0, 0);
682
+ const total = await usage.get(subscriber.type, subscriber.id, metric, since);
683
+ return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "USAGE_FETCHED", "Usage retrieved successfully.", {
684
+ metric,
685
+ total,
686
+ since
687
+ });
688
+ }
689
+ };
690
+ }
691
+
692
+ // src/controllers/webhook.controller.ts
693
+ var import_core5 = require("@fonderie/core");
694
+ function webhookController(store, config) {
695
+ const subscriptions = new SubscriptionModel(store);
696
+ return {
697
+ async handle(ctx) {
698
+ if (!config.webhookSecret) {
699
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.SERVER_ERROR, "SERVER_ERROR", "Webhook secret not configured");
700
+ }
701
+ const signature = ctx.request.headers.get("stripe-signature") ?? ctx.request.headers.get("paypal-auth-algo") ?? "";
702
+ if (!signature) {
703
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "INVALID_REQUEST", "Missing webhook signature");
704
+ }
705
+ const payload = await ctx.request.text();
706
+ let event;
707
+ try {
708
+ event = await config.provider.constructEvent({
709
+ payload,
710
+ signature,
711
+ secret: config.webhookSecret
712
+ });
713
+ } catch {
714
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "INVALID_REQUEST", "Invalid webhook signature");
715
+ }
716
+ if (event.subscription) {
717
+ await subscriptions.upsert({
718
+ subscriberType: event.subscription.subscriberType,
719
+ subscriberId: event.subscription.subscriberId,
720
+ plan: event.subscription.plan,
721
+ status: event.subscription.status,
722
+ providerCustomerId: event.subscription.providerCustomerId,
723
+ providerSubscriptionId: event.subscription.providerSubscriptionId,
724
+ currentPeriodStart: event.subscription.currentPeriodStart,
725
+ currentPeriodEnd: event.subscription.currentPeriodEnd,
726
+ cancelAtPeriodEnd: event.subscription.cancelAtPeriodEnd,
727
+ trialEndsAt: event.subscription.trialEndsAt
728
+ });
729
+ }
730
+ return Response.json({ received: true });
731
+ }
732
+ };
733
+ }
734
+
735
+ // src/routes.ts
736
+ function buildBillingRoutes(store, config) {
737
+ const plan = planController(store);
738
+ const subscription = subscriptionController(store);
739
+ const checkout = checkoutController(store, config);
740
+ const usage = usageController(store);
741
+ const webhook = webhookController(store, config);
742
+ return [
743
+ // Plans — public read-only
744
+ ["GET", "/plans", plan.list],
745
+ ["GET", "/plans/:planId", plan.get],
746
+ // Plans — admin write (caller is responsible for authorization)
747
+ ["POST", "/plans", plan.create],
748
+ ["PUT", "/plans/:planId", plan.update],
749
+ ["DELETE", "/plans/:planId", plan.delete],
750
+ // Billing — subscriber resolved from X-Workspace-ID header (workspace) or session (user)
751
+ // Workspace membership is verified automatically by the withBilling global middleware
752
+ ["GET", "/billing/subscription", import_middlewares.requireAuth, subscription.get],
753
+ ["POST", "/billing/checkout", import_middlewares.requireAuth, checkout.createSession],
754
+ ["POST", "/billing/portal", import_middlewares.requireAuth, checkout.createPortal],
755
+ ["POST", "/billing/usage", import_middlewares.requireAuth, usage.record],
756
+ ["GET", "/billing/usage/:metric", import_middlewares.requireAuth, usage.get],
757
+ // Webhook — signature verified inside the handler
758
+ ["POST", "/billing/webhook", webhook.handle]
759
+ ];
760
+ }
761
+
762
+ // src/middlewares/billing.ts
763
+ var import_core6 = require("@fonderie/core");
764
+
765
+ // src/config.ts
766
+ var MESSAGE_KEYS = {
767
+ limitWarning: "billing.limit-warning",
768
+ limitReached: "billing.limit-reached",
769
+ limitBlocked: "billing.limit-blocked"
770
+ };
771
+
772
+ // src/services/policy.ts
773
+ function buildBillingContext(opts) {
774
+ const { subscriber, plan, active, counters } = opts;
775
+ const defaults = plan.defaults ?? {};
776
+ const statuses = {};
777
+ for (const [key, entry] of Object.entries(plan.policy ?? {})) {
778
+ if ("enabled" in entry) {
779
+ statuses[key] = { type: "feature", enabled: entry.enabled };
780
+ continue;
781
+ }
782
+ const { limit, buffer = defaults.buffer ?? 0, warnAt = defaults.warnAt ?? 0.8, window } = entry;
783
+ const used = counters[key] ?? 0;
784
+ const hardLimit = limit !== null ? limit + buffer : null;
785
+ let status = "ok";
786
+ if (hardLimit !== null && used >= hardLimit) status = "blocked";
787
+ else if (limit !== null && used >= limit) status = "over_limit";
788
+ else if (limit !== null && used >= limit * warnAt) status = "warning";
789
+ let resetsAt = null;
790
+ if (window) {
791
+ const windowMs = parseWindowMs(window);
792
+ const windowStart = Math.floor(Date.now() / windowMs) * windowMs;
793
+ resetsAt = new Date(windowStart + windowMs).toISOString();
794
+ }
795
+ statuses[key] = { type: "counter", limit, used, status, resetsAt };
796
+ }
797
+ return { subscriber, plan: plan.name, active, statuses };
798
+ }
799
+
800
+ // src/middlewares/billing.ts
801
+ var notified = /* @__PURE__ */ new Set();
802
+ function withBilling(store, config, backend) {
803
+ return async (ctx, next) => {
804
+ const subscriber = resolveSubscriber(ctx);
805
+ if (!subscriber) return next();
806
+ const subscription = await getSubscription(subscriber.type, subscriber.id, store);
807
+ const planName = subscription?.plan ?? config.plans[0]?.name ?? "free";
808
+ const active = !subscription || subscription.status === "active" || subscription.status === "trialing";
809
+ const plan = config.plans.find((p) => p.name === planName) ?? config.plans[0];
810
+ if (!plan) return next();
811
+ const counters = {};
812
+ for (const [key, entry] of Object.entries(plan.policy ?? {})) {
813
+ if ("enabled" in entry || !entry.window) continue;
814
+ const windowMs = parseWindowMs(entry.window);
815
+ const counterKey = `${subscriber.type}:${subscriber.id}:${key}`;
816
+ counters[key] = await backend.increment(counterKey, windowMs);
817
+ }
818
+ const billingCtx = buildBillingContext({ subscriber, plan, active, counters });
819
+ ctx.meta["billing"] = billingCtx;
820
+ for (const [key, status] of Object.entries(billingCtx.statuses)) {
821
+ if (status.type === "counter" && status.status === "blocked") {
822
+ return (0, import_core6.setApiResponse)(
823
+ import_core6.HTTP.TOO_MANY_REQUESTS,
824
+ "RATE_LIMIT_EXCEEDED",
825
+ `Limit exceeded for: ${key}`,
826
+ { key, limit: status.limit, used: status.used, resetsAt: status.resetsAt }
827
+ );
828
+ }
829
+ }
830
+ if (config.notifications) {
831
+ const toNotify = [];
832
+ const recipient = {
833
+ email: ctx.user?.email ?? null,
834
+ phone: null,
835
+ deviceToken: null
836
+ };
837
+ for (const [key, status] of Object.entries(billingCtx.statuses)) {
838
+ if (status.type !== "counter" || status.limit === null) continue;
839
+ const base = `${subscriber.type}:${subscriber.id}:${key}`;
840
+ if (config.notifications.softHit && status.status === "over_limit") {
841
+ const nk = `${base}:reached`;
842
+ if (!notified.has(nk)) {
843
+ notified.add(nk);
844
+ toNotify.push({
845
+ type: MESSAGE_KEYS.limitReached,
846
+ recipient,
847
+ data: {
848
+ key,
849
+ plan: plan.name,
850
+ limit: status.limit,
851
+ used: status.used
852
+ }
853
+ });
854
+ }
855
+ } else if (config.notifications.warnAt && status.status === "warning") {
856
+ const nk = `${base}:warning`;
857
+ if (!notified.has(nk)) {
858
+ notified.add(nk);
859
+ toNotify.push({
860
+ type: MESSAGE_KEYS.limitWarning,
861
+ recipient,
862
+ data: {
863
+ key,
864
+ plan: plan.name,
865
+ limit: status.limit,
866
+ used: status.used
867
+ }
868
+ });
869
+ }
870
+ }
871
+ }
872
+ if (toNotify.length > 0) {
873
+ const existing = ctx.meta["messages"];
874
+ ctx.meta["messages"] = [...existing ?? [], ...toNotify];
875
+ }
876
+ }
877
+ return next();
878
+ };
879
+ }
880
+
881
+ // src/backends/memory.ts
882
+ var MemoryCounterBackend = class {
883
+ counters = /* @__PURE__ */ new Map();
884
+ async increment(key, windowMs, quantity = 1) {
885
+ const now = Date.now();
886
+ const existing = this.counters.get(key);
887
+ if (!existing || windowMs !== null && now - existing.windowStart >= windowMs) {
888
+ this.counters.set(key, { count: quantity, windowStart: now });
889
+ return quantity;
890
+ }
891
+ existing.count += quantity;
892
+ return existing.count;
893
+ }
894
+ async get(key, windowMs) {
895
+ const now = Date.now();
896
+ const existing = this.counters.get(key);
897
+ if (!existing) return 0;
898
+ if (windowMs !== null && now - existing.windowStart >= windowMs) return 0;
899
+ return existing.count;
900
+ }
901
+ };
902
+
903
+ // src/backends/db.ts
904
+ var DBCounterBackend = class {
905
+ constructor(store) {
906
+ this.store = store;
907
+ }
908
+ store;
909
+ async increment(key, windowMs, quantity = 1) {
910
+ const [subscriberType, subscriberId, ...rest] = key.split(":");
911
+ const metric = rest.join(":");
912
+ await this.store.query(
913
+ `INSERT INTO fonderie_usage_records (subscriber_type, subscriber_id, metric, quantity)
914
+ VALUES ($1, $2, $3, $4)`,
915
+ [subscriberType, subscriberId, metric, quantity]
916
+ );
917
+ return this.get(key, windowMs);
918
+ }
919
+ async get(key, windowMs) {
920
+ const [subscriberType, subscriberId, ...rest] = key.split(":");
921
+ const metric = rest.join(":");
922
+ const since = windowMs !== null ? new Date(Date.now() - windowMs) : /* @__PURE__ */ new Date(0);
923
+ const rows = await this.store.query(
924
+ `SELECT COALESCE(SUM(quantity), 0) AS total
925
+ FROM fonderie_usage_records
926
+ WHERE subscriber_type = $1
927
+ AND subscriber_id = $2
928
+ AND metric = $3
929
+ AND recorded_at >= $4`,
930
+ [subscriberType, subscriberId, metric, since]
931
+ );
932
+ return parseInt(rows[0]?.total ?? "0", 10);
933
+ }
934
+ };
935
+
936
+ // src/backends/index.ts
937
+ function createBackend(config, store) {
938
+ if (!config || config === "memory") return new MemoryCounterBackend();
939
+ if (config === "db") return new DBCounterBackend(store);
940
+ return config;
941
+ }
942
+
943
+ // src/module.ts
944
+ var BillingModule = class {
945
+ constructor(store, config) {
946
+ this.store = store;
947
+ this.config = config;
948
+ }
949
+ store;
950
+ config;
951
+ name = "@fonderie/billing";
952
+ deps = ["@fonderie/auth"];
953
+ async install(app) {
954
+ await syncPlansToDB(this.config, this.store);
955
+ const backend = createBackend(this.config.rateLimit?.backend, this.store);
956
+ app.use(withBilling(this.store, this.config, backend));
957
+ const routes = buildBillingRoutes(this.store, this.config);
958
+ for (const [method, path, ...handlers] of routes) {
959
+ app.addRoute(method, path, ...handlers);
960
+ }
961
+ }
962
+ };
963
+
964
+ // src/providers/stripe.ts
965
+ var _client = null;
966
+ async function getClient(secretKey) {
967
+ if (_client) return _client;
968
+ const pkg = "stripe";
969
+ const mod = await import(pkg).catch(() => {
970
+ throw new Error("[billing:stripe] stripe is required: npm install stripe");
971
+ });
972
+ const Stripe = mod.default ?? mod;
973
+ _client = new Stripe(secretKey, { apiVersion: "2024-11-20.acacia" });
974
+ return _client;
975
+ }
976
+ function normalizeSubscription(sub) {
977
+ return {
978
+ subscriberType: sub.metadata?.["subscriberType"] ?? "workspace",
979
+ subscriberId: sub.metadata?.["subscriberId"] ?? "",
980
+ plan: sub.items.data[0]?.price.nickname ?? "unknown",
981
+ status: sub.status,
982
+ providerCustomerId: sub.customer,
983
+ providerSubscriptionId: sub.id,
984
+ currentPeriodStart: new Date(sub.current_period_start * 1e3),
985
+ currentPeriodEnd: new Date(sub.current_period_end * 1e3),
986
+ cancelAtPeriodEnd: sub.cancel_at_period_end,
987
+ trialEndsAt: sub.trial_end ? new Date(sub.trial_end * 1e3) : null
988
+ };
989
+ }
990
+ var StripeProvider = class {
991
+ constructor(secretKey, webhookSecret) {
992
+ this.secretKey = secretKey;
993
+ this.webhookSecret = webhookSecret;
994
+ }
995
+ secretKey;
996
+ webhookSecret;
997
+ name = "stripe";
998
+ async client() {
999
+ return getClient(this.secretKey);
1000
+ }
1001
+ async createCustomer(opts) {
1002
+ const stripe = await this.client();
1003
+ const customer = await stripe.customers.create({
1004
+ email: opts.email,
1005
+ metadata: {
1006
+ subscriberType: opts.subscriberType,
1007
+ subscriberId: opts.subscriberId,
1008
+ userId: opts.userId
1009
+ }
1010
+ });
1011
+ return { customerId: customer.id };
1012
+ }
1013
+ async createCheckoutSession(opts) {
1014
+ const stripe = await this.client();
1015
+ const session = await stripe.checkout.sessions.create({
1016
+ customer: opts.customerId,
1017
+ mode: "subscription",
1018
+ line_items: [{ price: opts.priceId, quantity: 1 }],
1019
+ success_url: opts.successUrl,
1020
+ cancel_url: opts.cancelUrl,
1021
+ subscription_data: {
1022
+ metadata: {
1023
+ subscriberType: opts.subscriberType,
1024
+ subscriberId: opts.subscriberId
1025
+ },
1026
+ ...opts.trialDays && opts.trialDays > 0 ? { trial_period_days: opts.trialDays } : {}
1027
+ }
1028
+ });
1029
+ return { url: session.url ?? "" };
1030
+ }
1031
+ async createPortalSession(opts) {
1032
+ const stripe = await this.client();
1033
+ const session = await stripe.billingPortal.sessions.create({
1034
+ customer: opts.customerId,
1035
+ return_url: opts.returnUrl
1036
+ });
1037
+ return { url: session.url };
1038
+ }
1039
+ async constructEvent(opts) {
1040
+ const stripe = await this.client();
1041
+ let raw;
1042
+ try {
1043
+ raw = stripe.webhooks.constructEvent(opts.payload, opts.signature, opts.secret);
1044
+ } catch {
1045
+ throw new Error("[billing:stripe] Invalid webhook signature");
1046
+ }
1047
+ const isSubscriptionEvent = [
1048
+ "customer.subscription.created",
1049
+ "customer.subscription.updated",
1050
+ "customer.subscription.deleted"
1051
+ ].includes(raw.type);
1052
+ if (!isSubscriptionEvent) {
1053
+ return { type: raw.type, subscription: null };
1054
+ }
1055
+ const sub = raw.data.object;
1056
+ if (raw.type === "customer.subscription.deleted") {
1057
+ return {
1058
+ type: raw.type,
1059
+ subscription: { ...normalizeSubscription(sub), plan: "free", status: "canceled" }
1060
+ };
1061
+ }
1062
+ return { type: raw.type, subscription: normalizeSubscription(sub) };
1063
+ }
1064
+ };
1065
+
1066
+ // src/middlewares/require-plan.ts
1067
+ var import_core7 = require("@fonderie/core");
1068
+ function makeHandler(plans, store) {
1069
+ const allowed = Array.isArray(plans) ? plans : [plans];
1070
+ return async (ctx, next) => {
1071
+ if (!ctx.user) {
1072
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
1073
+ }
1074
+ const subscriber = resolveSubscriber(ctx);
1075
+ if (!subscriber) {
1076
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.BAD_REQUEST, "SUBSCRIBER_REQUIRED", "Subscriber context required");
1077
+ }
1078
+ const subscription = await getSubscription(subscriber.type, subscriber.id, store);
1079
+ if (!subscription || !allowed.includes(subscription.plan)) {
1080
+ return (0, import_core7.setApiResponse)(
1081
+ import_core7.HTTP.PAYMENT_REQUIRED,
1082
+ "PLAN_UPGRADE_REQUIRED",
1083
+ "Plan upgrade required",
1084
+ { required: allowed, current: subscription?.plan ?? "none" }
1085
+ );
1086
+ }
1087
+ if (subscription.status !== "active" && subscription.status !== "trialing") {
1088
+ return (0, import_core7.setApiResponse)(
1089
+ import_core7.HTTP.PAYMENT_REQUIRED,
1090
+ "SUBSCRIPTION_INACTIVE",
1091
+ "Subscription is not active",
1092
+ { status: subscription.status }
1093
+ );
1094
+ }
1095
+ return next();
1096
+ };
1097
+ }
1098
+ function requirePlan(plans, store, ctx, next) {
1099
+ const handler = makeHandler(plans, store);
1100
+ if (ctx !== void 0 && next !== void 0) return handler(ctx, next);
1101
+ return handler;
1102
+ }
1103
+
1104
+ // src/helpers.ts
1105
+ var import_core8 = require("@fonderie/core");
1106
+ function getBillingContext(ctx) {
1107
+ return ctx.meta["billing"] ?? null;
1108
+ }
1109
+ function hasFeature(ctx, key) {
1110
+ const billing = getBillingContext(ctx);
1111
+ if (!billing) return true;
1112
+ const status = billing.statuses[key];
1113
+ if (!status) return true;
1114
+ if (status.type === "feature") return status.enabled;
1115
+ return true;
1116
+ }
1117
+ function getPlanLimit(ctx, key) {
1118
+ const billing = getBillingContext(ctx);
1119
+ if (!billing) return null;
1120
+ const status = billing.statuses[key];
1121
+ if (!status || status.type === "feature") return null;
1122
+ return status.limit;
1123
+ }
1124
+ function getLimitStatus(ctx, key) {
1125
+ const billing = getBillingContext(ctx);
1126
+ if (!billing) return null;
1127
+ return billing.statuses[key] ?? null;
1128
+ }
1129
+ function requireFeature(key) {
1130
+ return (ctx, next) => {
1131
+ if (!hasFeature(ctx, key)) {
1132
+ return Promise.resolve(
1133
+ (0, import_core8.setApiResponse)(
1134
+ import_core8.HTTP.PAYMENT_REQUIRED,
1135
+ "FEATURE_UNAVAILABLE",
1136
+ `Feature '${key}' is not available on your current plan`
1137
+ )
1138
+ );
1139
+ }
1140
+ return next();
1141
+ };
1142
+ }
1143
+ // Annotate the CommonJS export names for ESM import in node:
1144
+ 0 && (module.exports = {
1145
+ BillingModule,
1146
+ DBCounterBackend,
1147
+ MESSAGE_KEYS,
1148
+ MemoryCounterBackend,
1149
+ StripeProvider,
1150
+ createPlan,
1151
+ deletePlan,
1152
+ getDBPlans,
1153
+ getLimitStatus,
1154
+ getPlanById,
1155
+ getPlanByName,
1156
+ getPlanLimit,
1157
+ getPlans,
1158
+ getSubscription,
1159
+ getUsage,
1160
+ hasFeature,
1161
+ recordUsage,
1162
+ requireFeature,
1163
+ requirePlan,
1164
+ toPlanDTO,
1165
+ toSubscriptionDTO,
1166
+ toUsageRecordDTO,
1167
+ updatePlan,
1168
+ withBilling
1169
+ });
1170
+ //# sourceMappingURL=index.cjs.map