@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.
package/dist/cli.cjs ADDED
@@ -0,0 +1,1365 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
11
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
12
+ var __spreadValues = (a, b) => {
13
+ for (var prop in b || (b = {}))
14
+ if (__hasOwnProp.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ if (__getOwnPropSymbols)
17
+ for (var prop of __getOwnPropSymbols(b)) {
18
+ if (__propIsEnum.call(b, prop))
19
+ __defNormalProp(a, prop, b[prop]);
20
+ }
21
+ return a;
22
+ };
23
+ var __esm = (fn, res) => function __init() {
24
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
25
+ };
26
+ var __export = (target, all) => {
27
+ for (var name in all)
28
+ __defProp(target, name, { get: all[name], enumerable: true });
29
+ };
30
+ var __copyProps = (to, from, except, desc) => {
31
+ if (from && typeof from === "object" || typeof from === "function") {
32
+ for (let key of __getOwnPropNames(from))
33
+ if (!__hasOwnProp.call(to, key) && key !== except)
34
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
35
+ }
36
+ return to;
37
+ };
38
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
39
+ // If the importer is in node compatibility mode or this is not an ESM
40
+ // file that has been converted to a CommonJS file using a Babel-
41
+ // compatible transform (i.e. "__esModule" has not been set), then set
42
+ // "default" to the CommonJS "module.exports" for node compatibility.
43
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
44
+ mod
45
+ ));
46
+
47
+ // src/pull-from-stripe.ts
48
+ var pull_from_stripe_exports = {};
49
+ __export(pull_from_stripe_exports, {
50
+ fetchStripeState: () => fetchStripeState
51
+ });
52
+ async function fetchStripeState(stripeKey) {
53
+ var _a, _b, _c, _d, _e;
54
+ const Stripe = (await import("stripe")).default;
55
+ const stripe = new Stripe(stripeKey);
56
+ const [products, prices] = await Promise.all([
57
+ stripe.products.list({ active: true, limit: 100 }),
58
+ stripe.prices.list({ active: true, limit: 100 })
59
+ ]);
60
+ const pricesByProduct = /* @__PURE__ */ new Map();
61
+ for (const sp of prices.data) {
62
+ const productId = typeof sp.product === "string" ? sp.product : sp.product.id;
63
+ const mapped = {
64
+ id: sp.id,
65
+ configKey: null,
66
+ amount: sp.unit_amount,
67
+ currency: sp.currency,
68
+ interval: (_b = (_a = sp.recurring) == null ? void 0 : _a.interval) != null ? _b : "monthly",
69
+ intervalCount: (_d = (_c = sp.recurring) == null ? void 0 : _c.interval_count) != null ? _d : 1,
70
+ isActive: sp.active,
71
+ stripePriceId: sp.id
72
+ };
73
+ pricesByProduct.set(productId, [
74
+ ...(_e = pricesByProduct.get(productId)) != null ? _e : [],
75
+ mapped
76
+ ]);
77
+ }
78
+ const serverProducts = products.data.map((p) => {
79
+ var _a2;
80
+ return {
81
+ id: p.id,
82
+ configKey: null,
83
+ name: p.name,
84
+ description: p.description,
85
+ features: [],
86
+ isActive: p.active,
87
+ managedBy: "config",
88
+ stripeProductId: p.id,
89
+ previousConfigKeys: [],
90
+ prices: (_a2 = pricesByProduct.get(p.id)) != null ? _a2 : []
91
+ };
92
+ });
93
+ return { products: serverProducts };
94
+ }
95
+ var init_pull_from_stripe = __esm({
96
+ "src/pull-from-stripe.ts"() {
97
+ "use strict";
98
+ }
99
+ });
100
+
101
+ // src/config-loader.ts
102
+ var import_path = require("path");
103
+ var import_fs = require("fs");
104
+
105
+ // src/validation.ts
106
+ function validateTiers(price, prefix) {
107
+ if (!Array.isArray(price.tiers) || price.tiers.length === 0) {
108
+ throw new Error(`${prefix}: tiered/metered price must have a non-empty "tiers" array.`);
109
+ }
110
+ if (!["graduated", "volume"].includes(price.tierMode)) {
111
+ throw new Error(`${prefix}.tierMode must be "graduated" or "volume".`);
112
+ }
113
+ for (let j = 0; j < price.tiers.length; j++) {
114
+ const tier = price.tiers[j];
115
+ if (typeof tier.unitAmount !== "number" || tier.unitAmount < 0) {
116
+ throw new Error(`${prefix}.tiers[${j}].unitAmount must be a non-negative number.`);
117
+ }
118
+ }
119
+ }
120
+ function validateConfig(config) {
121
+ var _a;
122
+ if (!config || typeof config !== "object") {
123
+ throw new Error("Billing config must be an object. Did you forget `export default defineBilling({ ... })`?");
124
+ }
125
+ const unwrapped = "_config" in config ? config._config : config;
126
+ const c = unwrapped;
127
+ if (!c.plans || typeof c.plans !== "object") {
128
+ throw new Error("Billing config must have a `plans` object.");
129
+ }
130
+ const plans = c.plans;
131
+ const planKeys = Object.keys(plans);
132
+ if (planKeys.length === 0) {
133
+ throw new Error("Billing config must define at least one plan.");
134
+ }
135
+ for (const key of planKeys) {
136
+ if (!/^[a-z][a-z0-9_]*$/.test(key)) {
137
+ throw new Error(
138
+ `Plan key "${key}" is invalid. Use lowercase alphanumeric with underscores (e.g., "pro", "team_plan").`
139
+ );
140
+ }
141
+ const plan = plans[key];
142
+ if (!plan || typeof plan !== "object") {
143
+ throw new Error(`Plan "${key}" must be an object.`);
144
+ }
145
+ if (!plan.name || typeof plan.name !== "string") {
146
+ throw new Error(`Plan "${key}" must have a "name" string.`);
147
+ }
148
+ if (plan.description !== void 0 && typeof plan.description !== "string") {
149
+ throw new Error(`Plan "${key}".description must be a string.`);
150
+ }
151
+ if (plan.features !== void 0) {
152
+ if (!Array.isArray(plan.features) || !plan.features.every((f) => typeof f === "string")) {
153
+ throw new Error(`Plan "${key}".features must be an array of strings.`);
154
+ }
155
+ }
156
+ if (plan.grandfathering !== void 0) {
157
+ const validStrategies = ["keep_price", "migrate_at_renewal", "migrate_immediately"];
158
+ if (!validStrategies.includes(plan.grandfathering)) {
159
+ throw new Error(
160
+ `Plan "${key}".grandfathering must be one of: ${validStrategies.join(", ")}.`
161
+ );
162
+ }
163
+ }
164
+ if (!Array.isArray(plan.prices) || plan.prices.length === 0) {
165
+ throw new Error(`Plan "${key}" must have at least one price.`);
166
+ }
167
+ for (let i = 0; i < plan.prices.length; i++) {
168
+ const price = plan.prices[i];
169
+ const prefix = `Plan "${key}".prices[${i}]`;
170
+ const priceType = (_a = price.type) != null ? _a : "recurring";
171
+ if (typeof price.currency !== "string" || price.currency.length !== 3) {
172
+ throw new Error(`${prefix}.currency must be a 3-letter ISO 4217 code (e.g., "usd").`);
173
+ }
174
+ if (!["monthly", "yearly"].includes(price.interval)) {
175
+ throw new Error(`${prefix}.interval must be "monthly" or "yearly".`);
176
+ }
177
+ if (price.intervalCount !== void 0) {
178
+ if (typeof price.intervalCount !== "number" || !Number.isInteger(price.intervalCount) || price.intervalCount < 1) {
179
+ throw new Error(`${prefix}.intervalCount must be a positive integer.`);
180
+ }
181
+ }
182
+ if (priceType === "recurring" || priceType === "per_seat") {
183
+ if (typeof price.amount !== "number" || !Number.isInteger(price.amount) || price.amount < 0) {
184
+ throw new Error(`${prefix}.amount must be a non-negative integer (cents).`);
185
+ }
186
+ if (priceType === "per_seat") {
187
+ if (typeof price.metric !== "string" || !price.metric) {
188
+ throw new Error(`${prefix}: per_seat price must have a "metric" string.`);
189
+ }
190
+ }
191
+ } else if (priceType === "metered") {
192
+ if (typeof price.metric !== "string" || !price.metric) {
193
+ throw new Error(`${prefix}: metered price must have a "metric" string.`);
194
+ }
195
+ validateTiers(price, prefix);
196
+ } else if (priceType === "tiered") {
197
+ validateTiers(price, prefix);
198
+ } else {
199
+ throw new Error(`${prefix}.type must be one of: "recurring", "per_seat", "metered", "tiered".`);
200
+ }
201
+ }
202
+ }
203
+ const featuresRegistry = c.features;
204
+ if (featuresRegistry !== void 0) {
205
+ if (typeof featuresRegistry !== "object" || Array.isArray(featuresRegistry)) {
206
+ throw new Error("Billing config `features` must be an object (features registry).");
207
+ }
208
+ for (const [fKey, fDef] of Object.entries(featuresRegistry)) {
209
+ if (!fDef || typeof fDef !== "object") {
210
+ throw new Error(`Feature "${fKey}" must be an object with a "type" field.`);
211
+ }
212
+ if (!["boolean", "metered"].includes(fDef.type)) {
213
+ throw new Error(`Feature "${fKey}".type must be "boolean" or "metered".`);
214
+ }
215
+ }
216
+ }
217
+ for (const key of planKeys) {
218
+ const plan = plans[key];
219
+ if (plan.entitlements !== void 0) {
220
+ if (typeof plan.entitlements !== "object" || plan.entitlements === null || Array.isArray(plan.entitlements)) {
221
+ throw new Error(`Plan "${key}".entitlements must be an object.`);
222
+ }
223
+ const entitlements = plan.entitlements;
224
+ for (const [eKey, eVal] of Object.entries(entitlements)) {
225
+ if (featuresRegistry && !featuresRegistry[eKey]) {
226
+ throw new Error(`Plan "${key}".entitlements.${eKey} is not defined in features registry.`);
227
+ }
228
+ const featureDef = featuresRegistry == null ? void 0 : featuresRegistry[eKey];
229
+ if (featureDef) {
230
+ if (featureDef.type === "metered") {
231
+ if (eVal === true || typeof eVal !== "object" || !eVal || !("limit" in eVal)) {
232
+ throw new Error(`Plan "${key}".entitlements.${eKey}: metered feature must have { limit: number }.`);
233
+ }
234
+ } else if (featureDef.type === "boolean") {
235
+ if (eVal !== true) {
236
+ throw new Error(`Plan "${key}".entitlements.${eKey}: boolean feature must be true.`);
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+ const renameTargets = /* @__PURE__ */ new Map();
244
+ for (const key of planKeys) {
245
+ const plan = plans[key];
246
+ if (plan.renamedFrom !== void 0) {
247
+ if (typeof plan.renamedFrom !== "string") {
248
+ throw new Error(`Plan "${key}".renamedFrom must be a string.`);
249
+ }
250
+ if (plan.renamedFrom === key) {
251
+ throw new Error(`Plan "${key}".renamedFrom cannot equal its own key.`);
252
+ }
253
+ if (planKeys.includes(plan.renamedFrom)) {
254
+ throw new Error(
255
+ `Plan "${key}".renamedFrom "${plan.renamedFrom}" cannot reference an existing plan key.`
256
+ );
257
+ }
258
+ if (renameTargets.has(plan.renamedFrom)) {
259
+ throw new Error(
260
+ `renamedFrom "${plan.renamedFrom}" is claimed by multiple plans: "${renameTargets.get(plan.renamedFrom)}" and "${key}".`
261
+ );
262
+ }
263
+ renameTargets.set(plan.renamedFrom, key);
264
+ }
265
+ }
266
+ if (c.migrations !== void 0) {
267
+ if (!Array.isArray(c.migrations)) {
268
+ throw new Error("Billing config `migrations` must be an array.");
269
+ }
270
+ const migrationFroms = /* @__PURE__ */ new Set();
271
+ for (let i = 0; i < c.migrations.length; i++) {
272
+ const m = c.migrations[i];
273
+ const prefix = `migrations[${i}]`;
274
+ if (typeof m.from !== "string" || !m.from) {
275
+ throw new Error(`${prefix}.from must be a non-empty string.`);
276
+ }
277
+ if (typeof m.to !== "string" || !m.to) {
278
+ throw new Error(`${prefix}.to must be a non-empty string.`);
279
+ }
280
+ if (m.from === m.to) {
281
+ throw new Error(`${prefix}: migration "from" cannot equal "to".`);
282
+ }
283
+ if (!planKeys.includes(m.to)) {
284
+ throw new Error(`${prefix}: migration target "${m.to}" must reference an existing plan in config.`);
285
+ }
286
+ if (migrationFroms.has(m.from)) {
287
+ throw new Error(`${prefix}: duplicate migration from "${m.from}".`);
288
+ }
289
+ migrationFroms.add(m.from);
290
+ if (renameTargets.has(m.from)) {
291
+ throw new Error(
292
+ `Cannot rename and migrate the same key "${m.from}" in one sync. Rename first, then migrate in a subsequent sync.`
293
+ );
294
+ }
295
+ if (m.priceMapping !== void 0) {
296
+ if (typeof m.priceMapping !== "object" || m.priceMapping === null || Array.isArray(m.priceMapping)) {
297
+ throw new Error(`${prefix}.priceMapping must be an object { oldKey: newKey }.`);
298
+ }
299
+ }
300
+ }
301
+ }
302
+ return unwrapped;
303
+ }
304
+
305
+ // src/config-loader.ts
306
+ var CONFIG_FILENAMES = [
307
+ "authgate.billing.ts",
308
+ "authgate.billing.js",
309
+ "authgate.billing.mjs"
310
+ ];
311
+ async function loadConfig(cwd) {
312
+ var _a, _b;
313
+ let configPath = null;
314
+ for (const filename of CONFIG_FILENAMES) {
315
+ const candidate = (0, import_path.resolve)(cwd, filename);
316
+ if ((0, import_fs.existsSync)(candidate)) {
317
+ configPath = candidate;
318
+ break;
319
+ }
320
+ }
321
+ if (!configPath) {
322
+ throw new Error(
323
+ `No billing config found. Expected one of: ${CONFIG_FILENAMES.join(", ")}
324
+ Run \`npx @auth-gate/billing init\` to create one.`
325
+ );
326
+ }
327
+ const { createJiti } = await import("jiti");
328
+ const jiti = createJiti(cwd, { interopDefault: true });
329
+ const mod = await jiti.import(configPath);
330
+ const raw = (_b = (_a = mod.default) != null ? _a : mod.billing) != null ? _b : mod;
331
+ const config = raw && typeof raw === "object" && "_config" in raw ? raw._config : raw;
332
+ return validateConfig(config);
333
+ }
334
+
335
+ // src/diff.ts
336
+ function priceConfigKey(planKey, price) {
337
+ var _a, _b;
338
+ const interval = price.interval;
339
+ const count = (_a = price.intervalCount) != null ? _a : 1;
340
+ const suffix = count > 1 ? `_x${count}` : "";
341
+ const priceType = (_b = price.type) != null ? _b : "recurring";
342
+ if (priceType === "per_seat") {
343
+ const p2 = price;
344
+ return `${planKey}_seat_${p2.metric}_${interval}${suffix}_${p2.amount}_${p2.currency}`;
345
+ }
346
+ if (priceType === "metered") {
347
+ const p2 = price;
348
+ return `${planKey}_${p2.metric}_${interval}${suffix}_${p2.tierMode}_${p2.currency}`;
349
+ }
350
+ if (priceType === "tiered") {
351
+ const p2 = price;
352
+ return `${planKey}_${interval}${suffix}_${p2.tierMode}_${p2.currency}`;
353
+ }
354
+ const p = price;
355
+ return `${planKey}_${interval}${suffix}_${p.amount}_${p.currency}`;
356
+ }
357
+ function computeDiff(config, server, subscriberCounts) {
358
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
359
+ const planOps = [];
360
+ const priceOps = [];
361
+ let hasDestructive = false;
362
+ const serverByKey = /* @__PURE__ */ new Map();
363
+ const serverByPreviousKey = /* @__PURE__ */ new Map();
364
+ for (const product of server.products) {
365
+ if (product.configKey && product.managedBy === "config") {
366
+ serverByKey.set(product.configKey, product);
367
+ }
368
+ for (const prevKey of (_a = product.previousConfigKeys) != null ? _a : []) {
369
+ serverByPreviousKey.set(prevKey, product);
370
+ }
371
+ }
372
+ const renameMap = /* @__PURE__ */ new Map();
373
+ for (const [key, plan] of Object.entries(config.plans)) {
374
+ if (plan.renamedFrom) {
375
+ renameMap.set(plan.renamedFrom, key);
376
+ }
377
+ }
378
+ for (const [key, plan] of Object.entries(config.plans)) {
379
+ let existing = serverByKey.get(key);
380
+ if (!existing && plan.renamedFrom) {
381
+ existing = (_b = serverByKey.get(plan.renamedFrom)) != null ? _b : serverByPreviousKey.get(plan.renamedFrom);
382
+ if (existing) {
383
+ const subs = (_c = subscriberCounts[existing.id]) != null ? _c : 0;
384
+ planOps.push({ type: "rename", key, oldKey: plan.renamedFrom, plan, existing, activeSubscribers: subs });
385
+ const serverPricesByKey = /* @__PURE__ */ new Map();
386
+ for (const sp of existing.prices) {
387
+ if (sp.configKey && sp.isActive) {
388
+ serverPricesByKey.set(sp.configKey, sp);
389
+ }
390
+ }
391
+ const configPriceKeys = /* @__PURE__ */ new Set();
392
+ for (const price of plan.prices) {
393
+ const pk = priceConfigKey(key, price);
394
+ configPriceKeys.add(pk);
395
+ if (!serverPricesByKey.has(pk)) {
396
+ priceOps.push({ type: "create", planKey: key, price, configKey: pk });
397
+ }
398
+ }
399
+ for (const [pk, sp] of serverPricesByKey) {
400
+ if (!configPriceKeys.has(pk)) {
401
+ priceOps.push({ type: "archive", planKey: key, existing: sp, configKey: pk });
402
+ }
403
+ }
404
+ serverByKey.delete(existing.configKey);
405
+ serverByKey.delete(key);
406
+ continue;
407
+ }
408
+ }
409
+ if (!existing) {
410
+ planOps.push({ type: "create", key, plan });
411
+ for (const price of plan.prices) {
412
+ priceOps.push({
413
+ type: "create",
414
+ planKey: key,
415
+ price,
416
+ configKey: priceConfigKey(key, price)
417
+ });
418
+ }
419
+ } else {
420
+ const changes = [];
421
+ if (existing.name !== plan.name) changes.push(`name: "${existing.name}" \u2192 "${plan.name}"`);
422
+ if (((_d = existing.description) != null ? _d : void 0) !== ((_e = plan.description) != null ? _e : void 0)) {
423
+ changes.push(`description changed`);
424
+ }
425
+ const existingFeatures = ((_f = existing.features) != null ? _f : []).sort().join(",");
426
+ const configFeatures = ((_g = plan.features) != null ? _g : []).sort().join(",");
427
+ if (existingFeatures !== configFeatures) changes.push(`features changed`);
428
+ const existingEntitlements = JSON.stringify((_h = existing.entitlements) != null ? _h : null);
429
+ const configEntitlements = JSON.stringify((_i = plan.entitlements) != null ? _i : null);
430
+ if (existingEntitlements !== configEntitlements) changes.push(`entitlements changed`);
431
+ const serverPricesByKey = /* @__PURE__ */ new Map();
432
+ for (const sp of existing.prices) {
433
+ if (sp.configKey && sp.isActive) {
434
+ serverPricesByKey.set(sp.configKey, sp);
435
+ }
436
+ }
437
+ const configPriceKeys = /* @__PURE__ */ new Set();
438
+ for (const price of plan.prices) {
439
+ const pk = priceConfigKey(key, price);
440
+ configPriceKeys.add(pk);
441
+ if (!serverPricesByKey.has(pk)) {
442
+ priceOps.push({ type: "create", planKey: key, price, configKey: pk });
443
+ }
444
+ }
445
+ let hasPriceArchives = false;
446
+ for (const [pk, sp] of serverPricesByKey) {
447
+ if (!configPriceKeys.has(pk)) {
448
+ priceOps.push({ type: "archive", planKey: key, existing: sp, configKey: pk });
449
+ hasPriceArchives = true;
450
+ }
451
+ }
452
+ if (changes.length > 0 || hasPriceArchives) {
453
+ const versionBump = hasPriceArchives;
454
+ if (versionBump) changes.push("prices changed");
455
+ planOps.push(__spreadValues({
456
+ type: "update",
457
+ key,
458
+ plan,
459
+ existing,
460
+ changes
461
+ }, versionBump ? { versionBump: true, grandfathering: plan.grandfathering } : {}));
462
+ }
463
+ }
464
+ serverByKey.delete(key);
465
+ }
466
+ for (const [key, product] of serverByKey) {
467
+ if (renameMap.has(key)) continue;
468
+ if (product.isActive) {
469
+ const subs = (_j = subscriberCounts[product.id]) != null ? _j : 0;
470
+ if (subs > 0) hasDestructive = true;
471
+ planOps.push({ type: "archive", key, existing: product, activeSubscribers: subs });
472
+ for (const sp of product.prices) {
473
+ if (sp.isActive && sp.configKey) {
474
+ priceOps.push({ type: "archive", planKey: key, existing: sp, configKey: sp.configKey });
475
+ }
476
+ }
477
+ }
478
+ }
479
+ return { planOps, priceOps, hasDestructive };
480
+ }
481
+
482
+ // src/formatter.ts
483
+ var import_chalk = __toESM(require("chalk"), 1);
484
+ function formatAmount(cents, currency) {
485
+ const amount = (cents / 100).toFixed(2);
486
+ return `${currency.toUpperCase()} ${amount}`;
487
+ }
488
+ function formatInterval(interval, count) {
489
+ const c = count != null ? count : 1;
490
+ if (c === 1) return interval === "monthly" ? "/mo" : "/yr";
491
+ return `every ${c} ${interval.replace("ly", "s")}`;
492
+ }
493
+ function formatPrice(price) {
494
+ var _a;
495
+ const priceType = (_a = price.type) != null ? _a : "recurring";
496
+ const interval = formatInterval(price.interval, price.intervalCount);
497
+ if (priceType === "per_seat") {
498
+ const p2 = price;
499
+ return `per seat (${p2.metric}): ${formatAmount(p2.amount, p2.currency)}${interval}`;
500
+ }
501
+ if (priceType === "metered") {
502
+ const p2 = price;
503
+ return `metered (${p2.metric}): ${p2.tierMode} tiers ${p2.currency.toUpperCase()}${interval}`;
504
+ }
505
+ if (priceType === "tiered") {
506
+ const p2 = price;
507
+ return `tiered: ${p2.tierMode} tiers ${p2.currency.toUpperCase()}${interval}`;
508
+ }
509
+ const p = price;
510
+ return `${formatAmount(p.amount, p.currency)}${interval}`;
511
+ }
512
+ function formatPriceOp(op) {
513
+ if (op.type === "create") {
514
+ return formatPrice(op.price);
515
+ }
516
+ return op.configKey;
517
+ }
518
+ function formatDiff(diff, dryRun) {
519
+ var _a, _b;
520
+ const lines = [];
521
+ if (dryRun) {
522
+ lines.push(import_chalk.default.bold("AuthGate Billing Sync \u2014 DRY RUN") + import_chalk.default.dim(" (use --apply to execute)"));
523
+ } else {
524
+ lines.push(import_chalk.default.bold("AuthGate Billing Sync \u2014 APPLYING CHANGES"));
525
+ }
526
+ lines.push("");
527
+ if (diff.planOps.length === 0 && diff.priceOps.length === 0) {
528
+ lines.push(import_chalk.default.green(" Everything is in sync. No changes needed."));
529
+ return lines.join("\n");
530
+ }
531
+ for (const op of diff.planOps) {
532
+ if (op.type === "create") {
533
+ lines.push(import_chalk.default.green(` + CREATE plan "${op.key}"`));
534
+ if (op.plan.description) {
535
+ lines.push(import_chalk.default.dim(` ${op.plan.description}`));
536
+ }
537
+ if ((_a = op.plan.features) == null ? void 0 : _a.length) {
538
+ lines.push(import_chalk.default.dim(` features: ${op.plan.features.join(", ")}`));
539
+ }
540
+ if (op.plan.entitlements && Object.keys(op.plan.entitlements).length > 0) {
541
+ const entries = Object.entries(op.plan.entitlements).map(
542
+ ([k, v]) => v === true ? k : `${k} (limit: ${v.limit})`
543
+ );
544
+ lines.push(import_chalk.default.dim(` entitlements: ${entries.join(", ")}`));
545
+ }
546
+ for (const price of op.plan.prices) {
547
+ lines.push(
548
+ import_chalk.default.green(` + price: ${formatPrice(price)}`)
549
+ );
550
+ }
551
+ } else if (op.type === "update") {
552
+ let updateLabel = ` ~ UPDATE plan "${op.key}"`;
553
+ if (op.versionBump) {
554
+ const oldVersion = (_b = op.existing.version) != null ? _b : 1;
555
+ updateLabel += ` (v${oldVersion} \u2192 v${oldVersion + 1})`;
556
+ if (op.grandfathering) {
557
+ updateLabel += import_chalk.default.dim(` [${op.grandfathering}]`);
558
+ }
559
+ }
560
+ lines.push(import_chalk.default.yellow(updateLabel));
561
+ for (const change of op.changes) {
562
+ lines.push(import_chalk.default.yellow(` ${change}`));
563
+ }
564
+ } else if (op.type === "archive") {
565
+ lines.push(import_chalk.default.red(` - ARCHIVE plan "${op.key}"`));
566
+ if (op.activeSubscribers > 0) {
567
+ lines.push(
568
+ import_chalk.default.red.bold(` \u26A0 ${op.activeSubscribers} active subscriber${op.activeSubscribers > 1 ? "s" : ""} \u2014 they keep their current plan until cancellation`)
569
+ );
570
+ }
571
+ } else if (op.type === "rename") {
572
+ lines.push(import_chalk.default.cyan(` ~ RENAME plan "${op.oldKey}" \u2192 "${op.key}"`));
573
+ if (op.activeSubscribers > 0) {
574
+ lines.push(import_chalk.default.dim(` ${op.activeSubscribers} subscriber${op.activeSubscribers > 1 ? "s" : ""} preserved`));
575
+ }
576
+ }
577
+ lines.push("");
578
+ }
579
+ const standalonePriceOps = diff.priceOps.filter(
580
+ (op) => !diff.planOps.some((po) => po.type === "create" && po.key === op.planKey)
581
+ );
582
+ for (const op of standalonePriceOps) {
583
+ if (op.type === "create") {
584
+ lines.push(import_chalk.default.green(` + price on "${op.planKey}": ${formatPriceOp(op)}`));
585
+ } else if (op.type === "archive") {
586
+ lines.push(import_chalk.default.red(` - archive price on "${op.planKey}" (${op.configKey})`));
587
+ }
588
+ }
589
+ const creates = diff.planOps.filter((o) => o.type === "create").length;
590
+ const updates = diff.planOps.filter((o) => o.type === "update").length;
591
+ const archives = diff.planOps.filter((o) => o.type === "archive").length;
592
+ const renames = diff.planOps.filter((o) => o.type === "rename").length;
593
+ lines.push("");
594
+ lines.push(
595
+ import_chalk.default.dim(` Summary: ${creates} create, ${updates} update, ${renames} rename, ${archives} archive.`) + (dryRun ? import_chalk.default.dim(" Run with --apply to execute.") : "")
596
+ );
597
+ if (diff.hasDestructive && dryRun) {
598
+ lines.push(
599
+ import_chalk.default.red.bold("\n \u26A0 Destructive changes detected (plans with active subscribers).") + import_chalk.default.red(" Use --apply --force to proceed.")
600
+ );
601
+ }
602
+ return lines.join("\n");
603
+ }
604
+
605
+ // src/sync.ts
606
+ var SyncClient = class {
607
+ constructor(config) {
608
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
609
+ this.apiKey = config.apiKey;
610
+ this.environment = config.environment;
611
+ }
612
+ async request(method, path, body) {
613
+ var _a, _b;
614
+ const url = `${this.baseUrl}${path}`;
615
+ const headers = {
616
+ Authorization: `Bearer ${this.apiKey}`,
617
+ "Content-Type": "application/json"
618
+ };
619
+ if (this.environment) {
620
+ headers["X-AuthGate-Environment"] = this.environment;
621
+ }
622
+ const res = await fetch(url, {
623
+ method,
624
+ headers,
625
+ body: body ? JSON.stringify(body) : void 0
626
+ });
627
+ if (!res.ok) {
628
+ const text = await res.text();
629
+ let message;
630
+ try {
631
+ const json = JSON.parse(text);
632
+ message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : text;
633
+ } catch (e) {
634
+ message = text;
635
+ }
636
+ throw new Error(`API error (${res.status}): ${message}`);
637
+ }
638
+ return res.json();
639
+ }
640
+ async getState() {
641
+ return this.request("GET", "/api/v1/billing/sync/state");
642
+ }
643
+ async getSubscriberCounts() {
644
+ return this.request("GET", "/api/v1/billing/sync/subscribers");
645
+ }
646
+ async apply(config, force) {
647
+ return this.request("POST", "/api/v1/billing/sync/apply", {
648
+ plans: config.plans,
649
+ migrations: config.migrations,
650
+ force
651
+ });
652
+ }
653
+ async getMigration(migrationId) {
654
+ return this.request(
655
+ "GET",
656
+ `/api/v1/billing/migrations/${migrationId}`
657
+ );
658
+ }
659
+ async executeMigration(migrationId, opts) {
660
+ return this.request(
661
+ "POST",
662
+ `/api/v1/billing/migrations/${migrationId}/execute`,
663
+ (opts == null ? void 0 : opts.batchSize) ? { batchSize: opts.batchSize } : void 0
664
+ );
665
+ }
666
+ async createMigration(from, to, opts) {
667
+ return this.request(
668
+ "POST",
669
+ "/api/v1/billing/migrations",
670
+ { from, to, priceMapping: opts == null ? void 0 : opts.priceMapping }
671
+ );
672
+ }
673
+ };
674
+
675
+ // src/pull.ts
676
+ function slugify(name) {
677
+ let slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
678
+ if (/^[0-9]/.test(slug)) slug = `plan_${slug}`;
679
+ return slug || "plan";
680
+ }
681
+ function deduplicateKeys(keys) {
682
+ const seen = /* @__PURE__ */ new Map();
683
+ return keys.map((k) => {
684
+ var _a;
685
+ const count = ((_a = seen.get(k)) != null ? _a : 0) + 1;
686
+ seen.set(k, count);
687
+ return count > 1 ? `${k}_${count}` : k;
688
+ });
689
+ }
690
+ function serverStateToBillingConfig(state, options) {
691
+ const dashboardPlans = [];
692
+ const activeProducts = state.products.filter((p) => p.isActive);
693
+ const configProducts = [];
694
+ for (const product of activeProducts) {
695
+ if (product.managedBy === "dashboard" && !(options == null ? void 0 : options.includeDashboard)) {
696
+ dashboardPlans.push({
697
+ name: product.name,
698
+ suggestedKey: slugify(product.name)
699
+ });
700
+ } else {
701
+ configProducts.push(product);
702
+ }
703
+ }
704
+ const rawKeys = configProducts.map((p) => {
705
+ var _a;
706
+ return (_a = p.configKey) != null ? _a : slugify(p.name);
707
+ });
708
+ const keys = deduplicateKeys(rawKeys);
709
+ const plans = {};
710
+ for (let i = 0; i < configProducts.length; i++) {
711
+ const product = configProducts[i];
712
+ const key = keys[i];
713
+ const prices = product.prices.filter((p) => p.isActive).map((p) => {
714
+ var _a;
715
+ const price = {
716
+ amount: (_a = p.amount) != null ? _a : 0,
717
+ currency: p.currency,
718
+ interval: p.interval
719
+ };
720
+ if (p.intervalCount > 1) price.intervalCount = p.intervalCount;
721
+ return price;
722
+ });
723
+ const plan = { name: product.name, prices };
724
+ if (product.description) plan.description = product.description;
725
+ if (product.features.length > 0) plan.features = product.features;
726
+ if (product.entitlements && Object.keys(product.entitlements).length > 0) {
727
+ plan.entitlements = product.entitlements;
728
+ }
729
+ plans[key] = plan;
730
+ }
731
+ return { config: { plans }, dashboardPlans };
732
+ }
733
+ function renderConfigAsTypeScript(result) {
734
+ var _a;
735
+ const lines = [
736
+ 'import { defineBilling } from "@auth-gate/billing";',
737
+ "",
738
+ "export default defineBilling({",
739
+ " plans: {"
740
+ ];
741
+ const planEntries = Object.entries(result.config.plans);
742
+ for (const [key, plan] of planEntries) {
743
+ lines.push(` ${key}: {`);
744
+ lines.push(` name: ${JSON.stringify(plan.name)},`);
745
+ if (plan.description)
746
+ lines.push(` description: ${JSON.stringify(plan.description)},`);
747
+ if ((_a = plan.features) == null ? void 0 : _a.length)
748
+ lines.push(` features: ${JSON.stringify(plan.features)},`);
749
+ lines.push(" prices: [");
750
+ for (const price of plan.prices) {
751
+ const parts = [];
752
+ if ("amount" in price)
753
+ parts.push(`amount: ${price.amount}`);
754
+ parts.push(
755
+ `currency: ${JSON.stringify(price.currency)}`,
756
+ `interval: ${JSON.stringify(price.interval)}`
757
+ );
758
+ if (price.intervalCount && price.intervalCount > 1)
759
+ parts.push(`intervalCount: ${price.intervalCount}`);
760
+ lines.push(` { ${parts.join(", ")} },`);
761
+ }
762
+ lines.push(" ],");
763
+ lines.push(" },");
764
+ }
765
+ lines.push(" },");
766
+ lines.push("});");
767
+ if (result.dashboardPlans.length > 0) {
768
+ lines.push("");
769
+ lines.push("// Dashboard-managed plans (not synced by config):");
770
+ for (const dp of result.dashboardPlans) {
771
+ lines.push(
772
+ `// ${dp.suggestedKey}: { name: ${JSON.stringify(dp.name)} }`
773
+ );
774
+ }
775
+ }
776
+ return lines.join("\n") + "\n";
777
+ }
778
+
779
+ // src/json-formatter.ts
780
+ function calculateRevenueImpact(diff, subscriberCounts) {
781
+ var _a, _b;
782
+ const breakdown = [];
783
+ const archivedPrices = diff.priceOps.filter((op) => op.type === "archive");
784
+ for (const archived of archivedPrices) {
785
+ if (archived.type !== "archive") continue;
786
+ const matching = diff.priceOps.find(
787
+ (op) => op.type === "create" && op.planKey === archived.planKey && op.price.interval === archived.existing.interval && op.price.currency === archived.existing.currency
788
+ );
789
+ const subs = (_a = subscriberCounts[archived.planKey]) != null ? _a : 0;
790
+ const oldAmount = (_b = archived.existing.amount) != null ? _b : 0;
791
+ const newAmount = (matching == null ? void 0 : matching.type) === "create" ? matching.price.amount : 0;
792
+ const normalizer = archived.existing.interval === "yearly" ? 12 : 1;
793
+ const delta = Math.round((newAmount - oldAmount) / normalizer * subs);
794
+ if (delta !== 0) {
795
+ breakdown.push({
796
+ planKey: archived.planKey,
797
+ oldAmount,
798
+ newAmount,
799
+ subscribers: subs,
800
+ monthlyDelta: delta
801
+ });
802
+ }
803
+ }
804
+ return {
805
+ monthlyDelta: breakdown.reduce((s, b) => s + b.monthlyDelta, 0),
806
+ affectedSubscribers: breakdown.reduce((s, b) => s + b.subscribers, 0),
807
+ breakdown
808
+ };
809
+ }
810
+ function mapPlanOp(op) {
811
+ const base = { type: op.type, key: op.key };
812
+ switch (op.type) {
813
+ case "create":
814
+ base.name = op.plan.name;
815
+ break;
816
+ case "update":
817
+ base.name = op.plan.name;
818
+ base.changes = op.changes;
819
+ break;
820
+ case "archive":
821
+ base.name = op.existing.name;
822
+ base.activeSubscribers = op.activeSubscribers;
823
+ break;
824
+ case "rename":
825
+ base.name = op.plan.name;
826
+ base.oldKey = op.oldKey;
827
+ base.activeSubscribers = op.activeSubscribers;
828
+ break;
829
+ }
830
+ return base;
831
+ }
832
+ function mapPriceOp(op) {
833
+ var _a;
834
+ const base = {
835
+ type: op.type,
836
+ planKey: op.planKey,
837
+ configKey: op.configKey
838
+ };
839
+ if (op.type === "create") {
840
+ base.amount = op.price.amount;
841
+ base.currency = op.price.currency;
842
+ base.interval = op.price.interval;
843
+ } else {
844
+ base.amount = (_a = op.existing.amount) != null ? _a : void 0;
845
+ base.currency = op.existing.currency;
846
+ base.interval = op.existing.interval;
847
+ }
848
+ return base;
849
+ }
850
+ function formatDiffAsJson(diff, subscriberCounts, dryRun) {
851
+ const planOps = diff.planOps.map(mapPlanOp);
852
+ const priceOps = diff.priceOps.map(mapPriceOp);
853
+ const revenueImpact = calculateRevenueImpact(diff, subscriberCounts);
854
+ const creates = diff.planOps.filter((o) => o.type === "create").length;
855
+ const updates = diff.planOps.filter((o) => o.type === "update").length;
856
+ const archives = diff.planOps.filter((o) => o.type === "archive").length;
857
+ const renames = diff.planOps.filter((o) => o.type === "rename").length;
858
+ let summary;
859
+ if (planOps.length === 0 && priceOps.length === 0) {
860
+ summary = "No changes detected.";
861
+ } else {
862
+ const parts = [];
863
+ if (creates > 0) parts.push(`${creates} create`);
864
+ if (updates > 0) parts.push(`${updates} update`);
865
+ if (renames > 0) parts.push(`${renames} rename`);
866
+ if (archives > 0) parts.push(`${archives} archive`);
867
+ summary = parts.join(", ");
868
+ if (revenueImpact.monthlyDelta !== 0) {
869
+ const sign = revenueImpact.monthlyDelta > 0 ? "+" : "";
870
+ summary += ` | Revenue impact: ${sign}$${(revenueImpact.monthlyDelta / 100).toFixed(2)}/mo`;
871
+ }
872
+ }
873
+ return {
874
+ planOps,
875
+ priceOps,
876
+ revenueImpact,
877
+ hasDestructive: diff.hasDestructive,
878
+ dryRun,
879
+ summary
880
+ };
881
+ }
882
+
883
+ // src/env.ts
884
+ var VALID_ENVIRONMENTS = [
885
+ "production",
886
+ "staging",
887
+ "preview",
888
+ "development",
889
+ "test"
890
+ ];
891
+ function resolveEnvironment(opts) {
892
+ var _a, _b;
893
+ const value = (_b = (_a = opts.env) != null ? _a : process.env.AUTHGATE_ENV) != null ? _b : "production";
894
+ if (!VALID_ENVIRONMENTS.includes(value)) {
895
+ throw new Error(
896
+ `Invalid environment "${value}". Must be one of: ${VALID_ENVIRONMENTS.join(", ")}`
897
+ );
898
+ }
899
+ return value;
900
+ }
901
+
902
+ // src/codegen.ts
903
+ function generateBillingConstants(config) {
904
+ const lines = [];
905
+ lines.push("// Auto-generated by @auth-gate/billing. Do not edit manually.");
906
+ lines.push("// Regenerate with: npx @auth-gate/billing generate");
907
+ lines.push("");
908
+ const planKeys = Object.keys(config.plans);
909
+ lines.push("export const Plans = {");
910
+ for (const key of planKeys) {
911
+ lines.push(` ${key}: ${JSON.stringify(key)},`);
912
+ }
913
+ lines.push("} as const;");
914
+ lines.push("");
915
+ lines.push("export type PlanKey = (typeof Plans)[keyof typeof Plans];");
916
+ lines.push("");
917
+ if (config.features && Object.keys(config.features).length > 0) {
918
+ const featureKeys = Object.keys(config.features);
919
+ lines.push("export const Features = {");
920
+ for (const key of featureKeys) {
921
+ lines.push(` ${key}: ${JSON.stringify(key)},`);
922
+ }
923
+ lines.push("} as const;");
924
+ lines.push("");
925
+ lines.push(
926
+ "export type FeatureKey = (typeof Features)[keyof typeof Features];"
927
+ );
928
+ lines.push("");
929
+ const meteredFeatures = featureKeys.filter(
930
+ (k) => config.features[k].type === "metered"
931
+ );
932
+ if (meteredFeatures.length > 0) {
933
+ lines.push("export const Limits = {");
934
+ for (const planKey of planKeys) {
935
+ const plan = config.plans[planKey];
936
+ const entitlements = plan.entitlements;
937
+ if (!entitlements) continue;
938
+ const meteredEntries = meteredFeatures.filter((f) => entitlements[f] && typeof entitlements[f] === "object").map((f) => {
939
+ const val = entitlements[f];
940
+ return ` ${f}: ${val.limit},`;
941
+ });
942
+ if (meteredEntries.length > 0) {
943
+ lines.push(` ${planKey}: {`);
944
+ for (const entry of meteredEntries) {
945
+ lines.push(entry);
946
+ }
947
+ lines.push(" },");
948
+ }
949
+ }
950
+ lines.push("} as const;");
951
+ lines.push("");
952
+ }
953
+ }
954
+ return lines.join("\n") + "\n";
955
+ }
956
+
957
+ // src/cli.ts
958
+ var import_chalk2 = __toESM(require("chalk"), 1);
959
+ var import_fs2 = require("fs");
960
+ var import_path2 = require("path");
961
+ var HELP = `
962
+ Usage: @auth-gate/billing <command> [options]
963
+
964
+ Commands:
965
+ sync Preview or apply billing config changes
966
+ pull Generate config from existing plans
967
+ migrate Execute a pending migration by ID
968
+ init Create a starter authgate.billing.ts config file
969
+ generate Generate typed constants from billing config
970
+ env list List available environments
971
+
972
+ Options (sync):
973
+ --apply Apply changes (default: dry-run only)
974
+ --force Allow archiving plans with active subscribers
975
+ --strict Treat warnings as errors (recommended for CI/CD)
976
+ --json Output diff as JSON (for CI/CD integrations)
977
+
978
+ Options (pull):
979
+ --from-stripe Pull from Stripe directly (requires STRIPE_SECRET_KEY)
980
+ --dry-run Preview generated config without writing
981
+ --include-dashboard Include dashboard-managed plans
982
+ --output <path> Output file path (default: authgate.billing.ts)
983
+
984
+ Options (migrate):
985
+ --id Migration ID to execute
986
+ <from> <to> Plan config keys to migrate between
987
+ --batch-size Number of subscribers per batch (default: 100)
988
+ --dry-run Preview migration without executing
989
+
990
+ Global Options:
991
+ --env <name> Target environment (default: production)
992
+
993
+ Environment:
994
+ AUTHGATE_API_KEY Your project API key (required)
995
+ AUTHGATE_BASE_URL AuthGate instance URL (required)
996
+ AUTHGATE_ENV Default environment (overridden by --env)
997
+ STRIPE_SECRET_KEY Stripe secret key (for --from-stripe)
998
+ `;
999
+ function parseEnvFlag(args) {
1000
+ const idx = args.indexOf("--env");
1001
+ return idx !== -1 ? args[idx + 1] : void 0;
1002
+ }
1003
+ async function main() {
1004
+ const args = process.argv.slice(2);
1005
+ const command = args[0];
1006
+ if (!command || command === "--help" || command === "-h") {
1007
+ console.log(HELP);
1008
+ process.exit(0);
1009
+ }
1010
+ if (command === "init") {
1011
+ await runInit();
1012
+ return;
1013
+ }
1014
+ if (command === "generate") {
1015
+ const outputIdx = args.indexOf("--output");
1016
+ const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : void 0;
1017
+ await runGenerate({ outputPath });
1018
+ return;
1019
+ }
1020
+ if (command === "env") {
1021
+ const subcommand = args[1];
1022
+ if (subcommand === "list") {
1023
+ console.log(import_chalk2.default.bold("Available environments:"));
1024
+ for (const env of VALID_ENVIRONMENTS) {
1025
+ const marker = env === "production" ? import_chalk2.default.dim(" (default)") : "";
1026
+ console.log(` ${env}${marker}`);
1027
+ }
1028
+ return;
1029
+ }
1030
+ console.error(import_chalk2.default.red(`Unknown env subcommand: ${subcommand != null ? subcommand : "(none)"}`));
1031
+ process.exit(1);
1032
+ }
1033
+ if (command === "pull") {
1034
+ const fromStripe = args.includes("--from-stripe");
1035
+ const dryRun = args.includes("--dry-run");
1036
+ const includeDashboard = args.includes("--include-dashboard");
1037
+ const outputIdx = args.indexOf("--output");
1038
+ const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : void 0;
1039
+ await runPull({ fromStripe, dryRun, includeDashboard, outputPath });
1040
+ return;
1041
+ }
1042
+ if (command === "sync") {
1043
+ const apply = args.includes("--apply");
1044
+ const force = args.includes("--force");
1045
+ const strict = args.includes("--strict");
1046
+ const json = args.includes("--json");
1047
+ const envFlag = parseEnvFlag(args);
1048
+ await runSync({ apply, force, strict, json, envFlag });
1049
+ return;
1050
+ }
1051
+ if (command === "migrate") {
1052
+ const idIndex = args.indexOf("--id");
1053
+ const migrationId = idIndex !== -1 ? args[idIndex + 1] : void 0;
1054
+ const batchIndex = args.indexOf("--batch-size");
1055
+ const batchSize = batchIndex !== -1 ? parseInt(args[batchIndex + 1], 10) : void 0;
1056
+ const dryRun = args.includes("--dry-run");
1057
+ const positionalArgs = args.slice(1).filter((a) => !a.startsWith("--") && args[args.indexOf(a) - 1] !== "--id" && args[args.indexOf(a) - 1] !== "--batch-size");
1058
+ const fromKey = positionalArgs[0];
1059
+ const toKey = positionalArgs[1];
1060
+ await runMigrate({ migrationId, fromKey, toKey, batchSize, dryRun });
1061
+ return;
1062
+ }
1063
+ console.error(import_chalk2.default.red(`Unknown command: ${command}`));
1064
+ console.log(HELP);
1065
+ process.exit(1);
1066
+ }
1067
+ async function runGenerate(opts) {
1068
+ var _a;
1069
+ console.log(import_chalk2.default.yellow(
1070
+ "Note: The `generate` command is deprecated. defineBilling() now provides end-to-end type inference \u2014 use createBillingHooks(billing) and createBillingHelpers({ billing }) instead.\n"
1071
+ ));
1072
+ let config;
1073
+ try {
1074
+ config = await loadConfig(process.cwd());
1075
+ } catch (err) {
1076
+ console.error(import_chalk2.default.red(`Config error: ${err.message}`));
1077
+ process.exit(1);
1078
+ }
1079
+ const output = generateBillingConstants(config);
1080
+ const outPath = (_a = opts.outputPath) != null ? _a : (0, import_path2.resolve)(process.cwd(), "authgate.billing.generated.ts");
1081
+ (0, import_fs2.writeFileSync)(outPath, output);
1082
+ console.log(import_chalk2.default.green(`Generated ${outPath}`));
1083
+ const planCount = Object.keys(config.plans).length;
1084
+ const featureCount = config.features ? Object.keys(config.features).length : 0;
1085
+ console.log(import_chalk2.default.dim(`${planCount} plans${featureCount > 0 ? `, ${featureCount} features` : ""}`));
1086
+ }
1087
+ async function runPull(opts) {
1088
+ var _a;
1089
+ let state;
1090
+ if (opts.fromStripe) {
1091
+ const stripeKey = process.env.STRIPE_SECRET_KEY;
1092
+ if (!stripeKey) {
1093
+ console.error(
1094
+ import_chalk2.default.red("STRIPE_SECRET_KEY is required for --from-stripe")
1095
+ );
1096
+ process.exit(1);
1097
+ }
1098
+ const { fetchStripeState: fetchStripeState2 } = await Promise.resolve().then(() => (init_pull_from_stripe(), pull_from_stripe_exports));
1099
+ state = await fetchStripeState2(stripeKey);
1100
+ } else {
1101
+ const apiKey = process.env.AUTHGATE_API_KEY;
1102
+ const baseUrl = process.env.AUTHGATE_BASE_URL;
1103
+ if (!apiKey || !baseUrl) {
1104
+ console.error(
1105
+ import_chalk2.default.red("AUTHGATE_API_KEY and AUTHGATE_BASE_URL required")
1106
+ );
1107
+ process.exit(1);
1108
+ }
1109
+ const environment = resolveEnvironment({});
1110
+ const client = new SyncClient({ baseUrl, apiKey, environment });
1111
+ state = await client.getState();
1112
+ }
1113
+ const result = serverStateToBillingConfig(state, {
1114
+ includeDashboard: opts.includeDashboard
1115
+ });
1116
+ const output = renderConfigAsTypeScript(result);
1117
+ const planCount = Object.keys(result.config.plans).length;
1118
+ const priceCount = Object.values(result.config.plans).reduce(
1119
+ (sum, p) => sum + p.prices.length,
1120
+ 0
1121
+ );
1122
+ if (opts.dryRun) {
1123
+ console.log(output);
1124
+ } else {
1125
+ const outPath = (_a = opts.outputPath) != null ? _a : (0, import_path2.resolve)(process.cwd(), "authgate.billing.ts");
1126
+ (0, import_fs2.writeFileSync)(outPath, output);
1127
+ console.log(import_chalk2.default.green(`Created ${outPath}`));
1128
+ }
1129
+ console.log(
1130
+ import_chalk2.default.dim(
1131
+ `${planCount} plans, ${priceCount} prices${result.dashboardPlans.length > 0 ? `, ${result.dashboardPlans.length} dashboard-only plans skipped` : ""}`
1132
+ )
1133
+ );
1134
+ }
1135
+ async function runInit() {
1136
+ const configPath = (0, import_path2.resolve)(process.cwd(), "authgate.billing.ts");
1137
+ const template = `import { defineBilling } from "@auth-gate/billing";
1138
+
1139
+ /**
1140
+ * Billing config \u2014 plan/feature keys provide full IDE autocomplete.
1141
+ *
1142
+ * Sync: npx @auth-gate/billing sync --apply
1143
+ */
1144
+ export const billing = defineBilling({
1145
+ plans: {
1146
+ starter: {
1147
+ name: "Starter",
1148
+ description: "For individuals and small projects",
1149
+ prices: [
1150
+ { amount: 999, currency: "usd", interval: "monthly" },
1151
+ { amount: 9999, currency: "usd", interval: "yearly" },
1152
+ ],
1153
+ },
1154
+ pro: {
1155
+ name: "Pro",
1156
+ description: "For growing teams",
1157
+ prices: [
1158
+ { amount: 2999, currency: "usd", interval: "monthly" },
1159
+ { amount: 29999, currency: "usd", interval: "yearly" },
1160
+ ],
1161
+ },
1162
+ },
1163
+ });
1164
+
1165
+ // Default export for CLI compatibility
1166
+ export default billing;
1167
+ `;
1168
+ (0, import_fs2.writeFileSync)(configPath, template, "utf-8");
1169
+ console.log(import_chalk2.default.green(`Created ${configPath}`));
1170
+ console.log(import_chalk2.default.dim("Edit your plans, then run: npx @auth-gate/billing sync"));
1171
+ }
1172
+ async function runSync(opts) {
1173
+ var _a;
1174
+ const apiKey = process.env.AUTHGATE_API_KEY;
1175
+ const baseUrl = process.env.AUTHGATE_BASE_URL;
1176
+ if (!apiKey) {
1177
+ console.error(import_chalk2.default.red("Missing AUTHGATE_API_KEY environment variable."));
1178
+ process.exit(1);
1179
+ }
1180
+ if (!baseUrl) {
1181
+ console.error(import_chalk2.default.red("Missing AUTHGATE_BASE_URL environment variable."));
1182
+ process.exit(1);
1183
+ }
1184
+ const environment = resolveEnvironment({ env: opts.envFlag });
1185
+ let config;
1186
+ try {
1187
+ config = await loadConfig(process.cwd());
1188
+ } catch (err) {
1189
+ console.error(import_chalk2.default.red(`Config error: ${err.message}`));
1190
+ process.exit(1);
1191
+ }
1192
+ const planCount = Object.keys(config.plans).length;
1193
+ const envLabel = environment !== "production" ? ` [${environment}]` : "";
1194
+ console.log(import_chalk2.default.dim(`Loaded config: ${planCount} plan${planCount > 1 ? "s" : ""}${envLabel}`));
1195
+ const client = new SyncClient({ baseUrl, apiKey, environment });
1196
+ let serverState;
1197
+ let subscriberCounts;
1198
+ try {
1199
+ [serverState, subscriberCounts] = await Promise.all([
1200
+ client.getState(),
1201
+ client.getSubscriberCounts()
1202
+ ]);
1203
+ } catch (err) {
1204
+ console.error(import_chalk2.default.red(`Failed to fetch server state: ${err.message}`));
1205
+ process.exit(1);
1206
+ }
1207
+ const diff = computeDiff(config, serverState, subscriberCounts);
1208
+ if (opts.json) {
1209
+ const jsonOutput = formatDiffAsJson(diff, subscriberCounts, !opts.apply);
1210
+ console.log(JSON.stringify(jsonOutput, null, 2));
1211
+ process.exit(diff.hasDestructive && opts.strict ? 1 : 0);
1212
+ }
1213
+ console.log("");
1214
+ console.log(formatDiff(diff, !opts.apply));
1215
+ if (!opts.apply) {
1216
+ if (opts.strict && diff.hasDestructive) {
1217
+ console.error(import_chalk2.default.red.bold("\n --strict: destructive changes detected. Failing."));
1218
+ process.exit(1);
1219
+ }
1220
+ process.exit(0);
1221
+ }
1222
+ if (diff.hasDestructive && !opts.force) {
1223
+ console.error(import_chalk2.default.red("\nDestructive changes require --force flag."));
1224
+ process.exit(1);
1225
+ }
1226
+ if (diff.planOps.length === 0 && diff.priceOps.length === 0) {
1227
+ process.exit(0);
1228
+ }
1229
+ try {
1230
+ console.log("");
1231
+ const result = await client.apply(config, opts.force);
1232
+ console.log(import_chalk2.default.green.bold("Sync complete!"));
1233
+ if (result.created.length) console.log(import_chalk2.default.green(` Created: ${result.created.join(", ")}`));
1234
+ if (result.updated.length) console.log(import_chalk2.default.yellow(` Updated: ${result.updated.join(", ")}`));
1235
+ if (result.renamed.length) console.log(import_chalk2.default.cyan(` Renamed: ${result.renamed.join(", ")}`));
1236
+ if (result.archived.length) console.log(import_chalk2.default.red(` Archived: ${result.archived.join(", ")}`));
1237
+ if (result.warnings.length) {
1238
+ for (const w of result.warnings) {
1239
+ console.log(import_chalk2.default.yellow(` Warning: ${w}`));
1240
+ }
1241
+ if (opts.strict) {
1242
+ console.error(import_chalk2.default.red.bold(`
1243
+ --strict: ${result.warnings.length} warning${result.warnings.length > 1 ? "s" : ""} treated as errors.`));
1244
+ process.exit(1);
1245
+ }
1246
+ }
1247
+ const ss = result.stripeSync;
1248
+ if (ss.productsCreated || ss.pricesCreated || ss.pricesArchived) {
1249
+ console.log(
1250
+ import_chalk2.default.dim(` Stripe: ${ss.productsCreated} products created, ${ss.pricesCreated} prices created, ${ss.pricesArchived} prices archived`)
1251
+ );
1252
+ }
1253
+ if ((_a = result.migrationIds) == null ? void 0 : _a.length) {
1254
+ for (const migId of result.migrationIds) {
1255
+ console.log(import_chalk2.default.dim(`
1256
+ Migration queued (ID: ${migId})`));
1257
+ console.log(import_chalk2.default.dim(" Executing migration..."));
1258
+ try {
1259
+ const migResult = await client.executeMigration(migId);
1260
+ console.log(import_chalk2.default.green(` Migrated: ${migResult.migrated} subscribers`));
1261
+ if (migResult.skipped > 0) console.log(import_chalk2.default.yellow(` Skipped: ${migResult.skipped}`));
1262
+ if (migResult.failed > 0) console.log(import_chalk2.default.red(` Failed: ${migResult.failed}`));
1263
+ } catch (err) {
1264
+ console.log(import_chalk2.default.yellow(` Migration execution deferred: ${err.message}`));
1265
+ console.log(import_chalk2.default.dim(` Run: npx @auth-gate/billing migrate --id ${migId}`));
1266
+ }
1267
+ }
1268
+ }
1269
+ } catch (err) {
1270
+ console.error(import_chalk2.default.red(`Sync failed: ${err.message}`));
1271
+ process.exit(1);
1272
+ }
1273
+ }
1274
+ async function runMigrate(opts) {
1275
+ if (!opts.migrationId && (!opts.fromKey || !opts.toKey)) {
1276
+ console.error(import_chalk2.default.red("Usage: migrate --id <migrationId> OR migrate <from> <to>"));
1277
+ process.exit(1);
1278
+ }
1279
+ const apiKey = process.env.AUTHGATE_API_KEY;
1280
+ const baseUrl = process.env.AUTHGATE_BASE_URL;
1281
+ if (!apiKey) {
1282
+ console.error(import_chalk2.default.red("Missing AUTHGATE_API_KEY environment variable."));
1283
+ process.exit(1);
1284
+ }
1285
+ if (!baseUrl) {
1286
+ console.error(import_chalk2.default.red("Missing AUTHGATE_BASE_URL environment variable."));
1287
+ process.exit(1);
1288
+ }
1289
+ const client = new SyncClient({ baseUrl, apiKey });
1290
+ let migrationId = opts.migrationId;
1291
+ if (!migrationId && opts.fromKey && opts.toKey) {
1292
+ console.log(import_chalk2.default.dim(`Creating migration: ${opts.fromKey} \u2192 ${opts.toKey}`));
1293
+ let createResult;
1294
+ try {
1295
+ createResult = await client.createMigration(opts.fromKey, opts.toKey);
1296
+ } catch (err) {
1297
+ console.error(import_chalk2.default.red(`Failed to create migration: ${err.message}`));
1298
+ process.exit(1);
1299
+ }
1300
+ migrationId = createResult.migrationId;
1301
+ console.log(import_chalk2.default.dim(`Migration ID: ${migrationId}`));
1302
+ console.log("");
1303
+ console.log(import_chalk2.default.bold(" Price mapping:"));
1304
+ for (const [oldKey, newKey] of Object.entries(createResult.priceMapping)) {
1305
+ console.log(import_chalk2.default.dim(` ${oldKey} \u2192 ${newKey}`));
1306
+ }
1307
+ console.log("");
1308
+ console.log(import_chalk2.default.dim(` Subscribers to migrate: ${createResult.totalSubscribers}`));
1309
+ if (createResult.pastDueSkipped > 0) {
1310
+ console.log(import_chalk2.default.yellow(` past_due skipped: ${createResult.pastDueSkipped}`));
1311
+ }
1312
+ if (createResult.unmappedSkipped > 0) {
1313
+ console.log(import_chalk2.default.yellow(` Unmapped (no price match): ${createResult.unmappedSkipped}`));
1314
+ }
1315
+ console.log("");
1316
+ if (opts.dryRun) {
1317
+ console.log(import_chalk2.default.dim(" Dry run \u2014 no changes applied. Run without --dry-run to execute."));
1318
+ process.exit(0);
1319
+ }
1320
+ }
1321
+ let migration;
1322
+ try {
1323
+ migration = await client.getMigration(migrationId);
1324
+ } catch (err) {
1325
+ console.error(import_chalk2.default.red(`Failed to fetch migration: ${err.message}`));
1326
+ process.exit(1);
1327
+ }
1328
+ console.log(import_chalk2.default.dim(`Migration: ${migration.fromConfigKey} \u2192 ${migration.toConfigKey}`));
1329
+ console.log(import_chalk2.default.dim(`Status: ${migration.status} | Total: ${migration.totalCount} | Migrated: ${migration.migratedCount} | Failed: ${migration.failedCount}`));
1330
+ if (opts.dryRun) {
1331
+ console.log("");
1332
+ console.log(import_chalk2.default.dim(" Dry run \u2014 no changes applied. Run without --dry-run to execute."));
1333
+ process.exit(0);
1334
+ }
1335
+ if (migration.status === "completed") {
1336
+ console.log(import_chalk2.default.green("Migration already completed."));
1337
+ process.exit(0);
1338
+ }
1339
+ if (migration.status === "running") {
1340
+ console.error(import_chalk2.default.yellow("Migration is already running. Check back later."));
1341
+ process.exit(1);
1342
+ }
1343
+ console.log("");
1344
+ console.log(import_chalk2.default.cyan("Executing migration..."));
1345
+ try {
1346
+ const result = await client.executeMigration(migrationId, {
1347
+ batchSize: opts.batchSize
1348
+ });
1349
+ console.log(import_chalk2.default.green.bold("Migration complete!"));
1350
+ console.log(import_chalk2.default.green(` Migrated: ${result.migrated}`));
1351
+ if (result.skipped > 0) {
1352
+ console.log(import_chalk2.default.yellow(` Skipped: ${result.skipped}`));
1353
+ }
1354
+ if (result.failed > 0) {
1355
+ console.log(import_chalk2.default.red(` Failed: ${result.failed}`));
1356
+ }
1357
+ } catch (err) {
1358
+ console.error(import_chalk2.default.red(`Migration failed: ${err.message}`));
1359
+ process.exit(1);
1360
+ }
1361
+ }
1362
+ main().catch((err) => {
1363
+ console.error(import_chalk2.default.red(err.message));
1364
+ process.exit(1);
1365
+ });