@auth-gate/billing 0.8.0 → 0.9.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.
@@ -13,8 +13,11 @@ function validateTiers(price, prefix) {
13
13
  }
14
14
  for (let j = 0; j < price.tiers.length; j++) {
15
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.`);
16
+ if (typeof tier.unitAmount !== "number" || !Number.isFinite(tier.unitAmount) || tier.unitAmount < 0) {
17
+ throw new Error(`${prefix}.tiers[${j}].unitAmount must be a finite non-negative number.`);
18
+ }
19
+ if (tier.flatAmount !== void 0 && (typeof tier.flatAmount !== "number" || !Number.isFinite(tier.flatAmount) || tier.flatAmount < 0)) {
20
+ throw new Error(`${prefix}.tiers[${j}].flatAmount must be a finite non-negative number.`);
18
21
  }
19
22
  }
20
23
  }
@@ -206,7 +209,7 @@ function normalizeConfig(config) {
206
209
  const plans = {};
207
210
  for (const [key, plan] of Object.entries(config.plans)) {
208
211
  if (plan.entitlements) {
209
- plans[key] = plan;
212
+ plans[key] = __spreadValues({}, plan);
210
213
  } else if (plan.features && plan.features.length > 0) {
211
214
  const entitlements = {};
212
215
  for (const f of plan.features) {
@@ -214,13 +217,28 @@ function normalizeConfig(config) {
214
217
  }
215
218
  plans[key] = __spreadProps(__spreadValues({}, plan), { entitlements });
216
219
  } else {
217
- plans[key] = plan;
220
+ plans[key] = __spreadValues({}, plan);
218
221
  }
219
222
  }
220
223
  return __spreadProps(__spreadValues({}, config), { plans });
221
224
  }
222
225
 
223
226
  // src/diff.ts
227
+ function deepEqual(a, b) {
228
+ if (a === b) return true;
229
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
230
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
231
+ if (Array.isArray(a) && Array.isArray(b)) {
232
+ if (a.length !== b.length) return false;
233
+ return a.every((val, i) => deepEqual(val, b[i]));
234
+ }
235
+ const keysA = Object.keys(a).sort();
236
+ const keysB = Object.keys(b).sort();
237
+ if (keysA.length !== keysB.length) return false;
238
+ return keysA.every(
239
+ (key, i) => key === keysB[i] && deepEqual(a[key], b[key])
240
+ );
241
+ }
224
242
  function priceConfigKey(planKey, price) {
225
243
  var _a, _b;
226
244
  const interval = price.interval;
@@ -242,6 +260,30 @@ function priceConfigKey(planKey, price) {
242
260
  const p = price;
243
261
  return `${planKey}_${interval}${suffix}_${p.amount}_${p.currency}`;
244
262
  }
263
+ function diffPrices(planKey, configPrices, serverPrices) {
264
+ const serverPricesByKey = /* @__PURE__ */ new Map();
265
+ for (const sp of serverPrices) {
266
+ if (sp.configKey && sp.isActive) {
267
+ serverPricesByKey.set(sp.configKey, sp);
268
+ }
269
+ }
270
+ const creates = [];
271
+ const archives = [];
272
+ const configPriceKeys = /* @__PURE__ */ new Set();
273
+ for (const price of configPrices) {
274
+ const pk = priceConfigKey(planKey, price);
275
+ configPriceKeys.add(pk);
276
+ if (!serverPricesByKey.has(pk)) {
277
+ creates.push({ type: "create", planKey, price, configKey: pk });
278
+ }
279
+ }
280
+ for (const [pk, sp] of serverPricesByKey) {
281
+ if (!configPriceKeys.has(pk)) {
282
+ archives.push({ type: "archive", planKey, existing: sp, configKey: pk });
283
+ }
284
+ }
285
+ return { creates, archives };
286
+ }
245
287
  function computeDiff(config, server, subscriberCounts) {
246
288
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
247
289
  const planOps = [];
@@ -270,25 +312,8 @@ function computeDiff(config, server, subscriberCounts) {
270
312
  if (existing) {
271
313
  const subs = (_c = subscriberCounts[existing.id]) != null ? _c : 0;
272
314
  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
- }
315
+ const priceDiff = diffPrices(key, plan.prices, existing.prices);
316
+ priceOps.push(...priceDiff.creates, ...priceDiff.archives);
292
317
  serverByKey.delete(existing.configKey);
293
318
  serverByKey.delete(key);
294
319
  continue;
@@ -313,30 +338,12 @@ function computeDiff(config, server, subscriberCounts) {
313
338
  const existingFeatures = ((_f = existing.features) != null ? _f : []).sort().join(",");
314
339
  const configFeatures = ((_g = plan.features) != null ? _g : []).sort().join(",");
315
340
  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
- }
341
+ if (!deepEqual((_h = existing.entitlements) != null ? _h : null, (_i = plan.entitlements) != null ? _i : null)) {
342
+ changes.push(`entitlements changed`);
339
343
  }
344
+ const priceDiff = diffPrices(key, plan.prices, existing.prices);
345
+ priceOps.push(...priceDiff.creates, ...priceDiff.archives);
346
+ const hasPriceArchives = priceDiff.archives.length > 0;
340
347
  if (changes.length > 0 || hasPriceArchives) {
341
348
  const versionBump = hasPriceArchives;
342
349
  if (versionBump) changes.push("prices changed");
package/dist/cli.cjs CHANGED
@@ -54,11 +54,11 @@ async function fetchStripeState(stripeKey) {
54
54
  const Stripe = (await import("stripe")).default;
55
55
  const stripe = new Stripe(stripeKey);
56
56
  const [products, prices] = await Promise.all([
57
- stripe.products.list({ active: true, limit: 100 }),
58
- stripe.prices.list({ active: true, limit: 100 })
57
+ stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 }),
58
+ stripe.prices.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 })
59
59
  ]);
60
60
  const pricesByProduct = /* @__PURE__ */ new Map();
61
- for (const sp of prices.data) {
61
+ for (const sp of prices) {
62
62
  const productId = typeof sp.product === "string" ? sp.product : sp.product.id;
63
63
  const mapped = {
64
64
  id: sp.id,
@@ -75,7 +75,7 @@ async function fetchStripeState(stripeKey) {
75
75
  mapped
76
76
  ]);
77
77
  }
78
- const serverProducts = products.data.map((p) => {
78
+ const serverProducts = products.map((p) => {
79
79
  var _a2;
80
80
  return {
81
81
  id: p.id,
@@ -112,8 +112,11 @@ function validateTiers(price, prefix) {
112
112
  }
113
113
  for (let j = 0; j < price.tiers.length; j++) {
114
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.`);
115
+ if (typeof tier.unitAmount !== "number" || !Number.isFinite(tier.unitAmount) || tier.unitAmount < 0) {
116
+ throw new Error(`${prefix}.tiers[${j}].unitAmount must be a finite non-negative number.`);
117
+ }
118
+ if (tier.flatAmount !== void 0 && (typeof tier.flatAmount !== "number" || !Number.isFinite(tier.flatAmount) || tier.flatAmount < 0)) {
119
+ throw new Error(`${prefix}.tiers[${j}].flatAmount must be a finite non-negative number.`);
117
120
  }
118
121
  }
119
122
  }
@@ -333,6 +336,21 @@ Run \`npx @auth-gate/billing init\` to create one.`
333
336
  }
334
337
 
335
338
  // src/diff.ts
339
+ function deepEqual(a, b) {
340
+ if (a === b) return true;
341
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
342
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
343
+ if (Array.isArray(a) && Array.isArray(b)) {
344
+ if (a.length !== b.length) return false;
345
+ return a.every((val, i) => deepEqual(val, b[i]));
346
+ }
347
+ const keysA = Object.keys(a).sort();
348
+ const keysB = Object.keys(b).sort();
349
+ if (keysA.length !== keysB.length) return false;
350
+ return keysA.every(
351
+ (key, i) => key === keysB[i] && deepEqual(a[key], b[key])
352
+ );
353
+ }
336
354
  function priceConfigKey(planKey, price) {
337
355
  var _a, _b;
338
356
  const interval = price.interval;
@@ -354,6 +372,30 @@ function priceConfigKey(planKey, price) {
354
372
  const p = price;
355
373
  return `${planKey}_${interval}${suffix}_${p.amount}_${p.currency}`;
356
374
  }
375
+ function diffPrices(planKey, configPrices, serverPrices) {
376
+ const serverPricesByKey = /* @__PURE__ */ new Map();
377
+ for (const sp of serverPrices) {
378
+ if (sp.configKey && sp.isActive) {
379
+ serverPricesByKey.set(sp.configKey, sp);
380
+ }
381
+ }
382
+ const creates = [];
383
+ const archives = [];
384
+ const configPriceKeys = /* @__PURE__ */ new Set();
385
+ for (const price of configPrices) {
386
+ const pk = priceConfigKey(planKey, price);
387
+ configPriceKeys.add(pk);
388
+ if (!serverPricesByKey.has(pk)) {
389
+ creates.push({ type: "create", planKey, price, configKey: pk });
390
+ }
391
+ }
392
+ for (const [pk, sp] of serverPricesByKey) {
393
+ if (!configPriceKeys.has(pk)) {
394
+ archives.push({ type: "archive", planKey, existing: sp, configKey: pk });
395
+ }
396
+ }
397
+ return { creates, archives };
398
+ }
357
399
  function computeDiff(config, server, subscriberCounts) {
358
400
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
359
401
  const planOps = [];
@@ -382,25 +424,8 @@ function computeDiff(config, server, subscriberCounts) {
382
424
  if (existing) {
383
425
  const subs = (_c = subscriberCounts[existing.id]) != null ? _c : 0;
384
426
  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
- }
427
+ const priceDiff = diffPrices(key, plan.prices, existing.prices);
428
+ priceOps.push(...priceDiff.creates, ...priceDiff.archives);
404
429
  serverByKey.delete(existing.configKey);
405
430
  serverByKey.delete(key);
406
431
  continue;
@@ -425,30 +450,12 @@ function computeDiff(config, server, subscriberCounts) {
425
450
  const existingFeatures = ((_f = existing.features) != null ? _f : []).sort().join(",");
426
451
  const configFeatures = ((_g = plan.features) != null ? _g : []).sort().join(",");
427
452
  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
- }
453
+ if (!deepEqual((_h = existing.entitlements) != null ? _h : null, (_i = plan.entitlements) != null ? _i : null)) {
454
+ changes.push(`entitlements changed`);
451
455
  }
456
+ const priceDiff = diffPrices(key, plan.prices, existing.prices);
457
+ priceOps.push(...priceDiff.creates, ...priceDiff.archives);
458
+ const hasPriceArchives = priceDiff.archives.length > 0;
452
459
  if (changes.length > 0 || hasPriceArchives) {
453
460
  const versionBump = hasPriceArchives;
454
461
  if (versionBump) changes.push("prices changed");
@@ -608,6 +615,11 @@ var SyncClient = class {
608
615
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
609
616
  this.apiKey = config.apiKey;
610
617
  this.environment = config.environment;
618
+ if (!this.baseUrl.startsWith("https://") && !this.baseUrl.startsWith("http://localhost") && !this.baseUrl.startsWith("http://127.0.0.1")) {
619
+ console.warn(
620
+ "WARNING: AUTHGATE_BASE_URL does not use HTTPS. API key may be transmitted insecurely."
621
+ );
622
+ }
611
623
  }
612
624
  async request(method, path, body) {
613
625
  var _a, _b;
@@ -629,9 +641,12 @@ var SyncClient = class {
629
641
  let message;
630
642
  try {
631
643
  const json = JSON.parse(text);
632
- message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : text;
644
+ message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : "Unknown error";
633
645
  } catch (e) {
634
- message = text;
646
+ message = text.length > 500 ? text.slice(0, 500) + "..." : text;
647
+ }
648
+ if (this.apiKey) {
649
+ message = message.replaceAll(this.apiKey, "[REDACTED]");
635
650
  }
636
651
  throw new Error(`API error (${res.status}): ${message}`);
637
652
  }
@@ -646,6 +661,7 @@ var SyncClient = class {
646
661
  async apply(config, force) {
647
662
  return this.request("POST", "/api/v1/billing/sync/apply", {
648
663
  plans: config.plans,
664
+ features: config.features,
649
665
  migrations: config.migrations,
650
666
  force
651
667
  });
@@ -653,13 +669,13 @@ var SyncClient = class {
653
669
  async getMigration(migrationId) {
654
670
  return this.request(
655
671
  "GET",
656
- `/api/v1/billing/migrations/${migrationId}`
672
+ `/api/v1/billing/migrations/${encodeURIComponent(migrationId)}`
657
673
  );
658
674
  }
659
675
  async executeMigration(migrationId, opts) {
660
676
  return this.request(
661
677
  "POST",
662
- `/api/v1/billing/migrations/${migrationId}/execute`,
678
+ `/api/v1/billing/migrations/${encodeURIComponent(migrationId)}/execute`,
663
679
  (opts == null ? void 0 : opts.batchSize) ? { batchSize: opts.batchSize } : void 0
664
680
  );
665
681
  }
@@ -781,14 +797,21 @@ function calculateRevenueImpact(diff, subscriberCounts) {
781
797
  var _a, _b;
782
798
  const breakdown = [];
783
799
  const archivedPrices = diff.priceOps.filter((op) => op.type === "archive");
800
+ const planKeyToProductId = /* @__PURE__ */ new Map();
801
+ for (const op of diff.planOps) {
802
+ if (op.type === "update" || op.type === "archive" || op.type === "rename") {
803
+ planKeyToProductId.set(op.key, op.existing.id);
804
+ }
805
+ }
784
806
  for (const archived of archivedPrices) {
785
807
  if (archived.type !== "archive") continue;
786
808
  const matching = diff.priceOps.find(
787
809
  (op) => op.type === "create" && op.planKey === archived.planKey && op.price.interval === archived.existing.interval && op.price.currency === archived.existing.currency
788
810
  );
789
- const subs = (_a = subscriberCounts[archived.planKey]) != null ? _a : 0;
811
+ const productId = planKeyToProductId.get(archived.planKey);
812
+ const subs = (_a = subscriberCounts[productId != null ? productId : archived.planKey]) != null ? _a : 0;
790
813
  const oldAmount = (_b = archived.existing.amount) != null ? _b : 0;
791
- const newAmount = (matching == null ? void 0 : matching.type) === "create" ? matching.price.amount : 0;
814
+ const newAmount = (matching == null ? void 0 : matching.type) === "create" && "amount" in matching.price ? matching.price.amount : 0;
792
815
  const normalizer = archived.existing.interval === "yearly" ? 12 : 1;
793
816
  const delta = Math.round((newAmount - oldAmount) / normalizer * subs);
794
817
  if (delta !== 0) {
@@ -837,7 +860,7 @@ function mapPriceOp(op) {
837
860
  configKey: op.configKey
838
861
  };
839
862
  if (op.type === "create") {
840
- base.amount = op.price.amount;
863
+ base.amount = "amount" in op.price ? op.price.amount : void 0;
841
864
  base.currency = op.price.currency;
842
865
  base.interval = op.price.interval;
843
866
  } else {
@@ -998,7 +1021,13 @@ Environment:
998
1021
  `;
999
1022
  function parseEnvFlag(args) {
1000
1023
  const idx = args.indexOf("--env");
1001
- return idx !== -1 ? args[idx + 1] : void 0;
1024
+ if (idx === -1) return void 0;
1025
+ const value = args[idx + 1];
1026
+ if (!value || value.startsWith("--")) {
1027
+ console.error(import_chalk2.default.red("--env requires a value (e.g., --env staging)."));
1028
+ process.exit(1);
1029
+ }
1030
+ return value;
1002
1031
  }
1003
1032
  async function main() {
1004
1033
  const args = process.argv.slice(2);
@@ -1054,7 +1083,16 @@ async function main() {
1054
1083
  const batchIndex = args.indexOf("--batch-size");
1055
1084
  const batchSize = batchIndex !== -1 ? parseInt(args[batchIndex + 1], 10) : void 0;
1056
1085
  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");
1086
+ const positionalArgs = [];
1087
+ const migrateArgs = args.slice(1);
1088
+ for (let i = 0; i < migrateArgs.length; i++) {
1089
+ const a = migrateArgs[i];
1090
+ if (a.startsWith("--")) {
1091
+ if (a === "--id" || a === "--batch-size") i++;
1092
+ continue;
1093
+ }
1094
+ positionalArgs.push(a);
1095
+ }
1058
1096
  const fromKey = positionalArgs[0];
1059
1097
  const toKey = positionalArgs[1];
1060
1098
  await runMigrate({ migrationId, fromKey, toKey, batchSize, dryRun });
package/dist/cli.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  renderConfigAsTypeScript,
5
5
  serverStateToBillingConfig,
6
6
  validateConfig
7
- } from "./chunk-AHCLNQ6P.mjs";
7
+ } from "./chunk-VLL3I4K4.mjs";
8
8
  import "./chunk-I4E63NIC.mjs";
9
9
 
10
10
  // src/config-loader.ts
@@ -168,6 +168,11 @@ var SyncClient = class {
168
168
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
169
169
  this.apiKey = config.apiKey;
170
170
  this.environment = config.environment;
171
+ if (!this.baseUrl.startsWith("https://") && !this.baseUrl.startsWith("http://localhost") && !this.baseUrl.startsWith("http://127.0.0.1")) {
172
+ console.warn(
173
+ "WARNING: AUTHGATE_BASE_URL does not use HTTPS. API key may be transmitted insecurely."
174
+ );
175
+ }
171
176
  }
172
177
  async request(method, path, body) {
173
178
  var _a, _b;
@@ -189,9 +194,12 @@ var SyncClient = class {
189
194
  let message;
190
195
  try {
191
196
  const json = JSON.parse(text);
192
- message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : text;
197
+ message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : "Unknown error";
193
198
  } catch (e) {
194
- message = text;
199
+ message = text.length > 500 ? text.slice(0, 500) + "..." : text;
200
+ }
201
+ if (this.apiKey) {
202
+ message = message.replaceAll(this.apiKey, "[REDACTED]");
195
203
  }
196
204
  throw new Error(`API error (${res.status}): ${message}`);
197
205
  }
@@ -206,6 +214,7 @@ var SyncClient = class {
206
214
  async apply(config, force) {
207
215
  return this.request("POST", "/api/v1/billing/sync/apply", {
208
216
  plans: config.plans,
217
+ features: config.features,
209
218
  migrations: config.migrations,
210
219
  force
211
220
  });
@@ -213,13 +222,13 @@ var SyncClient = class {
213
222
  async getMigration(migrationId) {
214
223
  return this.request(
215
224
  "GET",
216
- `/api/v1/billing/migrations/${migrationId}`
225
+ `/api/v1/billing/migrations/${encodeURIComponent(migrationId)}`
217
226
  );
218
227
  }
219
228
  async executeMigration(migrationId, opts) {
220
229
  return this.request(
221
230
  "POST",
222
- `/api/v1/billing/migrations/${migrationId}/execute`,
231
+ `/api/v1/billing/migrations/${encodeURIComponent(migrationId)}/execute`,
223
232
  (opts == null ? void 0 : opts.batchSize) ? { batchSize: opts.batchSize } : void 0
224
233
  );
225
234
  }
@@ -237,14 +246,21 @@ function calculateRevenueImpact(diff, subscriberCounts) {
237
246
  var _a, _b;
238
247
  const breakdown = [];
239
248
  const archivedPrices = diff.priceOps.filter((op) => op.type === "archive");
249
+ const planKeyToProductId = /* @__PURE__ */ new Map();
250
+ for (const op of diff.planOps) {
251
+ if (op.type === "update" || op.type === "archive" || op.type === "rename") {
252
+ planKeyToProductId.set(op.key, op.existing.id);
253
+ }
254
+ }
240
255
  for (const archived of archivedPrices) {
241
256
  if (archived.type !== "archive") continue;
242
257
  const matching = diff.priceOps.find(
243
258
  (op) => op.type === "create" && op.planKey === archived.planKey && op.price.interval === archived.existing.interval && op.price.currency === archived.existing.currency
244
259
  );
245
- const subs = (_a = subscriberCounts[archived.planKey]) != null ? _a : 0;
260
+ const productId = planKeyToProductId.get(archived.planKey);
261
+ const subs = (_a = subscriberCounts[productId != null ? productId : archived.planKey]) != null ? _a : 0;
246
262
  const oldAmount = (_b = archived.existing.amount) != null ? _b : 0;
247
- const newAmount = (matching == null ? void 0 : matching.type) === "create" ? matching.price.amount : 0;
263
+ const newAmount = (matching == null ? void 0 : matching.type) === "create" && "amount" in matching.price ? matching.price.amount : 0;
248
264
  const normalizer = archived.existing.interval === "yearly" ? 12 : 1;
249
265
  const delta = Math.round((newAmount - oldAmount) / normalizer * subs);
250
266
  if (delta !== 0) {
@@ -293,7 +309,7 @@ function mapPriceOp(op) {
293
309
  configKey: op.configKey
294
310
  };
295
311
  if (op.type === "create") {
296
- base.amount = op.price.amount;
312
+ base.amount = "amount" in op.price ? op.price.amount : void 0;
297
313
  base.currency = op.price.currency;
298
314
  base.interval = op.price.interval;
299
315
  } else {
@@ -454,7 +470,13 @@ Environment:
454
470
  `;
455
471
  function parseEnvFlag(args) {
456
472
  const idx = args.indexOf("--env");
457
- return idx !== -1 ? args[idx + 1] : void 0;
473
+ if (idx === -1) return void 0;
474
+ const value = args[idx + 1];
475
+ if (!value || value.startsWith("--")) {
476
+ console.error(chalk2.red("--env requires a value (e.g., --env staging)."));
477
+ process.exit(1);
478
+ }
479
+ return value;
458
480
  }
459
481
  async function main() {
460
482
  const args = process.argv.slice(2);
@@ -510,7 +532,16 @@ async function main() {
510
532
  const batchIndex = args.indexOf("--batch-size");
511
533
  const batchSize = batchIndex !== -1 ? parseInt(args[batchIndex + 1], 10) : void 0;
512
534
  const dryRun = args.includes("--dry-run");
513
- const positionalArgs = args.slice(1).filter((a) => !a.startsWith("--") && args[args.indexOf(a) - 1] !== "--id" && args[args.indexOf(a) - 1] !== "--batch-size");
535
+ const positionalArgs = [];
536
+ const migrateArgs = args.slice(1);
537
+ for (let i = 0; i < migrateArgs.length; i++) {
538
+ const a = migrateArgs[i];
539
+ if (a.startsWith("--")) {
540
+ if (a === "--id" || a === "--batch-size") i++;
541
+ continue;
542
+ }
543
+ positionalArgs.push(a);
544
+ }
514
545
  const fromKey = positionalArgs[0];
515
546
  const toKey = positionalArgs[1];
516
547
  await runMigrate({ migrationId, fromKey, toKey, batchSize, dryRun });
@@ -551,7 +582,7 @@ async function runPull(opts) {
551
582
  );
552
583
  process.exit(1);
553
584
  }
554
- const { fetchStripeState } = await import("./pull-from-stripe-GX2Y5XMC.mjs");
585
+ const { fetchStripeState } = await import("./pull-from-stripe-AAY6MWMX.mjs");
555
586
  state = await fetchStripeState(stripeKey);
556
587
  } else {
557
588
  const apiKey = process.env.AUTHGATE_API_KEY;
package/dist/index.cjs CHANGED
@@ -58,8 +58,11 @@ function validateTiers(price, prefix) {
58
58
  }
59
59
  for (let j = 0; j < price.tiers.length; j++) {
60
60
  const tier = price.tiers[j];
61
- if (typeof tier.unitAmount !== "number" || tier.unitAmount < 0) {
62
- throw new Error(`${prefix}.tiers[${j}].unitAmount must be a non-negative number.`);
61
+ if (typeof tier.unitAmount !== "number" || !Number.isFinite(tier.unitAmount) || tier.unitAmount < 0) {
62
+ throw new Error(`${prefix}.tiers[${j}].unitAmount must be a finite non-negative number.`);
63
+ }
64
+ if (tier.flatAmount !== void 0 && (typeof tier.flatAmount !== "number" || !Number.isFinite(tier.flatAmount) || tier.flatAmount < 0)) {
65
+ throw new Error(`${prefix}.tiers[${j}].flatAmount must be a finite non-negative number.`);
63
66
  }
64
67
  }
65
68
  }
@@ -251,7 +254,7 @@ function normalizeConfig(config) {
251
254
  const plans = {};
252
255
  for (const [key, plan] of Object.entries(config.plans)) {
253
256
  if (plan.entitlements) {
254
- plans[key] = plan;
257
+ plans[key] = __spreadValues({}, plan);
255
258
  } else if (plan.features && plan.features.length > 0) {
256
259
  const entitlements = {};
257
260
  for (const f of plan.features) {
@@ -259,13 +262,28 @@ function normalizeConfig(config) {
259
262
  }
260
263
  plans[key] = __spreadProps(__spreadValues({}, plan), { entitlements });
261
264
  } else {
262
- plans[key] = plan;
265
+ plans[key] = __spreadValues({}, plan);
263
266
  }
264
267
  }
265
268
  return __spreadProps(__spreadValues({}, config), { plans });
266
269
  }
267
270
 
268
271
  // src/diff.ts
272
+ function deepEqual(a, b) {
273
+ if (a === b) return true;
274
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
275
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
276
+ if (Array.isArray(a) && Array.isArray(b)) {
277
+ if (a.length !== b.length) return false;
278
+ return a.every((val, i) => deepEqual(val, b[i]));
279
+ }
280
+ const keysA = Object.keys(a).sort();
281
+ const keysB = Object.keys(b).sort();
282
+ if (keysA.length !== keysB.length) return false;
283
+ return keysA.every(
284
+ (key, i) => key === keysB[i] && deepEqual(a[key], b[key])
285
+ );
286
+ }
269
287
  function priceConfigKey(planKey, price) {
270
288
  var _a, _b;
271
289
  const interval = price.interval;
@@ -287,6 +305,30 @@ function priceConfigKey(planKey, price) {
287
305
  const p = price;
288
306
  return `${planKey}_${interval}${suffix}_${p.amount}_${p.currency}`;
289
307
  }
308
+ function diffPrices(planKey, configPrices, serverPrices) {
309
+ const serverPricesByKey = /* @__PURE__ */ new Map();
310
+ for (const sp of serverPrices) {
311
+ if (sp.configKey && sp.isActive) {
312
+ serverPricesByKey.set(sp.configKey, sp);
313
+ }
314
+ }
315
+ const creates = [];
316
+ const archives = [];
317
+ const configPriceKeys = /* @__PURE__ */ new Set();
318
+ for (const price of configPrices) {
319
+ const pk = priceConfigKey(planKey, price);
320
+ configPriceKeys.add(pk);
321
+ if (!serverPricesByKey.has(pk)) {
322
+ creates.push({ type: "create", planKey, price, configKey: pk });
323
+ }
324
+ }
325
+ for (const [pk, sp] of serverPricesByKey) {
326
+ if (!configPriceKeys.has(pk)) {
327
+ archives.push({ type: "archive", planKey, existing: sp, configKey: pk });
328
+ }
329
+ }
330
+ return { creates, archives };
331
+ }
290
332
  function computeDiff(config, server, subscriberCounts) {
291
333
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
292
334
  const planOps = [];
@@ -315,25 +357,8 @@ function computeDiff(config, server, subscriberCounts) {
315
357
  if (existing) {
316
358
  const subs = (_c = subscriberCounts[existing.id]) != null ? _c : 0;
317
359
  planOps.push({ type: "rename", key, oldKey: plan.renamedFrom, plan, existing, activeSubscribers: subs });
318
- const serverPricesByKey = /* @__PURE__ */ new Map();
319
- for (const sp of existing.prices) {
320
- if (sp.configKey && sp.isActive) {
321
- serverPricesByKey.set(sp.configKey, sp);
322
- }
323
- }
324
- const configPriceKeys = /* @__PURE__ */ new Set();
325
- for (const price of plan.prices) {
326
- const pk = priceConfigKey(key, price);
327
- configPriceKeys.add(pk);
328
- if (!serverPricesByKey.has(pk)) {
329
- priceOps.push({ type: "create", planKey: key, price, configKey: pk });
330
- }
331
- }
332
- for (const [pk, sp] of serverPricesByKey) {
333
- if (!configPriceKeys.has(pk)) {
334
- priceOps.push({ type: "archive", planKey: key, existing: sp, configKey: pk });
335
- }
336
- }
360
+ const priceDiff = diffPrices(key, plan.prices, existing.prices);
361
+ priceOps.push(...priceDiff.creates, ...priceDiff.archives);
337
362
  serverByKey.delete(existing.configKey);
338
363
  serverByKey.delete(key);
339
364
  continue;
@@ -358,30 +383,12 @@ function computeDiff(config, server, subscriberCounts) {
358
383
  const existingFeatures = ((_f = existing.features) != null ? _f : []).sort().join(",");
359
384
  const configFeatures = ((_g = plan.features) != null ? _g : []).sort().join(",");
360
385
  if (existingFeatures !== configFeatures) changes.push(`features changed`);
361
- const existingEntitlements = JSON.stringify((_h = existing.entitlements) != null ? _h : null);
362
- const configEntitlements = JSON.stringify((_i = plan.entitlements) != null ? _i : null);
363
- if (existingEntitlements !== configEntitlements) changes.push(`entitlements changed`);
364
- const serverPricesByKey = /* @__PURE__ */ new Map();
365
- for (const sp of existing.prices) {
366
- if (sp.configKey && sp.isActive) {
367
- serverPricesByKey.set(sp.configKey, sp);
368
- }
369
- }
370
- const configPriceKeys = /* @__PURE__ */ new Set();
371
- for (const price of plan.prices) {
372
- const pk = priceConfigKey(key, price);
373
- configPriceKeys.add(pk);
374
- if (!serverPricesByKey.has(pk)) {
375
- priceOps.push({ type: "create", planKey: key, price, configKey: pk });
376
- }
377
- }
378
- let hasPriceArchives = false;
379
- for (const [pk, sp] of serverPricesByKey) {
380
- if (!configPriceKeys.has(pk)) {
381
- priceOps.push({ type: "archive", planKey: key, existing: sp, configKey: pk });
382
- hasPriceArchives = true;
383
- }
386
+ if (!deepEqual((_h = existing.entitlements) != null ? _h : null, (_i = plan.entitlements) != null ? _i : null)) {
387
+ changes.push(`entitlements changed`);
384
388
  }
389
+ const priceDiff = diffPrices(key, plan.prices, existing.prices);
390
+ priceOps.push(...priceDiff.creates, ...priceDiff.archives);
391
+ const hasPriceArchives = priceDiff.archives.length > 0;
385
392
  if (changes.length > 0 || hasPriceArchives) {
386
393
  const versionBump = hasPriceArchives;
387
394
  if (versionBump) changes.push("prices changed");
package/dist/index.mjs CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  serverStateToBillingConfig,
7
7
  slugify,
8
8
  validateConfig
9
- } from "./chunk-AHCLNQ6P.mjs";
9
+ } from "./chunk-VLL3I4K4.mjs";
10
10
  import "./chunk-I4E63NIC.mjs";
11
11
 
12
12
  // src/index.ts
@@ -6,11 +6,11 @@ async function fetchStripeState(stripeKey) {
6
6
  const Stripe = (await import("stripe")).default;
7
7
  const stripe = new Stripe(stripeKey);
8
8
  const [products, prices] = await Promise.all([
9
- stripe.products.list({ active: true, limit: 100 }),
10
- stripe.prices.list({ active: true, limit: 100 })
9
+ stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 }),
10
+ stripe.prices.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 })
11
11
  ]);
12
12
  const pricesByProduct = /* @__PURE__ */ new Map();
13
- for (const sp of prices.data) {
13
+ for (const sp of prices) {
14
14
  const productId = typeof sp.product === "string" ? sp.product : sp.product.id;
15
15
  const mapped = {
16
16
  id: sp.id,
@@ -27,7 +27,7 @@ async function fetchStripeState(stripeKey) {
27
27
  mapped
28
28
  ]);
29
29
  }
30
- const serverProducts = products.data.map((p) => {
30
+ const serverProducts = products.map((p) => {
31
31
  var _a2;
32
32
  return {
33
33
  id: p.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auth-gate/billing",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {