stripe-experiment-sync 1.0.6 → 1.0.7

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/README.md CHANGED
@@ -120,11 +120,32 @@ The library will create and manage a `stripe` schema in your PostgreSQL database
120
120
 
121
121
  > **Important:** The library uses a fixed schema name of `stripe`. This cannot be configured as the SQL migrations hardcode this schema name.
122
122
 
123
- > **Note:** Fields and tables prefixed with an underscore (`_`) are reserved for internal metadata managed by the sync engine and should not be modified directly. These include fields like `_account`, `_cursor`, `_synced_at`, and tables like `_migrations`, `_accounts`, `_sync_run`, and `_sync_obj_run`.
123
+ > **Note:** Fields and tables prefixed with an underscore (`_`) are reserved for internal metadata managed by the sync engine and should not be modified directly. These include fields like `_account_id`, `_last_synced_at`, `_updated_at`, and tables like `_migrations`, `_managed_webhooks`, `_sync_runs`, and `_sync_obj_runs`.
124
+
125
+ ### Observability
126
+
127
+ The sync engine tracks sync operations in the `sync_runs` view, which provides aggregated metrics for each sync session:
128
+
129
+ ```sql
130
+ SELECT
131
+ account_id,
132
+ started_at,
133
+ closed_at,
134
+ status, -- 'running', 'complete', or 'error'
135
+ total_processed, -- Total records synced across all objects
136
+ complete_count, -- Number of object types completed
137
+ error_count, -- Number of object types with errors
138
+ running_count, -- Number of object types currently syncing
139
+ pending_count -- Number of object types not yet started
140
+ FROM stripe.sync_runs
141
+ ORDER BY started_at DESC;
142
+ ```
143
+
144
+ For detailed per-object status, you can query the internal `_sync_obj_runs` table (though this is not recommended for production use).
124
145
 
125
146
  ### Migrations
126
147
 
127
- Migrations are included in the `db/migrations` directory. You can run them using the provided `runMigrations` function:
148
+ Migrations are automatically included with the package and bundled in the `dist/migrations` directory when built. You can run them using the provided `runMigrations` function:
128
149
 
129
150
  ```ts
130
151
  import { runMigrations } from 'stripe-experiment-sync'
@@ -205,10 +226,10 @@ await sync.processUntilDone({
205
226
  })
206
227
  ```
207
228
 
208
- - `object` can be one of: `all`, `charge`, `customer`, `dispute`, `invoice`, `payment_method`, `payment_intent`, `plan`, `price`, `product`, `setup_intent`, `subscription`.
229
+ - `object` can be one of: `all`, `charge`, `checkout_sessions`, `credit_note`, `customer`, `customer_with_entitlements`, `dispute`, `early_fraud_warning`, `invoice`, `payment_intent`, `payment_method`, `plan`, `price`, `product`, `refund`, `setup_intent`, `subscription`, `subscription_schedules`, `tax_id`.
209
230
  - `created` is a Stripe RangeQueryParam and supports `gt`, `gte`, `lt`, `lte`.
210
231
 
211
- The sync engine automatically tracks per-account cursors in the `_sync_run` and `_sync_obj_run` tables. When you call sync methods without an explicit `created` filter, they will automatically resume from the last synced position for that account and resource. This enables incremental syncing that can resume after interruptions.
232
+ The sync engine automatically tracks per-account cursors in the `_sync_runs` and `_sync_obj_runs` tables. When you call sync methods without an explicit `created` filter, they will automatically resume from the last synced position for that account and resource. This enables incremental syncing that can resume after interruptions.
212
233
 
213
234
  > **Note:**
214
235
  > For large Stripe accounts (more than 10,000 objects), it is recommended to write a script that loops through each day and sets the `created` date filters to the start and end of day. This avoids timeouts and memory issues when syncing large datasets.
@@ -0,0 +1,595 @@
1
+ import {
2
+ StripeSync,
3
+ runMigrations
4
+ } from "./chunk-7JWRDXNB.js";
5
+ import {
6
+ install,
7
+ uninstall
8
+ } from "./chunk-SX3HLE4H.js";
9
+
10
+ // src/cli/config.ts
11
+ import dotenv from "dotenv";
12
+ import inquirer from "inquirer";
13
+ import chalk from "chalk";
14
+ async function loadConfig(options) {
15
+ dotenv.config();
16
+ const config = {};
17
+ config.stripeApiKey = options.stripeKey || process.env.STRIPE_API_KEY || "";
18
+ config.ngrokAuthToken = options.ngrokToken || process.env.NGROK_AUTH_TOKEN || "";
19
+ config.databaseUrl = options.databaseUrl || process.env.DATABASE_URL || "";
20
+ const questions = [];
21
+ if (!config.stripeApiKey) {
22
+ questions.push({
23
+ type: "password",
24
+ name: "stripeApiKey",
25
+ message: "Enter your Stripe API key:",
26
+ mask: "*",
27
+ validate: (input) => {
28
+ if (!input || input.trim() === "") {
29
+ return "Stripe API key is required";
30
+ }
31
+ if (!input.startsWith("sk_")) {
32
+ return 'Stripe API key should start with "sk_"';
33
+ }
34
+ return true;
35
+ }
36
+ });
37
+ }
38
+ if (!config.ngrokAuthToken) {
39
+ questions.push({
40
+ type: "password",
41
+ name: "ngrokAuthToken",
42
+ message: "Enter your ngrok auth token:",
43
+ mask: "*",
44
+ validate: (input) => {
45
+ if (!input || input.trim() === "") {
46
+ return "Ngrok auth token is required";
47
+ }
48
+ return true;
49
+ }
50
+ });
51
+ }
52
+ if (!config.databaseUrl) {
53
+ questions.push({
54
+ type: "password",
55
+ name: "databaseUrl",
56
+ message: "Enter your Postgres DATABASE_URL:",
57
+ mask: "*",
58
+ validate: (input) => {
59
+ if (!input || input.trim() === "") {
60
+ return "DATABASE_URL is required";
61
+ }
62
+ if (!input.startsWith("postgres://") && !input.startsWith("postgresql://")) {
63
+ return 'DATABASE_URL should start with "postgres://" or "postgresql://"';
64
+ }
65
+ return true;
66
+ }
67
+ });
68
+ }
69
+ if (questions.length > 0) {
70
+ console.log(chalk.yellow("\nMissing required configuration. Please provide:"));
71
+ const answers = await inquirer.prompt(questions);
72
+ Object.assign(config, answers);
73
+ }
74
+ return config;
75
+ }
76
+
77
+ // src/cli/ngrok.ts
78
+ import ngrok from "@ngrok/ngrok";
79
+ import chalk2 from "chalk";
80
+ async function createTunnel(port, authToken) {
81
+ try {
82
+ console.log(chalk2.blue(`
83
+ Creating ngrok tunnel for port ${port}...`));
84
+ const listener = await ngrok.forward({
85
+ addr: port,
86
+ authtoken: authToken
87
+ });
88
+ const url = listener.url();
89
+ if (!url) {
90
+ throw new Error("Failed to get ngrok URL");
91
+ }
92
+ console.log(chalk2.green(`\u2713 ngrok tunnel created: ${url}`));
93
+ return {
94
+ url,
95
+ close: async () => {
96
+ console.log(chalk2.blue("\nClosing ngrok tunnel..."));
97
+ await listener.close();
98
+ console.log(chalk2.green("\u2713 ngrok tunnel closed"));
99
+ }
100
+ };
101
+ } catch (error) {
102
+ console.error(chalk2.red("\nFailed to create ngrok tunnel:"));
103
+ if (error instanceof Error) {
104
+ console.error(chalk2.red(error.message));
105
+ }
106
+ throw error;
107
+ }
108
+ }
109
+
110
+ // src/cli/commands.ts
111
+ import chalk3 from "chalk";
112
+ import express from "express";
113
+ import dotenv2 from "dotenv";
114
+ var VALID_SYNC_OBJECTS = [
115
+ "all",
116
+ "customer",
117
+ "customer_with_entitlements",
118
+ "invoice",
119
+ "price",
120
+ "product",
121
+ "subscription",
122
+ "subscription_schedules",
123
+ "setup_intent",
124
+ "payment_method",
125
+ "dispute",
126
+ "charge",
127
+ "payment_intent",
128
+ "plan",
129
+ "tax_id",
130
+ "credit_note",
131
+ "early_fraud_warning",
132
+ "refund",
133
+ "checkout_sessions"
134
+ ];
135
+ async function backfillCommand(options, entityName) {
136
+ let stripeSync = null;
137
+ try {
138
+ if (!VALID_SYNC_OBJECTS.includes(entityName)) {
139
+ console.error(
140
+ chalk3.red(
141
+ `Error: Invalid entity name "${entityName}". Valid entities are: ${VALID_SYNC_OBJECTS.join(", ")}`
142
+ )
143
+ );
144
+ process.exit(1);
145
+ }
146
+ dotenv2.config();
147
+ let stripeApiKey = options.stripeKey || process.env.STRIPE_API_KEY || process.env.STRIPE_SECRET_KEY || "";
148
+ let databaseUrl = options.databaseUrl || process.env.DATABASE_URL || "";
149
+ if (!stripeApiKey || !databaseUrl) {
150
+ const inquirer2 = (await import("inquirer")).default;
151
+ const questions = [];
152
+ if (!stripeApiKey) {
153
+ questions.push({
154
+ type: "password",
155
+ name: "stripeApiKey",
156
+ message: "Enter your Stripe API key:",
157
+ mask: "*",
158
+ validate: (input) => {
159
+ if (!input || input.trim() === "") {
160
+ return "Stripe API key is required";
161
+ }
162
+ if (!input.startsWith("sk_")) {
163
+ return 'Stripe API key should start with "sk_"';
164
+ }
165
+ return true;
166
+ }
167
+ });
168
+ }
169
+ if (!databaseUrl) {
170
+ questions.push({
171
+ type: "password",
172
+ name: "databaseUrl",
173
+ message: "Enter your Postgres DATABASE_URL:",
174
+ mask: "*",
175
+ validate: (input) => {
176
+ if (!input || input.trim() === "") {
177
+ return "DATABASE_URL is required";
178
+ }
179
+ if (!input.startsWith("postgres://") && !input.startsWith("postgresql://")) {
180
+ return 'DATABASE_URL should start with "postgres://" or "postgresql://"';
181
+ }
182
+ return true;
183
+ }
184
+ });
185
+ }
186
+ if (questions.length > 0) {
187
+ console.log(chalk3.yellow("\nMissing required configuration. Please provide:"));
188
+ const answers = await inquirer2.prompt(questions);
189
+ if (answers.stripeApiKey) stripeApiKey = answers.stripeApiKey;
190
+ if (answers.databaseUrl) databaseUrl = answers.databaseUrl;
191
+ }
192
+ }
193
+ const config = {
194
+ stripeApiKey,
195
+ databaseUrl,
196
+ ngrokAuthToken: ""
197
+ // Not needed for backfill
198
+ };
199
+ console.log(chalk3.blue(`Backfilling ${entityName} from Stripe in 'stripe' schema...`));
200
+ console.log(chalk3.gray(`Database: ${config.databaseUrl.replace(/:[^:@]+@/, ":****@")}`));
201
+ try {
202
+ await runMigrations({
203
+ databaseUrl: config.databaseUrl
204
+ });
205
+ } catch (migrationError) {
206
+ console.error(chalk3.red("Failed to run migrations:"));
207
+ console.error(
208
+ migrationError instanceof Error ? migrationError.message : String(migrationError)
209
+ );
210
+ throw migrationError;
211
+ }
212
+ const poolConfig = {
213
+ max: 10,
214
+ connectionString: config.databaseUrl,
215
+ keepAlive: true
216
+ };
217
+ stripeSync = new StripeSync({
218
+ databaseUrl: config.databaseUrl,
219
+ stripeSecretKey: config.stripeApiKey,
220
+ stripeApiVersion: process.env.STRIPE_API_VERSION || "2020-08-27",
221
+ autoExpandLists: process.env.AUTO_EXPAND_LISTS === "true",
222
+ backfillRelatedEntities: process.env.BACKFILL_RELATED_ENTITIES !== "false",
223
+ poolConfig
224
+ });
225
+ const result = await stripeSync.processUntilDone({ object: entityName });
226
+ const totalSynced = Object.values(result).reduce(
227
+ (sum, syncResult) => sum + (syncResult?.synced || 0),
228
+ 0
229
+ );
230
+ console.log(chalk3.green(`\u2713 Backfill complete: ${totalSynced} ${entityName} objects synced`));
231
+ await stripeSync.close();
232
+ } catch (error) {
233
+ if (error instanceof Error) {
234
+ console.error(chalk3.red(error.message));
235
+ }
236
+ if (stripeSync) {
237
+ try {
238
+ await stripeSync.close();
239
+ } catch {
240
+ }
241
+ }
242
+ process.exit(1);
243
+ }
244
+ }
245
+ async function migrateCommand(options) {
246
+ try {
247
+ dotenv2.config();
248
+ let databaseUrl = options.databaseUrl || process.env.DATABASE_URL || "";
249
+ if (!databaseUrl) {
250
+ const inquirer2 = (await import("inquirer")).default;
251
+ const answers = await inquirer2.prompt([
252
+ {
253
+ type: "password",
254
+ name: "databaseUrl",
255
+ message: "Enter your Postgres DATABASE_URL:",
256
+ mask: "*",
257
+ validate: (input) => {
258
+ if (!input || input.trim() === "") {
259
+ return "DATABASE_URL is required";
260
+ }
261
+ if (!input.startsWith("postgres://") && !input.startsWith("postgresql://")) {
262
+ return 'DATABASE_URL should start with "postgres://" or "postgresql://"';
263
+ }
264
+ return true;
265
+ }
266
+ }
267
+ ]);
268
+ databaseUrl = answers.databaseUrl;
269
+ }
270
+ console.log(chalk3.blue("Running database migrations in 'stripe' schema..."));
271
+ console.log(chalk3.gray(`Database: ${databaseUrl.replace(/:[^:@]+@/, ":****@")}`));
272
+ try {
273
+ await runMigrations({
274
+ databaseUrl
275
+ });
276
+ console.log(chalk3.green("\u2713 Migrations completed successfully"));
277
+ } catch (migrationError) {
278
+ console.warn(chalk3.yellow("Migrations failed."));
279
+ if (migrationError instanceof Error) {
280
+ const errorMsg = migrationError.message || migrationError.toString();
281
+ console.warn("Migration error:", errorMsg);
282
+ if (migrationError.stack) {
283
+ console.warn(chalk3.gray(migrationError.stack));
284
+ }
285
+ } else {
286
+ console.warn("Migration error:", String(migrationError));
287
+ }
288
+ throw migrationError;
289
+ }
290
+ } catch (error) {
291
+ if (error instanceof Error) {
292
+ console.error(chalk3.red(error.message));
293
+ }
294
+ process.exit(1);
295
+ }
296
+ }
297
+ async function syncCommand(options) {
298
+ let stripeSync = null;
299
+ let tunnel = null;
300
+ let server = null;
301
+ let webhookId = null;
302
+ const cleanup = async (signal) => {
303
+ console.log(chalk3.blue(`
304
+
305
+ Cleaning up... (signal: ${signal || "manual"})`));
306
+ const keepWebhooksOnShutdown = process.env.KEEP_WEBHOOKS_ON_SHUTDOWN === "true";
307
+ if (webhookId && stripeSync && !keepWebhooksOnShutdown) {
308
+ try {
309
+ await stripeSync.deleteManagedWebhook(webhookId);
310
+ console.log(chalk3.green("\u2713 Webhook cleanup complete"));
311
+ } catch {
312
+ console.log(chalk3.yellow("\u26A0 Could not delete webhook"));
313
+ }
314
+ }
315
+ if (server) {
316
+ try {
317
+ await new Promise((resolve, reject) => {
318
+ server.close((err) => {
319
+ if (err) reject(err);
320
+ else resolve();
321
+ });
322
+ });
323
+ console.log(chalk3.green("\u2713 Server stopped"));
324
+ } catch {
325
+ console.log(chalk3.yellow("\u26A0 Server already stopped"));
326
+ }
327
+ }
328
+ if (tunnel) {
329
+ try {
330
+ await tunnel.close();
331
+ } catch {
332
+ console.log(chalk3.yellow("\u26A0 Could not close tunnel"));
333
+ }
334
+ }
335
+ if (stripeSync) {
336
+ try {
337
+ await stripeSync.close();
338
+ console.log(chalk3.green("\u2713 Database pool closed"));
339
+ } catch {
340
+ console.log(chalk3.yellow("\u26A0 Could not close database pool"));
341
+ }
342
+ }
343
+ process.exit(0);
344
+ };
345
+ process.on("SIGINT", () => cleanup("SIGINT"));
346
+ process.on("SIGTERM", () => cleanup("SIGTERM"));
347
+ try {
348
+ const config = await loadConfig(options);
349
+ console.log(chalk3.gray(`$ stripe-sync start ${config.databaseUrl}`));
350
+ try {
351
+ await runMigrations({
352
+ databaseUrl: config.databaseUrl
353
+ });
354
+ } catch (migrationError) {
355
+ console.error(chalk3.red("Failed to run migrations:"));
356
+ console.error(
357
+ migrationError instanceof Error ? migrationError.message : String(migrationError)
358
+ );
359
+ throw migrationError;
360
+ }
361
+ const poolConfig = {
362
+ max: 10,
363
+ connectionString: config.databaseUrl,
364
+ keepAlive: true
365
+ };
366
+ stripeSync = new StripeSync({
367
+ databaseUrl: config.databaseUrl,
368
+ stripeSecretKey: config.stripeApiKey,
369
+ stripeApiVersion: process.env.STRIPE_API_VERSION || "2020-08-27",
370
+ autoExpandLists: process.env.AUTO_EXPAND_LISTS === "true",
371
+ backfillRelatedEntities: process.env.BACKFILL_RELATED_ENTITIES !== "false",
372
+ poolConfig
373
+ });
374
+ const port = 3e3;
375
+ tunnel = await createTunnel(port, config.ngrokAuthToken);
376
+ const webhookPath = process.env.WEBHOOK_PATH || "/stripe-webhooks";
377
+ console.log(chalk3.blue("\nCreating Stripe webhook endpoint..."));
378
+ const webhook = await stripeSync.findOrCreateManagedWebhook(`${tunnel.url}${webhookPath}`);
379
+ webhookId = webhook.id;
380
+ const eventCount = webhook.enabled_events?.length || 0;
381
+ console.log(chalk3.green(`\u2713 Webhook created: ${webhook.id}`));
382
+ console.log(chalk3.cyan(` URL: ${webhook.url}`));
383
+ console.log(chalk3.cyan(` Events: ${eventCount} supported events`));
384
+ const app = express();
385
+ const webhookRoute = webhookPath;
386
+ app.use(webhookRoute, express.raw({ type: "application/json" }));
387
+ app.post(webhookRoute, async (req, res) => {
388
+ const sig = req.headers["stripe-signature"];
389
+ if (!sig || typeof sig !== "string") {
390
+ console.error("[Webhook] Missing stripe-signature header");
391
+ return res.status(400).send({ error: "Missing stripe-signature header" });
392
+ }
393
+ const rawBody = req.body;
394
+ if (!rawBody || !Buffer.isBuffer(rawBody)) {
395
+ console.error("[Webhook] Body is not a Buffer!");
396
+ return res.status(400).send({ error: "Missing raw body for signature verification" });
397
+ }
398
+ try {
399
+ await stripeSync.processWebhook(rawBody, sig);
400
+ return res.status(200).send({ received: true });
401
+ } catch (error) {
402
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
403
+ console.error("[Webhook] Processing error:", errorMessage);
404
+ return res.status(400).send({ error: errorMessage });
405
+ }
406
+ });
407
+ app.use(express.json());
408
+ app.use(express.urlencoded({ extended: false }));
409
+ app.get("/health", async (req, res) => {
410
+ return res.status(200).json({ status: "ok" });
411
+ });
412
+ console.log(chalk3.blue(`
413
+ Starting server on port ${port}...`));
414
+ await new Promise((resolve, reject) => {
415
+ server = app.listen(port, "0.0.0.0", () => {
416
+ resolve();
417
+ });
418
+ server.on("error", reject);
419
+ });
420
+ console.log(chalk3.green(`\u2713 Server started on port ${port}`));
421
+ if (process.env.SKIP_BACKFILL !== "true") {
422
+ console.log(chalk3.blue("\nStarting initial sync of all Stripe data..."));
423
+ const syncResult = await stripeSync.processUntilDone();
424
+ const totalSynced = Object.values(syncResult).reduce(
425
+ (sum, result) => sum + (result?.synced || 0),
426
+ 0
427
+ );
428
+ console.log(chalk3.green(`\u2713 Sync complete: ${totalSynced} objects synced`));
429
+ } else {
430
+ console.log(chalk3.yellow("\n\u23ED\uFE0F Skipping initial sync (SKIP_BACKFILL=true)"));
431
+ }
432
+ console.log(
433
+ chalk3.cyan("\n\u25CF Streaming live changes...") + chalk3.gray(" [press Ctrl-C to abort]")
434
+ );
435
+ await new Promise(() => {
436
+ });
437
+ } catch (error) {
438
+ if (error instanceof Error) {
439
+ console.error(chalk3.red(error.message));
440
+ }
441
+ await cleanup();
442
+ process.exit(1);
443
+ }
444
+ }
445
+ async function installCommand(options) {
446
+ try {
447
+ dotenv2.config();
448
+ let accessToken = options.supabaseAccessToken || process.env.SUPABASE_ACCESS_TOKEN || "";
449
+ let projectRef = options.supabaseProjectRef || process.env.SUPABASE_PROJECT_REF || "";
450
+ let stripeKey = options.stripeKey || process.env.STRIPE_API_KEY || process.env.STRIPE_SECRET_KEY || "";
451
+ if (!accessToken || !projectRef || !stripeKey) {
452
+ const inquirer2 = (await import("inquirer")).default;
453
+ const questions = [];
454
+ if (!accessToken) {
455
+ questions.push({
456
+ type: "password",
457
+ name: "accessToken",
458
+ message: "Enter your Supabase access token (from supabase.com/dashboard/account/tokens):",
459
+ mask: "*",
460
+ validate: (input) => input.trim() !== "" || "Access token is required"
461
+ });
462
+ }
463
+ if (!projectRef) {
464
+ questions.push({
465
+ type: "input",
466
+ name: "projectRef",
467
+ message: "Enter your Supabase project ref (e.g., abcdefghijklmnop):",
468
+ validate: (input) => input.trim() !== "" || "Project ref is required"
469
+ });
470
+ }
471
+ if (!stripeKey) {
472
+ questions.push({
473
+ type: "password",
474
+ name: "stripeKey",
475
+ message: "Enter your Stripe secret key:",
476
+ mask: "*",
477
+ validate: (input) => {
478
+ if (!input.trim()) return "Stripe key is required";
479
+ if (!input.startsWith("sk_")) return 'Stripe key should start with "sk_"';
480
+ return true;
481
+ }
482
+ });
483
+ }
484
+ if (questions.length > 0) {
485
+ console.log(chalk3.yellow("\nMissing required configuration. Please provide:"));
486
+ const answers = await inquirer2.prompt(questions);
487
+ if (answers.accessToken) accessToken = answers.accessToken;
488
+ if (answers.projectRef) projectRef = answers.projectRef;
489
+ if (answers.stripeKey) stripeKey = answers.stripeKey;
490
+ }
491
+ }
492
+ console.log(chalk3.blue("\n\u{1F680} Installing Stripe Sync to Supabase Edge Functions...\n"));
493
+ console.log(chalk3.gray("Validating project access..."));
494
+ await install({
495
+ supabaseAccessToken: accessToken,
496
+ supabaseProjectRef: projectRef,
497
+ stripeKey
498
+ });
499
+ console.log(chalk3.cyan("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
500
+ console.log(chalk3.cyan.bold(" Installation Complete!"));
501
+ console.log(chalk3.cyan("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n"));
502
+ console.log(chalk3.gray("\n Your Stripe data will stay in sync to your Supabase database."));
503
+ console.log(
504
+ chalk3.gray(' View your data in the Supabase dashboard under the "stripe" schema.\n')
505
+ );
506
+ } catch (error) {
507
+ if (error instanceof Error) {
508
+ console.error(chalk3.red(`
509
+ \u2717 Installation failed: ${error.message}`));
510
+ }
511
+ process.exit(1);
512
+ }
513
+ }
514
+ async function uninstallCommand(options) {
515
+ try {
516
+ dotenv2.config();
517
+ let accessToken = options.supabaseAccessToken || process.env.SUPABASE_ACCESS_TOKEN || "";
518
+ let projectRef = options.supabaseProjectRef || process.env.SUPABASE_PROJECT_REF || "";
519
+ let stripeKey = options.stripeKey || process.env.STRIPE_API_KEY || process.env.STRIPE_SECRET_KEY || "";
520
+ if (!accessToken || !projectRef || !stripeKey) {
521
+ const inquirer2 = (await import("inquirer")).default;
522
+ const questions = [];
523
+ if (!accessToken) {
524
+ questions.push({
525
+ type: "password",
526
+ name: "accessToken",
527
+ message: "Enter your Supabase access token (from supabase.com/dashboard/account/tokens):",
528
+ mask: "*",
529
+ validate: (input) => input.trim() !== "" || "Access token is required"
530
+ });
531
+ }
532
+ if (!projectRef) {
533
+ questions.push({
534
+ type: "input",
535
+ name: "projectRef",
536
+ message: "Enter your Supabase project ref (e.g., abcdefghijklmnop):",
537
+ validate: (input) => input.trim() !== "" || "Project ref is required"
538
+ });
539
+ }
540
+ if (!stripeKey) {
541
+ questions.push({
542
+ type: "password",
543
+ name: "stripeKey",
544
+ message: "Enter your Stripe secret key:",
545
+ mask: "*",
546
+ validate: (input) => {
547
+ if (!input.trim()) return "Stripe key is required";
548
+ if (!input.startsWith("sk_")) return 'Stripe key should start with "sk_"';
549
+ return true;
550
+ }
551
+ });
552
+ }
553
+ if (questions.length > 0) {
554
+ console.log(chalk3.yellow("\nMissing required configuration. Please provide:"));
555
+ const answers = await inquirer2.prompt(questions);
556
+ if (answers.accessToken) accessToken = answers.accessToken;
557
+ if (answers.projectRef) projectRef = answers.projectRef;
558
+ if (answers.stripeKey) stripeKey = answers.stripeKey;
559
+ }
560
+ }
561
+ console.log(chalk3.blue("\n\u{1F5D1}\uFE0F Uninstalling Stripe Sync from Supabase...\n"));
562
+ console.log(chalk3.yellow("\u26A0\uFE0F Warning: This will delete all Stripe data from your database!\n"));
563
+ console.log(chalk3.gray("Removing all resources..."));
564
+ await uninstall({
565
+ supabaseAccessToken: accessToken,
566
+ supabaseProjectRef: projectRef,
567
+ stripeKey
568
+ });
569
+ console.log(chalk3.cyan("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
570
+ console.log(chalk3.cyan.bold(" Uninstall Complete!"));
571
+ console.log(chalk3.cyan("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n"));
572
+ console.log(
573
+ chalk3.gray("\n All Stripe sync resources have been removed from your Supabase project.")
574
+ );
575
+ console.log(chalk3.gray(" - Edge Functions deleted"));
576
+ console.log(chalk3.gray(" - Stripe webhooks removed"));
577
+ console.log(chalk3.gray(" - Database schema dropped\n"));
578
+ } catch (error) {
579
+ if (error instanceof Error) {
580
+ console.error(chalk3.red(`
581
+ \u2717 Uninstall failed: ${error.message}`));
582
+ }
583
+ process.exit(1);
584
+ }
585
+ }
586
+
587
+ export {
588
+ loadConfig,
589
+ createTunnel,
590
+ backfillCommand,
591
+ migrateCommand,
592
+ syncCommand,
593
+ installCommand,
594
+ uninstallCommand
595
+ };