stripe-experiment-sync 1.0.7 → 1.0.8-beta.1765872477

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.
@@ -33,7 +33,7 @@ var import_commander = require("commander");
33
33
  // package.json
34
34
  var package_default = {
35
35
  name: "stripe-experiment-sync",
36
- version: "1.0.7",
36
+ version: "1.0.8-beta.1765872477",
37
37
  private: false,
38
38
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
39
39
  type: "module",
@@ -145,20 +145,6 @@ async function loadConfig(options) {
145
145
  }
146
146
  });
147
147
  }
148
- if (!config.ngrokAuthToken) {
149
- questions.push({
150
- type: "password",
151
- name: "ngrokAuthToken",
152
- message: "Enter your ngrok auth token:",
153
- mask: "*",
154
- validate: (input) => {
155
- if (!input || input.trim() === "") {
156
- return "Ngrok auth token is required";
157
- }
158
- return true;
159
- }
160
- });
161
- }
162
148
  if (!config.databaseUrl) {
163
149
  questions.push({
164
150
  type: "password",
@@ -1789,19 +1775,8 @@ var StripeSync = class {
1789
1775
  if (params?.runStartedAt) {
1790
1776
  runStartedAt = params.runStartedAt;
1791
1777
  } else {
1792
- const runKey = await this.postgresClient.getOrCreateSyncRun(
1793
- accountId,
1794
- params?.triggeredBy ?? "processNext"
1795
- );
1796
- if (!runKey) {
1797
- const activeRun = await this.postgresClient.getActiveSyncRun(accountId);
1798
- if (!activeRun) {
1799
- throw new Error("Failed to get or create sync run");
1800
- }
1801
- runStartedAt = activeRun.runStartedAt;
1802
- } else {
1803
- runStartedAt = runKey.runStartedAt;
1804
- }
1778
+ const { runKey } = await this.joinOrCreateSyncRun(params?.triggeredBy ?? "processNext");
1779
+ runStartedAt = runKey.runStartedAt;
1805
1780
  }
1806
1781
  await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
1807
1782
  const objRun = await this.postgresClient.getObjectRun(accountId, runStartedAt, resourceName);
@@ -1964,18 +1939,39 @@ var StripeSync = class {
1964
1939
  }
1965
1940
  return { synced: totalSynced };
1966
1941
  }
1967
- async processUntilDone(params) {
1968
- const { object } = params ?? { object: "all" };
1942
+ /**
1943
+ * Join existing sync run or create a new one.
1944
+ * Returns sync run key and list of supported objects to sync.
1945
+ *
1946
+ * Cooperative behavior: If a sync run already exists, joins it instead of failing.
1947
+ * This is used by workers and background processes that should cooperate.
1948
+ *
1949
+ * @param triggeredBy - What triggered this sync (for observability)
1950
+ * @returns Run key and list of objects to sync
1951
+ */
1952
+ async joinOrCreateSyncRun(triggeredBy = "worker") {
1969
1953
  await this.getCurrentAccount();
1970
1954
  const accountId = await this.getAccountId();
1971
- const runKey = await this.postgresClient.getOrCreateSyncRun(accountId, "processUntilDone");
1972
- if (!runKey) {
1955
+ const result = await this.postgresClient.getOrCreateSyncRun(accountId, triggeredBy);
1956
+ if (!result) {
1973
1957
  const activeRun = await this.postgresClient.getActiveSyncRun(accountId);
1974
1958
  if (!activeRun) {
1975
1959
  throw new Error("Failed to get or create sync run");
1976
1960
  }
1977
- return this.processUntilDoneWithRun(activeRun.runStartedAt, object, params);
1961
+ return {
1962
+ runKey: { accountId: activeRun.accountId, runStartedAt: activeRun.runStartedAt },
1963
+ objects: this.getSupportedSyncObjects()
1964
+ };
1978
1965
  }
1966
+ const { accountId: runAccountId, runStartedAt } = result;
1967
+ return {
1968
+ runKey: { accountId: runAccountId, runStartedAt },
1969
+ objects: this.getSupportedSyncObjects()
1970
+ };
1971
+ }
1972
+ async processUntilDone(params) {
1973
+ const { object } = params ?? { object: "all" };
1974
+ const { runKey } = await this.joinOrCreateSyncRun("processUntilDone");
1979
1975
  return this.processUntilDoneWithRun(runKey.runStartedAt, object, params);
1980
1976
  }
1981
1977
  /**
@@ -3434,6 +3430,167 @@ async function runMigrations(config) {
3434
3430
  }
3435
3431
  }
3436
3432
 
3433
+ // src/websocket-client.ts
3434
+ var import_ws = __toESM(require("ws"), 1);
3435
+ var CLI_VERSION = "1.33.0";
3436
+ var PONG_WAIT = 10 * 1e3;
3437
+ var PING_PERIOD = PONG_WAIT * 2 / 10;
3438
+ function getClientUserAgent() {
3439
+ return JSON.stringify({
3440
+ name: "stripe-cli",
3441
+ version: CLI_VERSION,
3442
+ publisher: "stripe",
3443
+ os: process.platform
3444
+ });
3445
+ }
3446
+ async function createCliSession(stripeApiKey) {
3447
+ const params = new URLSearchParams();
3448
+ params.append("device_name", "stripe-sync-engine");
3449
+ params.append("websocket_features[]", "webhooks");
3450
+ const response = await fetch("https://api.stripe.com/v1/stripecli/sessions", {
3451
+ method: "POST",
3452
+ headers: {
3453
+ Authorization: `Bearer ${stripeApiKey}`,
3454
+ "Content-Type": "application/x-www-form-urlencoded",
3455
+ "User-Agent": `Stripe/v1 stripe-cli/${CLI_VERSION}`,
3456
+ "X-Stripe-Client-User-Agent": getClientUserAgent()
3457
+ },
3458
+ body: params.toString()
3459
+ });
3460
+ if (!response.ok) {
3461
+ const error = await response.text();
3462
+ throw new Error(`Failed to create CLI session: ${error}`);
3463
+ }
3464
+ return await response.json();
3465
+ }
3466
+ async function createStripeWebSocketClient(options) {
3467
+ const { stripeApiKey, onEvent, onReady, onError, onClose } = options;
3468
+ const session = await createCliSession(stripeApiKey);
3469
+ let ws = null;
3470
+ let pingInterval = null;
3471
+ let connected = false;
3472
+ let shouldReconnect = true;
3473
+ function connect() {
3474
+ const wsUrl = `${session.websocket_url}?websocket_feature=${encodeURIComponent(session.websocket_authorized_feature)}`;
3475
+ ws = new import_ws.default(wsUrl, {
3476
+ headers: {
3477
+ "Accept-Encoding": "identity",
3478
+ "User-Agent": `Stripe/v1 stripe-cli/${CLI_VERSION}`,
3479
+ "X-Stripe-Client-User-Agent": getClientUserAgent(),
3480
+ "Websocket-Id": session.websocket_id
3481
+ }
3482
+ });
3483
+ ws.on("pong", () => {
3484
+ });
3485
+ ws.on("open", () => {
3486
+ connected = true;
3487
+ pingInterval = setInterval(() => {
3488
+ if (ws && ws.readyState === import_ws.default.OPEN) {
3489
+ ws.ping();
3490
+ }
3491
+ }, PING_PERIOD);
3492
+ if (onReady) {
3493
+ onReady(session.secret);
3494
+ }
3495
+ });
3496
+ ws.on("message", async (data) => {
3497
+ try {
3498
+ const message = JSON.parse(data.toString());
3499
+ const ack = {
3500
+ type: "event_ack",
3501
+ event_id: message.webhook_id,
3502
+ webhook_conversation_id: message.webhook_conversation_id,
3503
+ webhook_id: message.webhook_id
3504
+ };
3505
+ if (ws && ws.readyState === import_ws.default.OPEN) {
3506
+ ws.send(JSON.stringify(ack));
3507
+ }
3508
+ let response;
3509
+ try {
3510
+ const result = await onEvent(message);
3511
+ response = {
3512
+ type: "webhook_response",
3513
+ webhook_id: message.webhook_id,
3514
+ webhook_conversation_id: message.webhook_conversation_id,
3515
+ forward_url: "stripe-sync-engine",
3516
+ status: result?.status ?? 200,
3517
+ http_headers: {},
3518
+ body: JSON.stringify({
3519
+ event_type: result?.event_type,
3520
+ event_id: result?.event_id,
3521
+ database_url: result?.databaseUrl,
3522
+ error: result?.error
3523
+ }),
3524
+ request_headers: message.http_headers,
3525
+ request_body: message.event_payload,
3526
+ notification_id: message.webhook_id
3527
+ };
3528
+ } catch (err) {
3529
+ const errorMessage = err instanceof Error ? err.message : String(err);
3530
+ response = {
3531
+ type: "webhook_response",
3532
+ webhook_id: message.webhook_id,
3533
+ webhook_conversation_id: message.webhook_conversation_id,
3534
+ forward_url: "stripe-sync-engine",
3535
+ status: 500,
3536
+ http_headers: {},
3537
+ body: JSON.stringify({ error: errorMessage }),
3538
+ request_headers: message.http_headers,
3539
+ request_body: message.event_payload,
3540
+ notification_id: message.webhook_id
3541
+ };
3542
+ if (onError) {
3543
+ onError(err instanceof Error ? err : new Error(errorMessage));
3544
+ }
3545
+ }
3546
+ if (ws && ws.readyState === import_ws.default.OPEN) {
3547
+ ws.send(JSON.stringify(response));
3548
+ }
3549
+ } catch (err) {
3550
+ if (onError) {
3551
+ onError(err instanceof Error ? err : new Error(String(err)));
3552
+ }
3553
+ }
3554
+ });
3555
+ ws.on("error", (error) => {
3556
+ if (onError) {
3557
+ onError(error);
3558
+ }
3559
+ });
3560
+ ws.on("close", (code, reason) => {
3561
+ connected = false;
3562
+ if (pingInterval) {
3563
+ clearInterval(pingInterval);
3564
+ pingInterval = null;
3565
+ }
3566
+ if (onClose) {
3567
+ onClose(code, reason.toString());
3568
+ }
3569
+ if (shouldReconnect) {
3570
+ const delay = (session.reconnect_delay || 5) * 1e3;
3571
+ setTimeout(() => {
3572
+ connect();
3573
+ }, delay);
3574
+ }
3575
+ });
3576
+ }
3577
+ connect();
3578
+ return {
3579
+ close: () => {
3580
+ shouldReconnect = false;
3581
+ if (pingInterval) {
3582
+ clearInterval(pingInterval);
3583
+ pingInterval = null;
3584
+ }
3585
+ if (ws) {
3586
+ ws.close(1e3, "Connection Done");
3587
+ ws = null;
3588
+ }
3589
+ },
3590
+ isConnected: () => connected
3591
+ };
3592
+ }
3593
+
3437
3594
  // src/index.ts
3438
3595
  var VERSION = package_default.version;
3439
3596
 
@@ -3473,14 +3630,14 @@ Creating ngrok tunnel for port ${port}...`));
3473
3630
  // src/supabase/supabase.ts
3474
3631
  var import_supabase_management_js = require("supabase-management-js");
3475
3632
 
3476
- // raw-ts:/home/runner/work/stripe-sync-engine/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
3633
+ // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
3477
3634
  var stripe_setup_default = "import { StripeSync, runMigrations } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
3478
3635
 
3479
- // raw-ts:/home/runner/work/stripe-sync-engine/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
3636
+ // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
3480
3637
  var stripe_webhook_default = "import { StripeSync } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const sig = req.headers.get('stripe-signature')\n if (!sig) {\n return new Response('Missing stripe-signature header', { status: 400 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n const stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n\n try {\n const rawBody = new Uint8Array(await req.arrayBuffer())\n await stripeSync.processWebhook(rawBody, sig)\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Webhook processing error:', error)\n const isSignatureError =\n error.message?.includes('signature') || error.type === 'StripeSignatureVerificationError'\n const status = isSignatureError ? 400 : 500\n return new Response(JSON.stringify({ error: error.message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n await stripeSync.postgresClient.pool.end()\n }\n})\n";
3481
3638
 
3482
- // raw-ts:/home/runner/work/stripe-sync-engine/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
3483
- var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron every 10 seconds. Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n const objects = stripeSync.getSupportedSyncObjects()\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
3639
+ // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
3640
+ var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron every 10 seconds. Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
3484
3641
 
3485
3642
  // src/supabase/edge-function-code.ts
3486
3643
  var setupFunctionCode = stripe_setup_default;
@@ -3787,45 +3944,59 @@ var SupabaseSetupClient = class {
3787
3944
  } catch (err) {
3788
3945
  console.warn("Could not delete vault secret:", err);
3789
3946
  }
3947
+ try {
3948
+ await this.runSQL(`
3949
+ SELECT pg_terminate_backend(pid)
3950
+ FROM pg_stat_activity
3951
+ WHERE datname = current_database()
3952
+ AND pid != pg_backend_pid()
3953
+ AND query ILIKE '%stripe.%'
3954
+ `);
3955
+ } catch (err) {
3956
+ console.warn("Could not terminate connections:", err);
3957
+ }
3790
3958
  await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
3791
3959
  } catch (error) {
3792
3960
  throw new Error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
3793
3961
  }
3794
3962
  }
3795
- async install(stripeKey) {
3963
+ /**
3964
+ * Inject package version into Edge Function code
3965
+ */
3966
+ injectPackageVersion(code, version) {
3967
+ if (version === "latest") {
3968
+ return code;
3969
+ }
3970
+ return code.replace(
3971
+ /from ['"]npm:stripe-experiment-sync['"]/g,
3972
+ `from 'npm:stripe-experiment-sync@${version}'`
3973
+ );
3974
+ }
3975
+ async install(stripeKey, packageVersion) {
3796
3976
  const trimmedStripeKey = stripeKey.trim();
3797
3977
  if (!trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
3798
3978
  throw new Error('Stripe key should start with "sk_" or "rk_"');
3799
3979
  }
3980
+ const version = packageVersion || "latest";
3800
3981
  try {
3801
3982
  await this.validateProject();
3802
3983
  await this.runSQL(`CREATE SCHEMA IF NOT EXISTS stripe`);
3803
3984
  await this.updateInstallationComment(
3804
3985
  `${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_STARTED_SUFFIX}`
3805
3986
  );
3806
- await this.deployFunction("stripe-setup", setupFunctionCode);
3807
- await this.deployFunction("stripe-webhook", webhookFunctionCode);
3808
- await this.deployFunction("stripe-worker", workerFunctionCode);
3987
+ const versionedSetup = this.injectPackageVersion(setupFunctionCode, version);
3988
+ const versionedWebhook = this.injectPackageVersion(webhookFunctionCode, version);
3989
+ const versionedWorker = this.injectPackageVersion(workerFunctionCode, version);
3990
+ await this.deployFunction("stripe-setup", versionedSetup);
3991
+ await this.deployFunction("stripe-webhook", versionedWebhook);
3992
+ await this.deployFunction("stripe-worker", versionedWorker);
3809
3993
  await this.setSecrets([{ name: "STRIPE_SECRET_KEY", value: trimmedStripeKey }]);
3810
3994
  const serviceRoleKey = await this.getServiceRoleKey();
3811
3995
  const setupResult = await this.invokeFunction("stripe-setup", serviceRoleKey);
3812
3996
  if (!setupResult.success) {
3813
3997
  throw new Error(`Setup failed: ${setupResult.error}`);
3814
3998
  }
3815
- let pgCronEnabled = false;
3816
- try {
3817
- await this.setupPgCronJob();
3818
- pgCronEnabled = true;
3819
- } catch {
3820
- console.warn("pg_cron setup failed - falling back to manual worker invocation");
3821
- }
3822
- if (!pgCronEnabled) {
3823
- try {
3824
- await this.invokeFunction("stripe-worker", serviceRoleKey);
3825
- } catch (err) {
3826
- console.warn("Failed to trigger initial worker invocation:", err);
3827
- }
3828
- }
3999
+ await this.setupPgCronJob();
3829
4000
  await this.updateInstallationComment(
3830
4001
  `${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_INSTALLED_SUFFIX}`
3831
4002
  );
@@ -3838,14 +4009,14 @@ var SupabaseSetupClient = class {
3838
4009
  }
3839
4010
  };
3840
4011
  async function install(params) {
3841
- const { supabaseAccessToken, supabaseProjectRef, stripeKey } = params;
4012
+ const { supabaseAccessToken, supabaseProjectRef, stripeKey, packageVersion } = params;
3842
4013
  const client = new SupabaseSetupClient({
3843
4014
  accessToken: supabaseAccessToken,
3844
4015
  projectRef: supabaseProjectRef,
3845
4016
  projectBaseUrl: params.baseProjectUrl,
3846
4017
  managementApiBaseUrl: params.baseManagementApiUrl
3847
4018
  });
3848
- await client.install(stripeKey);
4019
+ await client.install(stripeKey, packageVersion);
3849
4020
  }
3850
4021
  async function uninstall(params) {
3851
4022
  const { supabaseAccessToken, supabaseProjectRef, stripeKey } = params;
@@ -4049,10 +4220,19 @@ async function syncCommand(options) {
4049
4220
  let tunnel = null;
4050
4221
  let server = null;
4051
4222
  let webhookId = null;
4223
+ let wsClient = null;
4052
4224
  const cleanup = async (signal) => {
4053
4225
  console.log(import_chalk3.default.blue(`
4054
4226
 
4055
4227
  Cleaning up... (signal: ${signal || "manual"})`));
4228
+ if (wsClient) {
4229
+ try {
4230
+ wsClient.close();
4231
+ console.log(import_chalk3.default.green("\u2713 WebSocket closed"));
4232
+ } catch {
4233
+ console.log(import_chalk3.default.yellow("\u26A0 Could not close WebSocket"));
4234
+ }
4235
+ }
4056
4236
  const keepWebhooksOnShutdown = process.env.KEEP_WEBHOOKS_ON_SHUTDOWN === "true";
4057
4237
  if (webhookId && stripeSync && !keepWebhooksOnShutdown) {
4058
4238
  try {
@@ -4096,7 +4276,12 @@ Cleaning up... (signal: ${signal || "manual"})`));
4096
4276
  process.on("SIGTERM", () => cleanup("SIGTERM"));
4097
4277
  try {
4098
4278
  const config = await loadConfig(options);
4099
- console.log(import_chalk3.default.gray(`$ stripe-sync start ${config.databaseUrl}`));
4279
+ const useWebSocketMode = !config.ngrokAuthToken;
4280
+ const modeLabel = useWebSocketMode ? "WebSocket" : "Webhook (ngrok)";
4281
+ console.log(import_chalk3.default.blue(`
4282
+ Mode: ${modeLabel}`));
4283
+ const maskedDbUrl = config.databaseUrl.replace(/:[^:@]+@/, ":****@");
4284
+ console.log(import_chalk3.default.gray(`Database: ${maskedDbUrl}`));
4100
4285
  try {
4101
4286
  await runMigrations({
4102
4287
  databaseUrl: config.databaseUrl
@@ -4121,53 +4306,90 @@ Cleaning up... (signal: ${signal || "manual"})`));
4121
4306
  backfillRelatedEntities: process.env.BACKFILL_RELATED_ENTITIES !== "false",
4122
4307
  poolConfig
4123
4308
  });
4124
- const port = 3e3;
4125
- tunnel = await createTunnel(port, config.ngrokAuthToken);
4126
- const webhookPath = process.env.WEBHOOK_PATH || "/stripe-webhooks";
4127
- console.log(import_chalk3.default.blue("\nCreating Stripe webhook endpoint..."));
4128
- const webhook = await stripeSync.findOrCreateManagedWebhook(`${tunnel.url}${webhookPath}`);
4129
- webhookId = webhook.id;
4130
- const eventCount = webhook.enabled_events?.length || 0;
4131
- console.log(import_chalk3.default.green(`\u2713 Webhook created: ${webhook.id}`));
4132
- console.log(import_chalk3.default.cyan(` URL: ${webhook.url}`));
4133
- console.log(import_chalk3.default.cyan(` Events: ${eventCount} supported events`));
4134
- const app = (0, import_express.default)();
4135
- const webhookRoute = webhookPath;
4136
- app.use(webhookRoute, import_express.default.raw({ type: "application/json" }));
4137
- app.post(webhookRoute, async (req, res) => {
4138
- const sig = req.headers["stripe-signature"];
4139
- if (!sig || typeof sig !== "string") {
4140
- console.error("[Webhook] Missing stripe-signature header");
4141
- return res.status(400).send({ error: "Missing stripe-signature header" });
4142
- }
4143
- const rawBody = req.body;
4144
- if (!rawBody || !Buffer.isBuffer(rawBody)) {
4145
- console.error("[Webhook] Body is not a Buffer!");
4146
- return res.status(400).send({ error: "Missing raw body for signature verification" });
4147
- }
4148
- try {
4149
- await stripeSync.processWebhook(rawBody, sig);
4150
- return res.status(200).send({ received: true });
4151
- } catch (error) {
4152
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
4153
- console.error("[Webhook] Processing error:", errorMessage);
4154
- return res.status(400).send({ error: errorMessage });
4155
- }
4156
- });
4157
- app.use(import_express.default.json());
4158
- app.use(import_express.default.urlencoded({ extended: false }));
4159
- app.get("/health", async (req, res) => {
4160
- return res.status(200).json({ status: "ok" });
4161
- });
4162
- console.log(import_chalk3.default.blue(`
4309
+ const databaseUrlWithoutPassword = config.databaseUrl.replace(/:[^:@]+@/, ":****@");
4310
+ if (useWebSocketMode) {
4311
+ console.log(import_chalk3.default.blue("\nConnecting to Stripe WebSocket..."));
4312
+ wsClient = await createStripeWebSocketClient({
4313
+ stripeApiKey: config.stripeApiKey,
4314
+ onEvent: async (event) => {
4315
+ try {
4316
+ const payload = JSON.parse(event.event_payload);
4317
+ console.log(import_chalk3.default.cyan(`\u2190 ${payload.type}`) + import_chalk3.default.gray(` (${payload.id})`));
4318
+ if (stripeSync) {
4319
+ await stripeSync.processEvent(payload);
4320
+ return {
4321
+ status: 200,
4322
+ event_type: payload.type,
4323
+ event_id: payload.id,
4324
+ databaseUrl: databaseUrlWithoutPassword
4325
+ };
4326
+ }
4327
+ } catch (err) {
4328
+ console.error(import_chalk3.default.red("Error processing event:"), err);
4329
+ return {
4330
+ status: 500,
4331
+ databaseUrl: databaseUrlWithoutPassword,
4332
+ error: err instanceof Error ? err.message : String(err)
4333
+ };
4334
+ }
4335
+ },
4336
+ onReady: (secret) => {
4337
+ console.log(import_chalk3.default.green("\u2713 Connected to Stripe WebSocket"));
4338
+ const maskedSecret = secret.length > 14 ? `${secret.slice(0, 10)}...${secret.slice(-4)}` : "****";
4339
+ console.log(import_chalk3.default.gray(` Webhook secret: ${maskedSecret}`));
4340
+ },
4341
+ onError: (error) => {
4342
+ console.error(import_chalk3.default.red("WebSocket error:"), error.message);
4343
+ },
4344
+ onClose: (code, reason) => {
4345
+ console.log(import_chalk3.default.yellow(`WebSocket closed: ${code} - ${reason}`));
4346
+ }
4347
+ });
4348
+ } else {
4349
+ const port = 3e3;
4350
+ tunnel = await createTunnel(port, config.ngrokAuthToken);
4351
+ const webhookPath = process.env.WEBHOOK_PATH || "/stripe-webhooks";
4352
+ console.log(import_chalk3.default.blue("\nCreating Stripe webhook endpoint..."));
4353
+ const webhook = await stripeSync.findOrCreateManagedWebhook(`${tunnel.url}${webhookPath}`);
4354
+ webhookId = webhook.id;
4355
+ const eventCount = webhook.enabled_events?.length || 0;
4356
+ console.log(import_chalk3.default.green(`\u2713 Webhook created: ${webhook.id}`));
4357
+ console.log(import_chalk3.default.cyan(` URL: ${webhook.url}`));
4358
+ console.log(import_chalk3.default.cyan(` Events: ${eventCount} supported events`));
4359
+ const app = (0, import_express.default)();
4360
+ const webhookRoute = webhookPath;
4361
+ app.use(webhookRoute, import_express.default.raw({ type: "application/json" }));
4362
+ app.post(webhookRoute, async (req, res) => {
4363
+ const sig = req.headers["stripe-signature"];
4364
+ if (!sig || typeof sig !== "string") {
4365
+ console.error("[Webhook] Missing stripe-signature header");
4366
+ return res.status(400).send({ error: "Missing stripe-signature header" });
4367
+ }
4368
+ const rawBody = req.body;
4369
+ if (!rawBody || !Buffer.isBuffer(rawBody)) {
4370
+ console.error("[Webhook] Body is not a Buffer!");
4371
+ return res.status(400).send({ error: "Missing raw body for signature verification" });
4372
+ }
4373
+ try {
4374
+ await stripeSync.processWebhook(rawBody, sig);
4375
+ return res.status(200).send({ received: true });
4376
+ } catch (error) {
4377
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
4378
+ console.error("[Webhook] Processing error:", errorMessage);
4379
+ return res.status(400).send({ error: errorMessage });
4380
+ }
4381
+ });
4382
+ app.use(import_express.default.json());
4383
+ app.use(import_express.default.urlencoded({ extended: false }));
4384
+ app.get("/health", async (req, res) => res.status(200).json({ status: "ok" }));
4385
+ console.log(import_chalk3.default.blue(`
4163
4386
  Starting server on port ${port}...`));
4164
- await new Promise((resolve, reject) => {
4165
- server = app.listen(port, "0.0.0.0", () => {
4166
- resolve();
4387
+ await new Promise((resolve, reject) => {
4388
+ server = app.listen(port, "0.0.0.0", () => resolve());
4389
+ server.on("error", reject);
4167
4390
  });
4168
- server.on("error", reject);
4169
- });
4170
- console.log(import_chalk3.default.green(`\u2713 Server started on port ${port}`));
4391
+ console.log(import_chalk3.default.green(`\u2713 Server started on port ${port}`));
4392
+ }
4171
4393
  if (process.env.SKIP_BACKFILL !== "true") {
4172
4394
  console.log(import_chalk3.default.blue("\nStarting initial sync of all Stripe data..."));
4173
4395
  const syncResult = await stripeSync.processUntilDone();
@@ -4244,7 +4466,8 @@ async function installCommand(options) {
4244
4466
  await install({
4245
4467
  supabaseAccessToken: accessToken,
4246
4468
  supabaseProjectRef: projectRef,
4247
- stripeKey
4469
+ stripeKey,
4470
+ packageVersion: options.packageVersion
4248
4471
  });
4249
4472
  console.log(import_chalk3.default.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"));
4250
4473
  console.log(import_chalk3.default.cyan.bold(" Installation Complete!"));
@@ -4359,11 +4582,15 @@ program.command("backfill <entityName>").description("Backfill a specific entity
4359
4582
  );
4360
4583
  });
4361
4584
  var supabase = program.command("supabase").description("Supabase Edge Functions commands");
4362
- supabase.command("install").description("Install Stripe sync to Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").action(async (options) => {
4585
+ supabase.command("install").description("Install Stripe sync to Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").option(
4586
+ "--package-version <version>",
4587
+ "Package version to install (e.g., 1.0.8-beta.1, defaults to latest)"
4588
+ ).action(async (options) => {
4363
4589
  await installCommand({
4364
4590
  supabaseAccessToken: options.token,
4365
4591
  supabaseProjectRef: options.project,
4366
- stripeKey: options.stripeKey
4592
+ stripeKey: options.stripeKey,
4593
+ packageVersion: options.packageVersion
4367
4594
  });
4368
4595
  });
4369
4596
  supabase.command("uninstall").description("Uninstall Stripe sync from Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").action(async (options) => {
package/dist/cli/index.js CHANGED
@@ -5,12 +5,12 @@ import {
5
5
  migrateCommand,
6
6
  syncCommand,
7
7
  uninstallCommand
8
- } from "../chunk-3P5TZKWU.js";
9
- import "../chunk-7JWRDXNB.js";
10
- import "../chunk-SX3HLE4H.js";
8
+ } from "../chunk-XDVV7YHP.js";
9
+ import "../chunk-E7LIOBPP.js";
10
+ import "../chunk-E2HSDYSR.js";
11
11
  import {
12
12
  package_default
13
- } from "../chunk-YXQZXR7S.js";
13
+ } from "../chunk-M3MYAMG2.js";
14
14
 
15
15
  // src/cli/index.ts
16
16
  import { Command } from "commander";
@@ -38,11 +38,15 @@ program.command("backfill <entityName>").description("Backfill a specific entity
38
38
  );
39
39
  });
40
40
  var supabase = program.command("supabase").description("Supabase Edge Functions commands");
41
- supabase.command("install").description("Install Stripe sync to Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").action(async (options) => {
41
+ supabase.command("install").description("Install Stripe sync to Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").option(
42
+ "--package-version <version>",
43
+ "Package version to install (e.g., 1.0.8-beta.1, defaults to latest)"
44
+ ).action(async (options) => {
42
45
  await installCommand({
43
46
  supabaseAccessToken: options.token,
44
47
  supabaseProjectRef: options.project,
45
- stripeKey: options.stripeKey
48
+ stripeKey: options.stripeKey,
49
+ packageVersion: options.packageVersion
46
50
  });
47
51
  });
48
52
  supabase.command("uninstall").description("Uninstall Stripe sync from Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").action(async (options) => {