@auth-gate/billing 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,482 @@
1
+ import {
2
+ __spreadProps,
3
+ __spreadValues
4
+ } from "./chunk-I4E63NIC.mjs";
5
+
6
+ // src/validation.ts
7
+ function validateTiers(price, prefix) {
8
+ if (!Array.isArray(price.tiers) || price.tiers.length === 0) {
9
+ throw new Error(`${prefix}: tiered/metered price must have a non-empty "tiers" array.`);
10
+ }
11
+ if (!["graduated", "volume"].includes(price.tierMode)) {
12
+ throw new Error(`${prefix}.tierMode must be "graduated" or "volume".`);
13
+ }
14
+ for (let j = 0; j < price.tiers.length; j++) {
15
+ const tier = price.tiers[j];
16
+ if (typeof tier.unitAmount !== "number" || tier.unitAmount < 0) {
17
+ throw new Error(`${prefix}.tiers[${j}].unitAmount must be a non-negative number.`);
18
+ }
19
+ }
20
+ }
21
+ function validateConfig(config) {
22
+ var _a;
23
+ if (!config || typeof config !== "object") {
24
+ throw new Error("Billing config must be an object. Did you forget `export default defineBilling({ ... })`?");
25
+ }
26
+ const unwrapped = "_config" in config ? config._config : config;
27
+ const c = unwrapped;
28
+ if (!c.plans || typeof c.plans !== "object") {
29
+ throw new Error("Billing config must have a `plans` object.");
30
+ }
31
+ const plans = c.plans;
32
+ const planKeys = Object.keys(plans);
33
+ if (planKeys.length === 0) {
34
+ throw new Error("Billing config must define at least one plan.");
35
+ }
36
+ for (const key of planKeys) {
37
+ if (!/^[a-z][a-z0-9_]*$/.test(key)) {
38
+ throw new Error(
39
+ `Plan key "${key}" is invalid. Use lowercase alphanumeric with underscores (e.g., "pro", "team_plan").`
40
+ );
41
+ }
42
+ const plan = plans[key];
43
+ if (!plan || typeof plan !== "object") {
44
+ throw new Error(`Plan "${key}" must be an object.`);
45
+ }
46
+ if (!plan.name || typeof plan.name !== "string") {
47
+ throw new Error(`Plan "${key}" must have a "name" string.`);
48
+ }
49
+ if (plan.description !== void 0 && typeof plan.description !== "string") {
50
+ throw new Error(`Plan "${key}".description must be a string.`);
51
+ }
52
+ if (plan.features !== void 0) {
53
+ if (!Array.isArray(plan.features) || !plan.features.every((f) => typeof f === "string")) {
54
+ throw new Error(`Plan "${key}".features must be an array of strings.`);
55
+ }
56
+ }
57
+ if (plan.grandfathering !== void 0) {
58
+ const validStrategies = ["keep_price", "migrate_at_renewal", "migrate_immediately"];
59
+ if (!validStrategies.includes(plan.grandfathering)) {
60
+ throw new Error(
61
+ `Plan "${key}".grandfathering must be one of: ${validStrategies.join(", ")}.`
62
+ );
63
+ }
64
+ }
65
+ if (!Array.isArray(plan.prices) || plan.prices.length === 0) {
66
+ throw new Error(`Plan "${key}" must have at least one price.`);
67
+ }
68
+ for (let i = 0; i < plan.prices.length; i++) {
69
+ const price = plan.prices[i];
70
+ const prefix = `Plan "${key}".prices[${i}]`;
71
+ const priceType = (_a = price.type) != null ? _a : "recurring";
72
+ if (typeof price.currency !== "string" || price.currency.length !== 3) {
73
+ throw new Error(`${prefix}.currency must be a 3-letter ISO 4217 code (e.g., "usd").`);
74
+ }
75
+ if (!["monthly", "yearly"].includes(price.interval)) {
76
+ throw new Error(`${prefix}.interval must be "monthly" or "yearly".`);
77
+ }
78
+ if (price.intervalCount !== void 0) {
79
+ if (typeof price.intervalCount !== "number" || !Number.isInteger(price.intervalCount) || price.intervalCount < 1) {
80
+ throw new Error(`${prefix}.intervalCount must be a positive integer.`);
81
+ }
82
+ }
83
+ if (priceType === "recurring" || priceType === "per_seat") {
84
+ if (typeof price.amount !== "number" || !Number.isInteger(price.amount) || price.amount < 0) {
85
+ throw new Error(`${prefix}.amount must be a non-negative integer (cents).`);
86
+ }
87
+ if (priceType === "per_seat") {
88
+ if (typeof price.metric !== "string" || !price.metric) {
89
+ throw new Error(`${prefix}: per_seat price must have a "metric" string.`);
90
+ }
91
+ }
92
+ } else if (priceType === "metered") {
93
+ if (typeof price.metric !== "string" || !price.metric) {
94
+ throw new Error(`${prefix}: metered price must have a "metric" string.`);
95
+ }
96
+ validateTiers(price, prefix);
97
+ } else if (priceType === "tiered") {
98
+ validateTiers(price, prefix);
99
+ } else {
100
+ throw new Error(`${prefix}.type must be one of: "recurring", "per_seat", "metered", "tiered".`);
101
+ }
102
+ }
103
+ }
104
+ const featuresRegistry = c.features;
105
+ if (featuresRegistry !== void 0) {
106
+ if (typeof featuresRegistry !== "object" || Array.isArray(featuresRegistry)) {
107
+ throw new Error("Billing config `features` must be an object (features registry).");
108
+ }
109
+ for (const [fKey, fDef] of Object.entries(featuresRegistry)) {
110
+ if (!fDef || typeof fDef !== "object") {
111
+ throw new Error(`Feature "${fKey}" must be an object with a "type" field.`);
112
+ }
113
+ if (!["boolean", "metered"].includes(fDef.type)) {
114
+ throw new Error(`Feature "${fKey}".type must be "boolean" or "metered".`);
115
+ }
116
+ }
117
+ }
118
+ for (const key of planKeys) {
119
+ const plan = plans[key];
120
+ if (plan.entitlements !== void 0) {
121
+ if (typeof plan.entitlements !== "object" || plan.entitlements === null || Array.isArray(plan.entitlements)) {
122
+ throw new Error(`Plan "${key}".entitlements must be an object.`);
123
+ }
124
+ const entitlements = plan.entitlements;
125
+ for (const [eKey, eVal] of Object.entries(entitlements)) {
126
+ if (featuresRegistry && !featuresRegistry[eKey]) {
127
+ throw new Error(`Plan "${key}".entitlements.${eKey} is not defined in features registry.`);
128
+ }
129
+ const featureDef = featuresRegistry == null ? void 0 : featuresRegistry[eKey];
130
+ if (featureDef) {
131
+ if (featureDef.type === "metered") {
132
+ if (eVal === true || typeof eVal !== "object" || !eVal || !("limit" in eVal)) {
133
+ throw new Error(`Plan "${key}".entitlements.${eKey}: metered feature must have { limit: number }.`);
134
+ }
135
+ } else if (featureDef.type === "boolean") {
136
+ if (eVal !== true) {
137
+ throw new Error(`Plan "${key}".entitlements.${eKey}: boolean feature must be true.`);
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ const renameTargets = /* @__PURE__ */ new Map();
145
+ for (const key of planKeys) {
146
+ const plan = plans[key];
147
+ if (plan.renamedFrom !== void 0) {
148
+ if (typeof plan.renamedFrom !== "string") {
149
+ throw new Error(`Plan "${key}".renamedFrom must be a string.`);
150
+ }
151
+ if (plan.renamedFrom === key) {
152
+ throw new Error(`Plan "${key}".renamedFrom cannot equal its own key.`);
153
+ }
154
+ if (planKeys.includes(plan.renamedFrom)) {
155
+ throw new Error(
156
+ `Plan "${key}".renamedFrom "${plan.renamedFrom}" cannot reference an existing plan key.`
157
+ );
158
+ }
159
+ if (renameTargets.has(plan.renamedFrom)) {
160
+ throw new Error(
161
+ `renamedFrom "${plan.renamedFrom}" is claimed by multiple plans: "${renameTargets.get(plan.renamedFrom)}" and "${key}".`
162
+ );
163
+ }
164
+ renameTargets.set(plan.renamedFrom, key);
165
+ }
166
+ }
167
+ if (c.migrations !== void 0) {
168
+ if (!Array.isArray(c.migrations)) {
169
+ throw new Error("Billing config `migrations` must be an array.");
170
+ }
171
+ const migrationFroms = /* @__PURE__ */ new Set();
172
+ for (let i = 0; i < c.migrations.length; i++) {
173
+ const m = c.migrations[i];
174
+ const prefix = `migrations[${i}]`;
175
+ if (typeof m.from !== "string" || !m.from) {
176
+ throw new Error(`${prefix}.from must be a non-empty string.`);
177
+ }
178
+ if (typeof m.to !== "string" || !m.to) {
179
+ throw new Error(`${prefix}.to must be a non-empty string.`);
180
+ }
181
+ if (m.from === m.to) {
182
+ throw new Error(`${prefix}: migration "from" cannot equal "to".`);
183
+ }
184
+ if (!planKeys.includes(m.to)) {
185
+ throw new Error(`${prefix}: migration target "${m.to}" must reference an existing plan in config.`);
186
+ }
187
+ if (migrationFroms.has(m.from)) {
188
+ throw new Error(`${prefix}: duplicate migration from "${m.from}".`);
189
+ }
190
+ migrationFroms.add(m.from);
191
+ if (renameTargets.has(m.from)) {
192
+ throw new Error(
193
+ `Cannot rename and migrate the same key "${m.from}" in one sync. Rename first, then migrate in a subsequent sync.`
194
+ );
195
+ }
196
+ if (m.priceMapping !== void 0) {
197
+ if (typeof m.priceMapping !== "object" || m.priceMapping === null || Array.isArray(m.priceMapping)) {
198
+ throw new Error(`${prefix}.priceMapping must be an object { oldKey: newKey }.`);
199
+ }
200
+ }
201
+ }
202
+ }
203
+ return unwrapped;
204
+ }
205
+ function normalizeConfig(config) {
206
+ const plans = {};
207
+ for (const [key, plan] of Object.entries(config.plans)) {
208
+ if (plan.entitlements) {
209
+ plans[key] = plan;
210
+ } else if (plan.features && plan.features.length > 0) {
211
+ const entitlements = {};
212
+ for (const f of plan.features) {
213
+ entitlements[f] = true;
214
+ }
215
+ plans[key] = __spreadProps(__spreadValues({}, plan), { entitlements });
216
+ } else {
217
+ plans[key] = plan;
218
+ }
219
+ }
220
+ return __spreadProps(__spreadValues({}, config), { plans });
221
+ }
222
+
223
+ // src/diff.ts
224
+ function priceConfigKey(planKey, price) {
225
+ var _a, _b;
226
+ const interval = price.interval;
227
+ const count = (_a = price.intervalCount) != null ? _a : 1;
228
+ const suffix = count > 1 ? `_x${count}` : "";
229
+ const priceType = (_b = price.type) != null ? _b : "recurring";
230
+ if (priceType === "per_seat") {
231
+ const p2 = price;
232
+ return `${planKey}_seat_${p2.metric}_${interval}${suffix}_${p2.amount}_${p2.currency}`;
233
+ }
234
+ if (priceType === "metered") {
235
+ const p2 = price;
236
+ return `${planKey}_${p2.metric}_${interval}${suffix}_${p2.tierMode}_${p2.currency}`;
237
+ }
238
+ if (priceType === "tiered") {
239
+ const p2 = price;
240
+ return `${planKey}_${interval}${suffix}_${p2.tierMode}_${p2.currency}`;
241
+ }
242
+ const p = price;
243
+ return `${planKey}_${interval}${suffix}_${p.amount}_${p.currency}`;
244
+ }
245
+ function computeDiff(config, server, subscriberCounts) {
246
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
247
+ const planOps = [];
248
+ const priceOps = [];
249
+ let hasDestructive = false;
250
+ const serverByKey = /* @__PURE__ */ new Map();
251
+ const serverByPreviousKey = /* @__PURE__ */ new Map();
252
+ for (const product of server.products) {
253
+ if (product.configKey && product.managedBy === "config") {
254
+ serverByKey.set(product.configKey, product);
255
+ }
256
+ for (const prevKey of (_a = product.previousConfigKeys) != null ? _a : []) {
257
+ serverByPreviousKey.set(prevKey, product);
258
+ }
259
+ }
260
+ const renameMap = /* @__PURE__ */ new Map();
261
+ for (const [key, plan] of Object.entries(config.plans)) {
262
+ if (plan.renamedFrom) {
263
+ renameMap.set(plan.renamedFrom, key);
264
+ }
265
+ }
266
+ for (const [key, plan] of Object.entries(config.plans)) {
267
+ let existing = serverByKey.get(key);
268
+ if (!existing && plan.renamedFrom) {
269
+ existing = (_b = serverByKey.get(plan.renamedFrom)) != null ? _b : serverByPreviousKey.get(plan.renamedFrom);
270
+ if (existing) {
271
+ const subs = (_c = subscriberCounts[existing.id]) != null ? _c : 0;
272
+ planOps.push({ type: "rename", key, oldKey: plan.renamedFrom, plan, existing, activeSubscribers: subs });
273
+ const serverPricesByKey = /* @__PURE__ */ new Map();
274
+ for (const sp of existing.prices) {
275
+ if (sp.configKey && sp.isActive) {
276
+ serverPricesByKey.set(sp.configKey, sp);
277
+ }
278
+ }
279
+ const configPriceKeys = /* @__PURE__ */ new Set();
280
+ for (const price of plan.prices) {
281
+ const pk = priceConfigKey(key, price);
282
+ configPriceKeys.add(pk);
283
+ if (!serverPricesByKey.has(pk)) {
284
+ priceOps.push({ type: "create", planKey: key, price, configKey: pk });
285
+ }
286
+ }
287
+ for (const [pk, sp] of serverPricesByKey) {
288
+ if (!configPriceKeys.has(pk)) {
289
+ priceOps.push({ type: "archive", planKey: key, existing: sp, configKey: pk });
290
+ }
291
+ }
292
+ serverByKey.delete(existing.configKey);
293
+ serverByKey.delete(key);
294
+ continue;
295
+ }
296
+ }
297
+ if (!existing) {
298
+ planOps.push({ type: "create", key, plan });
299
+ for (const price of plan.prices) {
300
+ priceOps.push({
301
+ type: "create",
302
+ planKey: key,
303
+ price,
304
+ configKey: priceConfigKey(key, price)
305
+ });
306
+ }
307
+ } else {
308
+ const changes = [];
309
+ if (existing.name !== plan.name) changes.push(`name: "${existing.name}" \u2192 "${plan.name}"`);
310
+ if (((_d = existing.description) != null ? _d : void 0) !== ((_e = plan.description) != null ? _e : void 0)) {
311
+ changes.push(`description changed`);
312
+ }
313
+ const existingFeatures = ((_f = existing.features) != null ? _f : []).sort().join(",");
314
+ const configFeatures = ((_g = plan.features) != null ? _g : []).sort().join(",");
315
+ if (existingFeatures !== configFeatures) changes.push(`features changed`);
316
+ const existingEntitlements = JSON.stringify((_h = existing.entitlements) != null ? _h : null);
317
+ const configEntitlements = JSON.stringify((_i = plan.entitlements) != null ? _i : null);
318
+ if (existingEntitlements !== configEntitlements) changes.push(`entitlements changed`);
319
+ const serverPricesByKey = /* @__PURE__ */ new Map();
320
+ for (const sp of existing.prices) {
321
+ if (sp.configKey && sp.isActive) {
322
+ serverPricesByKey.set(sp.configKey, sp);
323
+ }
324
+ }
325
+ const configPriceKeys = /* @__PURE__ */ new Set();
326
+ for (const price of plan.prices) {
327
+ const pk = priceConfigKey(key, price);
328
+ configPriceKeys.add(pk);
329
+ if (!serverPricesByKey.has(pk)) {
330
+ priceOps.push({ type: "create", planKey: key, price, configKey: pk });
331
+ }
332
+ }
333
+ let hasPriceArchives = false;
334
+ for (const [pk, sp] of serverPricesByKey) {
335
+ if (!configPriceKeys.has(pk)) {
336
+ priceOps.push({ type: "archive", planKey: key, existing: sp, configKey: pk });
337
+ hasPriceArchives = true;
338
+ }
339
+ }
340
+ if (changes.length > 0 || hasPriceArchives) {
341
+ const versionBump = hasPriceArchives;
342
+ if (versionBump) changes.push("prices changed");
343
+ planOps.push(__spreadValues({
344
+ type: "update",
345
+ key,
346
+ plan,
347
+ existing,
348
+ changes
349
+ }, versionBump ? { versionBump: true, grandfathering: plan.grandfathering } : {}));
350
+ }
351
+ }
352
+ serverByKey.delete(key);
353
+ }
354
+ for (const [key, product] of serverByKey) {
355
+ if (renameMap.has(key)) continue;
356
+ if (product.isActive) {
357
+ const subs = (_j = subscriberCounts[product.id]) != null ? _j : 0;
358
+ if (subs > 0) hasDestructive = true;
359
+ planOps.push({ type: "archive", key, existing: product, activeSubscribers: subs });
360
+ for (const sp of product.prices) {
361
+ if (sp.isActive && sp.configKey) {
362
+ priceOps.push({ type: "archive", planKey: key, existing: sp, configKey: sp.configKey });
363
+ }
364
+ }
365
+ }
366
+ }
367
+ return { planOps, priceOps, hasDestructive };
368
+ }
369
+
370
+ // src/pull.ts
371
+ function slugify(name) {
372
+ let slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
373
+ if (/^[0-9]/.test(slug)) slug = `plan_${slug}`;
374
+ return slug || "plan";
375
+ }
376
+ function deduplicateKeys(keys) {
377
+ const seen = /* @__PURE__ */ new Map();
378
+ return keys.map((k) => {
379
+ var _a;
380
+ const count = ((_a = seen.get(k)) != null ? _a : 0) + 1;
381
+ seen.set(k, count);
382
+ return count > 1 ? `${k}_${count}` : k;
383
+ });
384
+ }
385
+ function serverStateToBillingConfig(state, options) {
386
+ const dashboardPlans = [];
387
+ const activeProducts = state.products.filter((p) => p.isActive);
388
+ const configProducts = [];
389
+ for (const product of activeProducts) {
390
+ if (product.managedBy === "dashboard" && !(options == null ? void 0 : options.includeDashboard)) {
391
+ dashboardPlans.push({
392
+ name: product.name,
393
+ suggestedKey: slugify(product.name)
394
+ });
395
+ } else {
396
+ configProducts.push(product);
397
+ }
398
+ }
399
+ const rawKeys = configProducts.map((p) => {
400
+ var _a;
401
+ return (_a = p.configKey) != null ? _a : slugify(p.name);
402
+ });
403
+ const keys = deduplicateKeys(rawKeys);
404
+ const plans = {};
405
+ for (let i = 0; i < configProducts.length; i++) {
406
+ const product = configProducts[i];
407
+ const key = keys[i];
408
+ const prices = product.prices.filter((p) => p.isActive).map((p) => {
409
+ var _a;
410
+ const price = {
411
+ amount: (_a = p.amount) != null ? _a : 0,
412
+ currency: p.currency,
413
+ interval: p.interval
414
+ };
415
+ if (p.intervalCount > 1) price.intervalCount = p.intervalCount;
416
+ return price;
417
+ });
418
+ const plan = { name: product.name, prices };
419
+ if (product.description) plan.description = product.description;
420
+ if (product.features.length > 0) plan.features = product.features;
421
+ if (product.entitlements && Object.keys(product.entitlements).length > 0) {
422
+ plan.entitlements = product.entitlements;
423
+ }
424
+ plans[key] = plan;
425
+ }
426
+ return { config: { plans }, dashboardPlans };
427
+ }
428
+ function renderConfigAsTypeScript(result) {
429
+ var _a;
430
+ const lines = [
431
+ 'import { defineBilling } from "@auth-gate/billing";',
432
+ "",
433
+ "export default defineBilling({",
434
+ " plans: {"
435
+ ];
436
+ const planEntries = Object.entries(result.config.plans);
437
+ for (const [key, plan] of planEntries) {
438
+ lines.push(` ${key}: {`);
439
+ lines.push(` name: ${JSON.stringify(plan.name)},`);
440
+ if (plan.description)
441
+ lines.push(` description: ${JSON.stringify(plan.description)},`);
442
+ if ((_a = plan.features) == null ? void 0 : _a.length)
443
+ lines.push(` features: ${JSON.stringify(plan.features)},`);
444
+ lines.push(" prices: [");
445
+ for (const price of plan.prices) {
446
+ const parts = [];
447
+ if ("amount" in price)
448
+ parts.push(`amount: ${price.amount}`);
449
+ parts.push(
450
+ `currency: ${JSON.stringify(price.currency)}`,
451
+ `interval: ${JSON.stringify(price.interval)}`
452
+ );
453
+ if (price.intervalCount && price.intervalCount > 1)
454
+ parts.push(`intervalCount: ${price.intervalCount}`);
455
+ lines.push(` { ${parts.join(", ")} },`);
456
+ }
457
+ lines.push(" ],");
458
+ lines.push(" },");
459
+ }
460
+ lines.push(" },");
461
+ lines.push("});");
462
+ if (result.dashboardPlans.length > 0) {
463
+ lines.push("");
464
+ lines.push("// Dashboard-managed plans (not synced by config):");
465
+ for (const dp of result.dashboardPlans) {
466
+ lines.push(
467
+ `// ${dp.suggestedKey}: { name: ${JSON.stringify(dp.name)} }`
468
+ );
469
+ }
470
+ }
471
+ return lines.join("\n") + "\n";
472
+ }
473
+
474
+ export {
475
+ validateConfig,
476
+ normalizeConfig,
477
+ priceConfigKey,
478
+ computeDiff,
479
+ slugify,
480
+ serverStateToBillingConfig,
481
+ renderConfigAsTypeScript
482
+ };
@@ -0,0 +1,24 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __spreadValues = (a, b) => {
9
+ for (var prop in b || (b = {}))
10
+ if (__hasOwnProp.call(b, prop))
11
+ __defNormalProp(a, prop, b[prop]);
12
+ if (__getOwnPropSymbols)
13
+ for (var prop of __getOwnPropSymbols(b)) {
14
+ if (__propIsEnum.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ }
17
+ return a;
18
+ };
19
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
+
21
+ export {
22
+ __spreadValues,
23
+ __spreadProps
24
+ };