@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/chunk-AHCLNQ6P.mjs +482 -0
- package/dist/chunk-I4E63NIC.mjs +24 -0
- package/dist/cli.cjs +1365 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +821 -0
- package/dist/index.cjs +557 -0
- package/dist/index.d.cts +302 -0
- package/dist/index.d.ts +302 -0
- package/dist/index.mjs +49 -0
- package/dist/pull-from-stripe-GX2Y5XMC.mjs +49 -0
- package/package.json +44 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
computeDiff,
|
|
4
|
+
renderConfigAsTypeScript,
|
|
5
|
+
serverStateToBillingConfig,
|
|
6
|
+
validateConfig
|
|
7
|
+
} from "./chunk-AHCLNQ6P.mjs";
|
|
8
|
+
import "./chunk-I4E63NIC.mjs";
|
|
9
|
+
|
|
10
|
+
// src/config-loader.ts
|
|
11
|
+
import { resolve } from "path";
|
|
12
|
+
import { existsSync } from "fs";
|
|
13
|
+
var CONFIG_FILENAMES = [
|
|
14
|
+
"authgate.billing.ts",
|
|
15
|
+
"authgate.billing.js",
|
|
16
|
+
"authgate.billing.mjs"
|
|
17
|
+
];
|
|
18
|
+
async function loadConfig(cwd) {
|
|
19
|
+
var _a, _b;
|
|
20
|
+
let configPath = null;
|
|
21
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
22
|
+
const candidate = resolve(cwd, filename);
|
|
23
|
+
if (existsSync(candidate)) {
|
|
24
|
+
configPath = candidate;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!configPath) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`No billing config found. Expected one of: ${CONFIG_FILENAMES.join(", ")}
|
|
31
|
+
Run \`npx @auth-gate/billing init\` to create one.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const { createJiti } = await import("jiti");
|
|
35
|
+
const jiti = createJiti(cwd, { interopDefault: true });
|
|
36
|
+
const mod = await jiti.import(configPath);
|
|
37
|
+
const raw = (_b = (_a = mod.default) != null ? _a : mod.billing) != null ? _b : mod;
|
|
38
|
+
const config = raw && typeof raw === "object" && "_config" in raw ? raw._config : raw;
|
|
39
|
+
return validateConfig(config);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/formatter.ts
|
|
43
|
+
import chalk from "chalk";
|
|
44
|
+
function formatAmount(cents, currency) {
|
|
45
|
+
const amount = (cents / 100).toFixed(2);
|
|
46
|
+
return `${currency.toUpperCase()} ${amount}`;
|
|
47
|
+
}
|
|
48
|
+
function formatInterval(interval, count) {
|
|
49
|
+
const c = count != null ? count : 1;
|
|
50
|
+
if (c === 1) return interval === "monthly" ? "/mo" : "/yr";
|
|
51
|
+
return `every ${c} ${interval.replace("ly", "s")}`;
|
|
52
|
+
}
|
|
53
|
+
function formatPrice(price) {
|
|
54
|
+
var _a;
|
|
55
|
+
const priceType = (_a = price.type) != null ? _a : "recurring";
|
|
56
|
+
const interval = formatInterval(price.interval, price.intervalCount);
|
|
57
|
+
if (priceType === "per_seat") {
|
|
58
|
+
const p2 = price;
|
|
59
|
+
return `per seat (${p2.metric}): ${formatAmount(p2.amount, p2.currency)}${interval}`;
|
|
60
|
+
}
|
|
61
|
+
if (priceType === "metered") {
|
|
62
|
+
const p2 = price;
|
|
63
|
+
return `metered (${p2.metric}): ${p2.tierMode} tiers ${p2.currency.toUpperCase()}${interval}`;
|
|
64
|
+
}
|
|
65
|
+
if (priceType === "tiered") {
|
|
66
|
+
const p2 = price;
|
|
67
|
+
return `tiered: ${p2.tierMode} tiers ${p2.currency.toUpperCase()}${interval}`;
|
|
68
|
+
}
|
|
69
|
+
const p = price;
|
|
70
|
+
return `${formatAmount(p.amount, p.currency)}${interval}`;
|
|
71
|
+
}
|
|
72
|
+
function formatPriceOp(op) {
|
|
73
|
+
if (op.type === "create") {
|
|
74
|
+
return formatPrice(op.price);
|
|
75
|
+
}
|
|
76
|
+
return op.configKey;
|
|
77
|
+
}
|
|
78
|
+
function formatDiff(diff, dryRun) {
|
|
79
|
+
var _a, _b;
|
|
80
|
+
const lines = [];
|
|
81
|
+
if (dryRun) {
|
|
82
|
+
lines.push(chalk.bold("AuthGate Billing Sync \u2014 DRY RUN") + chalk.dim(" (use --apply to execute)"));
|
|
83
|
+
} else {
|
|
84
|
+
lines.push(chalk.bold("AuthGate Billing Sync \u2014 APPLYING CHANGES"));
|
|
85
|
+
}
|
|
86
|
+
lines.push("");
|
|
87
|
+
if (diff.planOps.length === 0 && diff.priceOps.length === 0) {
|
|
88
|
+
lines.push(chalk.green(" Everything is in sync. No changes needed."));
|
|
89
|
+
return lines.join("\n");
|
|
90
|
+
}
|
|
91
|
+
for (const op of diff.planOps) {
|
|
92
|
+
if (op.type === "create") {
|
|
93
|
+
lines.push(chalk.green(` + CREATE plan "${op.key}"`));
|
|
94
|
+
if (op.plan.description) {
|
|
95
|
+
lines.push(chalk.dim(` ${op.plan.description}`));
|
|
96
|
+
}
|
|
97
|
+
if ((_a = op.plan.features) == null ? void 0 : _a.length) {
|
|
98
|
+
lines.push(chalk.dim(` features: ${op.plan.features.join(", ")}`));
|
|
99
|
+
}
|
|
100
|
+
if (op.plan.entitlements && Object.keys(op.plan.entitlements).length > 0) {
|
|
101
|
+
const entries = Object.entries(op.plan.entitlements).map(
|
|
102
|
+
([k, v]) => v === true ? k : `${k} (limit: ${v.limit})`
|
|
103
|
+
);
|
|
104
|
+
lines.push(chalk.dim(` entitlements: ${entries.join(", ")}`));
|
|
105
|
+
}
|
|
106
|
+
for (const price of op.plan.prices) {
|
|
107
|
+
lines.push(
|
|
108
|
+
chalk.green(` + price: ${formatPrice(price)}`)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
} else if (op.type === "update") {
|
|
112
|
+
let updateLabel = ` ~ UPDATE plan "${op.key}"`;
|
|
113
|
+
if (op.versionBump) {
|
|
114
|
+
const oldVersion = (_b = op.existing.version) != null ? _b : 1;
|
|
115
|
+
updateLabel += ` (v${oldVersion} \u2192 v${oldVersion + 1})`;
|
|
116
|
+
if (op.grandfathering) {
|
|
117
|
+
updateLabel += chalk.dim(` [${op.grandfathering}]`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
lines.push(chalk.yellow(updateLabel));
|
|
121
|
+
for (const change of op.changes) {
|
|
122
|
+
lines.push(chalk.yellow(` ${change}`));
|
|
123
|
+
}
|
|
124
|
+
} else if (op.type === "archive") {
|
|
125
|
+
lines.push(chalk.red(` - ARCHIVE plan "${op.key}"`));
|
|
126
|
+
if (op.activeSubscribers > 0) {
|
|
127
|
+
lines.push(
|
|
128
|
+
chalk.red.bold(` \u26A0 ${op.activeSubscribers} active subscriber${op.activeSubscribers > 1 ? "s" : ""} \u2014 they keep their current plan until cancellation`)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
} else if (op.type === "rename") {
|
|
132
|
+
lines.push(chalk.cyan(` ~ RENAME plan "${op.oldKey}" \u2192 "${op.key}"`));
|
|
133
|
+
if (op.activeSubscribers > 0) {
|
|
134
|
+
lines.push(chalk.dim(` ${op.activeSubscribers} subscriber${op.activeSubscribers > 1 ? "s" : ""} preserved`));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
lines.push("");
|
|
138
|
+
}
|
|
139
|
+
const standalonePriceOps = diff.priceOps.filter(
|
|
140
|
+
(op) => !diff.planOps.some((po) => po.type === "create" && po.key === op.planKey)
|
|
141
|
+
);
|
|
142
|
+
for (const op of standalonePriceOps) {
|
|
143
|
+
if (op.type === "create") {
|
|
144
|
+
lines.push(chalk.green(` + price on "${op.planKey}": ${formatPriceOp(op)}`));
|
|
145
|
+
} else if (op.type === "archive") {
|
|
146
|
+
lines.push(chalk.red(` - archive price on "${op.planKey}" (${op.configKey})`));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const creates = diff.planOps.filter((o) => o.type === "create").length;
|
|
150
|
+
const updates = diff.planOps.filter((o) => o.type === "update").length;
|
|
151
|
+
const archives = diff.planOps.filter((o) => o.type === "archive").length;
|
|
152
|
+
const renames = diff.planOps.filter((o) => o.type === "rename").length;
|
|
153
|
+
lines.push("");
|
|
154
|
+
lines.push(
|
|
155
|
+
chalk.dim(` Summary: ${creates} create, ${updates} update, ${renames} rename, ${archives} archive.`) + (dryRun ? chalk.dim(" Run with --apply to execute.") : "")
|
|
156
|
+
);
|
|
157
|
+
if (diff.hasDestructive && dryRun) {
|
|
158
|
+
lines.push(
|
|
159
|
+
chalk.red.bold("\n \u26A0 Destructive changes detected (plans with active subscribers).") + chalk.red(" Use --apply --force to proceed.")
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/sync.ts
|
|
166
|
+
var SyncClient = class {
|
|
167
|
+
constructor(config) {
|
|
168
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
169
|
+
this.apiKey = config.apiKey;
|
|
170
|
+
this.environment = config.environment;
|
|
171
|
+
}
|
|
172
|
+
async request(method, path, body) {
|
|
173
|
+
var _a, _b;
|
|
174
|
+
const url = `${this.baseUrl}${path}`;
|
|
175
|
+
const headers = {
|
|
176
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
177
|
+
"Content-Type": "application/json"
|
|
178
|
+
};
|
|
179
|
+
if (this.environment) {
|
|
180
|
+
headers["X-AuthGate-Environment"] = this.environment;
|
|
181
|
+
}
|
|
182
|
+
const res = await fetch(url, {
|
|
183
|
+
method,
|
|
184
|
+
headers,
|
|
185
|
+
body: body ? JSON.stringify(body) : void 0
|
|
186
|
+
});
|
|
187
|
+
if (!res.ok) {
|
|
188
|
+
const text = await res.text();
|
|
189
|
+
let message;
|
|
190
|
+
try {
|
|
191
|
+
const json = JSON.parse(text);
|
|
192
|
+
message = (_b = (_a = json.error) != null ? _a : json.message) != null ? _b : text;
|
|
193
|
+
} catch (e) {
|
|
194
|
+
message = text;
|
|
195
|
+
}
|
|
196
|
+
throw new Error(`API error (${res.status}): ${message}`);
|
|
197
|
+
}
|
|
198
|
+
return res.json();
|
|
199
|
+
}
|
|
200
|
+
async getState() {
|
|
201
|
+
return this.request("GET", "/api/v1/billing/sync/state");
|
|
202
|
+
}
|
|
203
|
+
async getSubscriberCounts() {
|
|
204
|
+
return this.request("GET", "/api/v1/billing/sync/subscribers");
|
|
205
|
+
}
|
|
206
|
+
async apply(config, force) {
|
|
207
|
+
return this.request("POST", "/api/v1/billing/sync/apply", {
|
|
208
|
+
plans: config.plans,
|
|
209
|
+
migrations: config.migrations,
|
|
210
|
+
force
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
async getMigration(migrationId) {
|
|
214
|
+
return this.request(
|
|
215
|
+
"GET",
|
|
216
|
+
`/api/v1/billing/migrations/${migrationId}`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
async executeMigration(migrationId, opts) {
|
|
220
|
+
return this.request(
|
|
221
|
+
"POST",
|
|
222
|
+
`/api/v1/billing/migrations/${migrationId}/execute`,
|
|
223
|
+
(opts == null ? void 0 : opts.batchSize) ? { batchSize: opts.batchSize } : void 0
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
async createMigration(from, to, opts) {
|
|
227
|
+
return this.request(
|
|
228
|
+
"POST",
|
|
229
|
+
"/api/v1/billing/migrations",
|
|
230
|
+
{ from, to, priceMapping: opts == null ? void 0 : opts.priceMapping }
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// src/json-formatter.ts
|
|
236
|
+
function calculateRevenueImpact(diff, subscriberCounts) {
|
|
237
|
+
var _a, _b;
|
|
238
|
+
const breakdown = [];
|
|
239
|
+
const archivedPrices = diff.priceOps.filter((op) => op.type === "archive");
|
|
240
|
+
for (const archived of archivedPrices) {
|
|
241
|
+
if (archived.type !== "archive") continue;
|
|
242
|
+
const matching = diff.priceOps.find(
|
|
243
|
+
(op) => op.type === "create" && op.planKey === archived.planKey && op.price.interval === archived.existing.interval && op.price.currency === archived.existing.currency
|
|
244
|
+
);
|
|
245
|
+
const subs = (_a = subscriberCounts[archived.planKey]) != null ? _a : 0;
|
|
246
|
+
const oldAmount = (_b = archived.existing.amount) != null ? _b : 0;
|
|
247
|
+
const newAmount = (matching == null ? void 0 : matching.type) === "create" ? matching.price.amount : 0;
|
|
248
|
+
const normalizer = archived.existing.interval === "yearly" ? 12 : 1;
|
|
249
|
+
const delta = Math.round((newAmount - oldAmount) / normalizer * subs);
|
|
250
|
+
if (delta !== 0) {
|
|
251
|
+
breakdown.push({
|
|
252
|
+
planKey: archived.planKey,
|
|
253
|
+
oldAmount,
|
|
254
|
+
newAmount,
|
|
255
|
+
subscribers: subs,
|
|
256
|
+
monthlyDelta: delta
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
monthlyDelta: breakdown.reduce((s, b) => s + b.monthlyDelta, 0),
|
|
262
|
+
affectedSubscribers: breakdown.reduce((s, b) => s + b.subscribers, 0),
|
|
263
|
+
breakdown
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function mapPlanOp(op) {
|
|
267
|
+
const base = { type: op.type, key: op.key };
|
|
268
|
+
switch (op.type) {
|
|
269
|
+
case "create":
|
|
270
|
+
base.name = op.plan.name;
|
|
271
|
+
break;
|
|
272
|
+
case "update":
|
|
273
|
+
base.name = op.plan.name;
|
|
274
|
+
base.changes = op.changes;
|
|
275
|
+
break;
|
|
276
|
+
case "archive":
|
|
277
|
+
base.name = op.existing.name;
|
|
278
|
+
base.activeSubscribers = op.activeSubscribers;
|
|
279
|
+
break;
|
|
280
|
+
case "rename":
|
|
281
|
+
base.name = op.plan.name;
|
|
282
|
+
base.oldKey = op.oldKey;
|
|
283
|
+
base.activeSubscribers = op.activeSubscribers;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
return base;
|
|
287
|
+
}
|
|
288
|
+
function mapPriceOp(op) {
|
|
289
|
+
var _a;
|
|
290
|
+
const base = {
|
|
291
|
+
type: op.type,
|
|
292
|
+
planKey: op.planKey,
|
|
293
|
+
configKey: op.configKey
|
|
294
|
+
};
|
|
295
|
+
if (op.type === "create") {
|
|
296
|
+
base.amount = op.price.amount;
|
|
297
|
+
base.currency = op.price.currency;
|
|
298
|
+
base.interval = op.price.interval;
|
|
299
|
+
} else {
|
|
300
|
+
base.amount = (_a = op.existing.amount) != null ? _a : void 0;
|
|
301
|
+
base.currency = op.existing.currency;
|
|
302
|
+
base.interval = op.existing.interval;
|
|
303
|
+
}
|
|
304
|
+
return base;
|
|
305
|
+
}
|
|
306
|
+
function formatDiffAsJson(diff, subscriberCounts, dryRun) {
|
|
307
|
+
const planOps = diff.planOps.map(mapPlanOp);
|
|
308
|
+
const priceOps = diff.priceOps.map(mapPriceOp);
|
|
309
|
+
const revenueImpact = calculateRevenueImpact(diff, subscriberCounts);
|
|
310
|
+
const creates = diff.planOps.filter((o) => o.type === "create").length;
|
|
311
|
+
const updates = diff.planOps.filter((o) => o.type === "update").length;
|
|
312
|
+
const archives = diff.planOps.filter((o) => o.type === "archive").length;
|
|
313
|
+
const renames = diff.planOps.filter((o) => o.type === "rename").length;
|
|
314
|
+
let summary;
|
|
315
|
+
if (planOps.length === 0 && priceOps.length === 0) {
|
|
316
|
+
summary = "No changes detected.";
|
|
317
|
+
} else {
|
|
318
|
+
const parts = [];
|
|
319
|
+
if (creates > 0) parts.push(`${creates} create`);
|
|
320
|
+
if (updates > 0) parts.push(`${updates} update`);
|
|
321
|
+
if (renames > 0) parts.push(`${renames} rename`);
|
|
322
|
+
if (archives > 0) parts.push(`${archives} archive`);
|
|
323
|
+
summary = parts.join(", ");
|
|
324
|
+
if (revenueImpact.monthlyDelta !== 0) {
|
|
325
|
+
const sign = revenueImpact.monthlyDelta > 0 ? "+" : "";
|
|
326
|
+
summary += ` | Revenue impact: ${sign}$${(revenueImpact.monthlyDelta / 100).toFixed(2)}/mo`;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
planOps,
|
|
331
|
+
priceOps,
|
|
332
|
+
revenueImpact,
|
|
333
|
+
hasDestructive: diff.hasDestructive,
|
|
334
|
+
dryRun,
|
|
335
|
+
summary
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/env.ts
|
|
340
|
+
var VALID_ENVIRONMENTS = [
|
|
341
|
+
"production",
|
|
342
|
+
"staging",
|
|
343
|
+
"preview",
|
|
344
|
+
"development",
|
|
345
|
+
"test"
|
|
346
|
+
];
|
|
347
|
+
function resolveEnvironment(opts) {
|
|
348
|
+
var _a, _b;
|
|
349
|
+
const value = (_b = (_a = opts.env) != null ? _a : process.env.AUTHGATE_ENV) != null ? _b : "production";
|
|
350
|
+
if (!VALID_ENVIRONMENTS.includes(value)) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
`Invalid environment "${value}". Must be one of: ${VALID_ENVIRONMENTS.join(", ")}`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
return value;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/codegen.ts
|
|
359
|
+
function generateBillingConstants(config) {
|
|
360
|
+
const lines = [];
|
|
361
|
+
lines.push("// Auto-generated by @auth-gate/billing. Do not edit manually.");
|
|
362
|
+
lines.push("// Regenerate with: npx @auth-gate/billing generate");
|
|
363
|
+
lines.push("");
|
|
364
|
+
const planKeys = Object.keys(config.plans);
|
|
365
|
+
lines.push("export const Plans = {");
|
|
366
|
+
for (const key of planKeys) {
|
|
367
|
+
lines.push(` ${key}: ${JSON.stringify(key)},`);
|
|
368
|
+
}
|
|
369
|
+
lines.push("} as const;");
|
|
370
|
+
lines.push("");
|
|
371
|
+
lines.push("export type PlanKey = (typeof Plans)[keyof typeof Plans];");
|
|
372
|
+
lines.push("");
|
|
373
|
+
if (config.features && Object.keys(config.features).length > 0) {
|
|
374
|
+
const featureKeys = Object.keys(config.features);
|
|
375
|
+
lines.push("export const Features = {");
|
|
376
|
+
for (const key of featureKeys) {
|
|
377
|
+
lines.push(` ${key}: ${JSON.stringify(key)},`);
|
|
378
|
+
}
|
|
379
|
+
lines.push("} as const;");
|
|
380
|
+
lines.push("");
|
|
381
|
+
lines.push(
|
|
382
|
+
"export type FeatureKey = (typeof Features)[keyof typeof Features];"
|
|
383
|
+
);
|
|
384
|
+
lines.push("");
|
|
385
|
+
const meteredFeatures = featureKeys.filter(
|
|
386
|
+
(k) => config.features[k].type === "metered"
|
|
387
|
+
);
|
|
388
|
+
if (meteredFeatures.length > 0) {
|
|
389
|
+
lines.push("export const Limits = {");
|
|
390
|
+
for (const planKey of planKeys) {
|
|
391
|
+
const plan = config.plans[planKey];
|
|
392
|
+
const entitlements = plan.entitlements;
|
|
393
|
+
if (!entitlements) continue;
|
|
394
|
+
const meteredEntries = meteredFeatures.filter((f) => entitlements[f] && typeof entitlements[f] === "object").map((f) => {
|
|
395
|
+
const val = entitlements[f];
|
|
396
|
+
return ` ${f}: ${val.limit},`;
|
|
397
|
+
});
|
|
398
|
+
if (meteredEntries.length > 0) {
|
|
399
|
+
lines.push(` ${planKey}: {`);
|
|
400
|
+
for (const entry of meteredEntries) {
|
|
401
|
+
lines.push(entry);
|
|
402
|
+
}
|
|
403
|
+
lines.push(" },");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
lines.push("} as const;");
|
|
407
|
+
lines.push("");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return lines.join("\n") + "\n";
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/cli.ts
|
|
414
|
+
import chalk2 from "chalk";
|
|
415
|
+
import { writeFileSync } from "fs";
|
|
416
|
+
import { resolve as resolve2 } from "path";
|
|
417
|
+
var HELP = `
|
|
418
|
+
Usage: @auth-gate/billing <command> [options]
|
|
419
|
+
|
|
420
|
+
Commands:
|
|
421
|
+
sync Preview or apply billing config changes
|
|
422
|
+
pull Generate config from existing plans
|
|
423
|
+
migrate Execute a pending migration by ID
|
|
424
|
+
init Create a starter authgate.billing.ts config file
|
|
425
|
+
generate Generate typed constants from billing config
|
|
426
|
+
env list List available environments
|
|
427
|
+
|
|
428
|
+
Options (sync):
|
|
429
|
+
--apply Apply changes (default: dry-run only)
|
|
430
|
+
--force Allow archiving plans with active subscribers
|
|
431
|
+
--strict Treat warnings as errors (recommended for CI/CD)
|
|
432
|
+
--json Output diff as JSON (for CI/CD integrations)
|
|
433
|
+
|
|
434
|
+
Options (pull):
|
|
435
|
+
--from-stripe Pull from Stripe directly (requires STRIPE_SECRET_KEY)
|
|
436
|
+
--dry-run Preview generated config without writing
|
|
437
|
+
--include-dashboard Include dashboard-managed plans
|
|
438
|
+
--output <path> Output file path (default: authgate.billing.ts)
|
|
439
|
+
|
|
440
|
+
Options (migrate):
|
|
441
|
+
--id Migration ID to execute
|
|
442
|
+
<from> <to> Plan config keys to migrate between
|
|
443
|
+
--batch-size Number of subscribers per batch (default: 100)
|
|
444
|
+
--dry-run Preview migration without executing
|
|
445
|
+
|
|
446
|
+
Global Options:
|
|
447
|
+
--env <name> Target environment (default: production)
|
|
448
|
+
|
|
449
|
+
Environment:
|
|
450
|
+
AUTHGATE_API_KEY Your project API key (required)
|
|
451
|
+
AUTHGATE_BASE_URL AuthGate instance URL (required)
|
|
452
|
+
AUTHGATE_ENV Default environment (overridden by --env)
|
|
453
|
+
STRIPE_SECRET_KEY Stripe secret key (for --from-stripe)
|
|
454
|
+
`;
|
|
455
|
+
function parseEnvFlag(args) {
|
|
456
|
+
const idx = args.indexOf("--env");
|
|
457
|
+
return idx !== -1 ? args[idx + 1] : void 0;
|
|
458
|
+
}
|
|
459
|
+
async function main() {
|
|
460
|
+
const args = process.argv.slice(2);
|
|
461
|
+
const command = args[0];
|
|
462
|
+
if (!command || command === "--help" || command === "-h") {
|
|
463
|
+
console.log(HELP);
|
|
464
|
+
process.exit(0);
|
|
465
|
+
}
|
|
466
|
+
if (command === "init") {
|
|
467
|
+
await runInit();
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (command === "generate") {
|
|
471
|
+
const outputIdx = args.indexOf("--output");
|
|
472
|
+
const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : void 0;
|
|
473
|
+
await runGenerate({ outputPath });
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (command === "env") {
|
|
477
|
+
const subcommand = args[1];
|
|
478
|
+
if (subcommand === "list") {
|
|
479
|
+
console.log(chalk2.bold("Available environments:"));
|
|
480
|
+
for (const env of VALID_ENVIRONMENTS) {
|
|
481
|
+
const marker = env === "production" ? chalk2.dim(" (default)") : "";
|
|
482
|
+
console.log(` ${env}${marker}`);
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
console.error(chalk2.red(`Unknown env subcommand: ${subcommand != null ? subcommand : "(none)"}`));
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
if (command === "pull") {
|
|
490
|
+
const fromStripe = args.includes("--from-stripe");
|
|
491
|
+
const dryRun = args.includes("--dry-run");
|
|
492
|
+
const includeDashboard = args.includes("--include-dashboard");
|
|
493
|
+
const outputIdx = args.indexOf("--output");
|
|
494
|
+
const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : void 0;
|
|
495
|
+
await runPull({ fromStripe, dryRun, includeDashboard, outputPath });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (command === "sync") {
|
|
499
|
+
const apply = args.includes("--apply");
|
|
500
|
+
const force = args.includes("--force");
|
|
501
|
+
const strict = args.includes("--strict");
|
|
502
|
+
const json = args.includes("--json");
|
|
503
|
+
const envFlag = parseEnvFlag(args);
|
|
504
|
+
await runSync({ apply, force, strict, json, envFlag });
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (command === "migrate") {
|
|
508
|
+
const idIndex = args.indexOf("--id");
|
|
509
|
+
const migrationId = idIndex !== -1 ? args[idIndex + 1] : void 0;
|
|
510
|
+
const batchIndex = args.indexOf("--batch-size");
|
|
511
|
+
const batchSize = batchIndex !== -1 ? parseInt(args[batchIndex + 1], 10) : void 0;
|
|
512
|
+
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");
|
|
514
|
+
const fromKey = positionalArgs[0];
|
|
515
|
+
const toKey = positionalArgs[1];
|
|
516
|
+
await runMigrate({ migrationId, fromKey, toKey, batchSize, dryRun });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
console.error(chalk2.red(`Unknown command: ${command}`));
|
|
520
|
+
console.log(HELP);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
async function runGenerate(opts) {
|
|
524
|
+
var _a;
|
|
525
|
+
console.log(chalk2.yellow(
|
|
526
|
+
"Note: The `generate` command is deprecated. defineBilling() now provides end-to-end type inference \u2014 use createBillingHooks(billing) and createBillingHelpers({ billing }) instead.\n"
|
|
527
|
+
));
|
|
528
|
+
let config;
|
|
529
|
+
try {
|
|
530
|
+
config = await loadConfig(process.cwd());
|
|
531
|
+
} catch (err) {
|
|
532
|
+
console.error(chalk2.red(`Config error: ${err.message}`));
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
const output = generateBillingConstants(config);
|
|
536
|
+
const outPath = (_a = opts.outputPath) != null ? _a : resolve2(process.cwd(), "authgate.billing.generated.ts");
|
|
537
|
+
writeFileSync(outPath, output);
|
|
538
|
+
console.log(chalk2.green(`Generated ${outPath}`));
|
|
539
|
+
const planCount = Object.keys(config.plans).length;
|
|
540
|
+
const featureCount = config.features ? Object.keys(config.features).length : 0;
|
|
541
|
+
console.log(chalk2.dim(`${planCount} plans${featureCount > 0 ? `, ${featureCount} features` : ""}`));
|
|
542
|
+
}
|
|
543
|
+
async function runPull(opts) {
|
|
544
|
+
var _a;
|
|
545
|
+
let state;
|
|
546
|
+
if (opts.fromStripe) {
|
|
547
|
+
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
|
548
|
+
if (!stripeKey) {
|
|
549
|
+
console.error(
|
|
550
|
+
chalk2.red("STRIPE_SECRET_KEY is required for --from-stripe")
|
|
551
|
+
);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
const { fetchStripeState } = await import("./pull-from-stripe-GX2Y5XMC.mjs");
|
|
555
|
+
state = await fetchStripeState(stripeKey);
|
|
556
|
+
} else {
|
|
557
|
+
const apiKey = process.env.AUTHGATE_API_KEY;
|
|
558
|
+
const baseUrl = process.env.AUTHGATE_BASE_URL;
|
|
559
|
+
if (!apiKey || !baseUrl) {
|
|
560
|
+
console.error(
|
|
561
|
+
chalk2.red("AUTHGATE_API_KEY and AUTHGATE_BASE_URL required")
|
|
562
|
+
);
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
const environment = resolveEnvironment({});
|
|
566
|
+
const client = new SyncClient({ baseUrl, apiKey, environment });
|
|
567
|
+
state = await client.getState();
|
|
568
|
+
}
|
|
569
|
+
const result = serverStateToBillingConfig(state, {
|
|
570
|
+
includeDashboard: opts.includeDashboard
|
|
571
|
+
});
|
|
572
|
+
const output = renderConfigAsTypeScript(result);
|
|
573
|
+
const planCount = Object.keys(result.config.plans).length;
|
|
574
|
+
const priceCount = Object.values(result.config.plans).reduce(
|
|
575
|
+
(sum, p) => sum + p.prices.length,
|
|
576
|
+
0
|
|
577
|
+
);
|
|
578
|
+
if (opts.dryRun) {
|
|
579
|
+
console.log(output);
|
|
580
|
+
} else {
|
|
581
|
+
const outPath = (_a = opts.outputPath) != null ? _a : resolve2(process.cwd(), "authgate.billing.ts");
|
|
582
|
+
writeFileSync(outPath, output);
|
|
583
|
+
console.log(chalk2.green(`Created ${outPath}`));
|
|
584
|
+
}
|
|
585
|
+
console.log(
|
|
586
|
+
chalk2.dim(
|
|
587
|
+
`${planCount} plans, ${priceCount} prices${result.dashboardPlans.length > 0 ? `, ${result.dashboardPlans.length} dashboard-only plans skipped` : ""}`
|
|
588
|
+
)
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
async function runInit() {
|
|
592
|
+
const configPath = resolve2(process.cwd(), "authgate.billing.ts");
|
|
593
|
+
const template = `import { defineBilling } from "@auth-gate/billing";
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Billing config \u2014 plan/feature keys provide full IDE autocomplete.
|
|
597
|
+
*
|
|
598
|
+
* Sync: npx @auth-gate/billing sync --apply
|
|
599
|
+
*/
|
|
600
|
+
export const billing = defineBilling({
|
|
601
|
+
plans: {
|
|
602
|
+
starter: {
|
|
603
|
+
name: "Starter",
|
|
604
|
+
description: "For individuals and small projects",
|
|
605
|
+
prices: [
|
|
606
|
+
{ amount: 999, currency: "usd", interval: "monthly" },
|
|
607
|
+
{ amount: 9999, currency: "usd", interval: "yearly" },
|
|
608
|
+
],
|
|
609
|
+
},
|
|
610
|
+
pro: {
|
|
611
|
+
name: "Pro",
|
|
612
|
+
description: "For growing teams",
|
|
613
|
+
prices: [
|
|
614
|
+
{ amount: 2999, currency: "usd", interval: "monthly" },
|
|
615
|
+
{ amount: 29999, currency: "usd", interval: "yearly" },
|
|
616
|
+
],
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// Default export for CLI compatibility
|
|
622
|
+
export default billing;
|
|
623
|
+
`;
|
|
624
|
+
writeFileSync(configPath, template, "utf-8");
|
|
625
|
+
console.log(chalk2.green(`Created ${configPath}`));
|
|
626
|
+
console.log(chalk2.dim("Edit your plans, then run: npx @auth-gate/billing sync"));
|
|
627
|
+
}
|
|
628
|
+
async function runSync(opts) {
|
|
629
|
+
var _a;
|
|
630
|
+
const apiKey = process.env.AUTHGATE_API_KEY;
|
|
631
|
+
const baseUrl = process.env.AUTHGATE_BASE_URL;
|
|
632
|
+
if (!apiKey) {
|
|
633
|
+
console.error(chalk2.red("Missing AUTHGATE_API_KEY environment variable."));
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
if (!baseUrl) {
|
|
637
|
+
console.error(chalk2.red("Missing AUTHGATE_BASE_URL environment variable."));
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
const environment = resolveEnvironment({ env: opts.envFlag });
|
|
641
|
+
let config;
|
|
642
|
+
try {
|
|
643
|
+
config = await loadConfig(process.cwd());
|
|
644
|
+
} catch (err) {
|
|
645
|
+
console.error(chalk2.red(`Config error: ${err.message}`));
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
const planCount = Object.keys(config.plans).length;
|
|
649
|
+
const envLabel = environment !== "production" ? ` [${environment}]` : "";
|
|
650
|
+
console.log(chalk2.dim(`Loaded config: ${planCount} plan${planCount > 1 ? "s" : ""}${envLabel}`));
|
|
651
|
+
const client = new SyncClient({ baseUrl, apiKey, environment });
|
|
652
|
+
let serverState;
|
|
653
|
+
let subscriberCounts;
|
|
654
|
+
try {
|
|
655
|
+
[serverState, subscriberCounts] = await Promise.all([
|
|
656
|
+
client.getState(),
|
|
657
|
+
client.getSubscriberCounts()
|
|
658
|
+
]);
|
|
659
|
+
} catch (err) {
|
|
660
|
+
console.error(chalk2.red(`Failed to fetch server state: ${err.message}`));
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
const diff = computeDiff(config, serverState, subscriberCounts);
|
|
664
|
+
if (opts.json) {
|
|
665
|
+
const jsonOutput = formatDiffAsJson(diff, subscriberCounts, !opts.apply);
|
|
666
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
667
|
+
process.exit(diff.hasDestructive && opts.strict ? 1 : 0);
|
|
668
|
+
}
|
|
669
|
+
console.log("");
|
|
670
|
+
console.log(formatDiff(diff, !opts.apply));
|
|
671
|
+
if (!opts.apply) {
|
|
672
|
+
if (opts.strict && diff.hasDestructive) {
|
|
673
|
+
console.error(chalk2.red.bold("\n --strict: destructive changes detected. Failing."));
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
process.exit(0);
|
|
677
|
+
}
|
|
678
|
+
if (diff.hasDestructive && !opts.force) {
|
|
679
|
+
console.error(chalk2.red("\nDestructive changes require --force flag."));
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
if (diff.planOps.length === 0 && diff.priceOps.length === 0) {
|
|
683
|
+
process.exit(0);
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
console.log("");
|
|
687
|
+
const result = await client.apply(config, opts.force);
|
|
688
|
+
console.log(chalk2.green.bold("Sync complete!"));
|
|
689
|
+
if (result.created.length) console.log(chalk2.green(` Created: ${result.created.join(", ")}`));
|
|
690
|
+
if (result.updated.length) console.log(chalk2.yellow(` Updated: ${result.updated.join(", ")}`));
|
|
691
|
+
if (result.renamed.length) console.log(chalk2.cyan(` Renamed: ${result.renamed.join(", ")}`));
|
|
692
|
+
if (result.archived.length) console.log(chalk2.red(` Archived: ${result.archived.join(", ")}`));
|
|
693
|
+
if (result.warnings.length) {
|
|
694
|
+
for (const w of result.warnings) {
|
|
695
|
+
console.log(chalk2.yellow(` Warning: ${w}`));
|
|
696
|
+
}
|
|
697
|
+
if (opts.strict) {
|
|
698
|
+
console.error(chalk2.red.bold(`
|
|
699
|
+
--strict: ${result.warnings.length} warning${result.warnings.length > 1 ? "s" : ""} treated as errors.`));
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const ss = result.stripeSync;
|
|
704
|
+
if (ss.productsCreated || ss.pricesCreated || ss.pricesArchived) {
|
|
705
|
+
console.log(
|
|
706
|
+
chalk2.dim(` Stripe: ${ss.productsCreated} products created, ${ss.pricesCreated} prices created, ${ss.pricesArchived} prices archived`)
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
if ((_a = result.migrationIds) == null ? void 0 : _a.length) {
|
|
710
|
+
for (const migId of result.migrationIds) {
|
|
711
|
+
console.log(chalk2.dim(`
|
|
712
|
+
Migration queued (ID: ${migId})`));
|
|
713
|
+
console.log(chalk2.dim(" Executing migration..."));
|
|
714
|
+
try {
|
|
715
|
+
const migResult = await client.executeMigration(migId);
|
|
716
|
+
console.log(chalk2.green(` Migrated: ${migResult.migrated} subscribers`));
|
|
717
|
+
if (migResult.skipped > 0) console.log(chalk2.yellow(` Skipped: ${migResult.skipped}`));
|
|
718
|
+
if (migResult.failed > 0) console.log(chalk2.red(` Failed: ${migResult.failed}`));
|
|
719
|
+
} catch (err) {
|
|
720
|
+
console.log(chalk2.yellow(` Migration execution deferred: ${err.message}`));
|
|
721
|
+
console.log(chalk2.dim(` Run: npx @auth-gate/billing migrate --id ${migId}`));
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
} catch (err) {
|
|
726
|
+
console.error(chalk2.red(`Sync failed: ${err.message}`));
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
async function runMigrate(opts) {
|
|
731
|
+
if (!opts.migrationId && (!opts.fromKey || !opts.toKey)) {
|
|
732
|
+
console.error(chalk2.red("Usage: migrate --id <migrationId> OR migrate <from> <to>"));
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
735
|
+
const apiKey = process.env.AUTHGATE_API_KEY;
|
|
736
|
+
const baseUrl = process.env.AUTHGATE_BASE_URL;
|
|
737
|
+
if (!apiKey) {
|
|
738
|
+
console.error(chalk2.red("Missing AUTHGATE_API_KEY environment variable."));
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
if (!baseUrl) {
|
|
742
|
+
console.error(chalk2.red("Missing AUTHGATE_BASE_URL environment variable."));
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
const client = new SyncClient({ baseUrl, apiKey });
|
|
746
|
+
let migrationId = opts.migrationId;
|
|
747
|
+
if (!migrationId && opts.fromKey && opts.toKey) {
|
|
748
|
+
console.log(chalk2.dim(`Creating migration: ${opts.fromKey} \u2192 ${opts.toKey}`));
|
|
749
|
+
let createResult;
|
|
750
|
+
try {
|
|
751
|
+
createResult = await client.createMigration(opts.fromKey, opts.toKey);
|
|
752
|
+
} catch (err) {
|
|
753
|
+
console.error(chalk2.red(`Failed to create migration: ${err.message}`));
|
|
754
|
+
process.exit(1);
|
|
755
|
+
}
|
|
756
|
+
migrationId = createResult.migrationId;
|
|
757
|
+
console.log(chalk2.dim(`Migration ID: ${migrationId}`));
|
|
758
|
+
console.log("");
|
|
759
|
+
console.log(chalk2.bold(" Price mapping:"));
|
|
760
|
+
for (const [oldKey, newKey] of Object.entries(createResult.priceMapping)) {
|
|
761
|
+
console.log(chalk2.dim(` ${oldKey} \u2192 ${newKey}`));
|
|
762
|
+
}
|
|
763
|
+
console.log("");
|
|
764
|
+
console.log(chalk2.dim(` Subscribers to migrate: ${createResult.totalSubscribers}`));
|
|
765
|
+
if (createResult.pastDueSkipped > 0) {
|
|
766
|
+
console.log(chalk2.yellow(` past_due skipped: ${createResult.pastDueSkipped}`));
|
|
767
|
+
}
|
|
768
|
+
if (createResult.unmappedSkipped > 0) {
|
|
769
|
+
console.log(chalk2.yellow(` Unmapped (no price match): ${createResult.unmappedSkipped}`));
|
|
770
|
+
}
|
|
771
|
+
console.log("");
|
|
772
|
+
if (opts.dryRun) {
|
|
773
|
+
console.log(chalk2.dim(" Dry run \u2014 no changes applied. Run without --dry-run to execute."));
|
|
774
|
+
process.exit(0);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
let migration;
|
|
778
|
+
try {
|
|
779
|
+
migration = await client.getMigration(migrationId);
|
|
780
|
+
} catch (err) {
|
|
781
|
+
console.error(chalk2.red(`Failed to fetch migration: ${err.message}`));
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
console.log(chalk2.dim(`Migration: ${migration.fromConfigKey} \u2192 ${migration.toConfigKey}`));
|
|
785
|
+
console.log(chalk2.dim(`Status: ${migration.status} | Total: ${migration.totalCount} | Migrated: ${migration.migratedCount} | Failed: ${migration.failedCount}`));
|
|
786
|
+
if (opts.dryRun) {
|
|
787
|
+
console.log("");
|
|
788
|
+
console.log(chalk2.dim(" Dry run \u2014 no changes applied. Run without --dry-run to execute."));
|
|
789
|
+
process.exit(0);
|
|
790
|
+
}
|
|
791
|
+
if (migration.status === "completed") {
|
|
792
|
+
console.log(chalk2.green("Migration already completed."));
|
|
793
|
+
process.exit(0);
|
|
794
|
+
}
|
|
795
|
+
if (migration.status === "running") {
|
|
796
|
+
console.error(chalk2.yellow("Migration is already running. Check back later."));
|
|
797
|
+
process.exit(1);
|
|
798
|
+
}
|
|
799
|
+
console.log("");
|
|
800
|
+
console.log(chalk2.cyan("Executing migration..."));
|
|
801
|
+
try {
|
|
802
|
+
const result = await client.executeMigration(migrationId, {
|
|
803
|
+
batchSize: opts.batchSize
|
|
804
|
+
});
|
|
805
|
+
console.log(chalk2.green.bold("Migration complete!"));
|
|
806
|
+
console.log(chalk2.green(` Migrated: ${result.migrated}`));
|
|
807
|
+
if (result.skipped > 0) {
|
|
808
|
+
console.log(chalk2.yellow(` Skipped: ${result.skipped}`));
|
|
809
|
+
}
|
|
810
|
+
if (result.failed > 0) {
|
|
811
|
+
console.log(chalk2.red(` Failed: ${result.failed}`));
|
|
812
|
+
}
|
|
813
|
+
} catch (err) {
|
|
814
|
+
console.error(chalk2.red(`Migration failed: ${err.message}`));
|
|
815
|
+
process.exit(1);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
main().catch((err) => {
|
|
819
|
+
console.error(chalk2.red(err.message));
|
|
820
|
+
process.exit(1);
|
|
821
|
+
});
|