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.
@@ -1,18 +1,18 @@
1
1
  import {
2
2
  package_default
3
- } from "./chunk-YXQZXR7S.js";
3
+ } from "./chunk-M3MYAMG2.js";
4
4
 
5
5
  // src/supabase/supabase.ts
6
6
  import { SupabaseManagementAPI } from "supabase-management-js";
7
7
 
8
- // raw-ts:/home/runner/work/stripe-sync-engine/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
8
+ // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
9
9
  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";
10
10
 
11
- // raw-ts:/home/runner/work/stripe-sync-engine/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
11
+ // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
12
12
  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";
13
13
 
14
- // raw-ts:/home/runner/work/stripe-sync-engine/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
15
- 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";
14
+ // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
15
+ 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";
16
16
 
17
17
  // src/supabase/edge-function-code.ts
18
18
  var setupFunctionCode = stripe_setup_default;
@@ -319,45 +319,59 @@ var SupabaseSetupClient = class {
319
319
  } catch (err) {
320
320
  console.warn("Could not delete vault secret:", err);
321
321
  }
322
+ try {
323
+ await this.runSQL(`
324
+ SELECT pg_terminate_backend(pid)
325
+ FROM pg_stat_activity
326
+ WHERE datname = current_database()
327
+ AND pid != pg_backend_pid()
328
+ AND query ILIKE '%stripe.%'
329
+ `);
330
+ } catch (err) {
331
+ console.warn("Could not terminate connections:", err);
332
+ }
322
333
  await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
323
334
  } catch (error) {
324
335
  throw new Error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
325
336
  }
326
337
  }
327
- async install(stripeKey) {
338
+ /**
339
+ * Inject package version into Edge Function code
340
+ */
341
+ injectPackageVersion(code, version) {
342
+ if (version === "latest") {
343
+ return code;
344
+ }
345
+ return code.replace(
346
+ /from ['"]npm:stripe-experiment-sync['"]/g,
347
+ `from 'npm:stripe-experiment-sync@${version}'`
348
+ );
349
+ }
350
+ async install(stripeKey, packageVersion) {
328
351
  const trimmedStripeKey = stripeKey.trim();
329
352
  if (!trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
330
353
  throw new Error('Stripe key should start with "sk_" or "rk_"');
331
354
  }
355
+ const version = packageVersion || "latest";
332
356
  try {
333
357
  await this.validateProject();
334
358
  await this.runSQL(`CREATE SCHEMA IF NOT EXISTS stripe`);
335
359
  await this.updateInstallationComment(
336
360
  `${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_STARTED_SUFFIX}`
337
361
  );
338
- await this.deployFunction("stripe-setup", setupFunctionCode);
339
- await this.deployFunction("stripe-webhook", webhookFunctionCode);
340
- await this.deployFunction("stripe-worker", workerFunctionCode);
362
+ const versionedSetup = this.injectPackageVersion(setupFunctionCode, version);
363
+ const versionedWebhook = this.injectPackageVersion(webhookFunctionCode, version);
364
+ const versionedWorker = this.injectPackageVersion(workerFunctionCode, version);
365
+ await this.deployFunction("stripe-setup", versionedSetup);
366
+ await this.deployFunction("stripe-webhook", versionedWebhook);
367
+ await this.deployFunction("stripe-worker", versionedWorker);
341
368
  await this.setSecrets([{ name: "STRIPE_SECRET_KEY", value: trimmedStripeKey }]);
342
369
  const serviceRoleKey = await this.getServiceRoleKey();
343
370
  const setupResult = await this.invokeFunction("stripe-setup", serviceRoleKey);
344
371
  if (!setupResult.success) {
345
372
  throw new Error(`Setup failed: ${setupResult.error}`);
346
373
  }
347
- let pgCronEnabled = false;
348
- try {
349
- await this.setupPgCronJob();
350
- pgCronEnabled = true;
351
- } catch {
352
- console.warn("pg_cron setup failed - falling back to manual worker invocation");
353
- }
354
- if (!pgCronEnabled) {
355
- try {
356
- await this.invokeFunction("stripe-worker", serviceRoleKey);
357
- } catch (err) {
358
- console.warn("Failed to trigger initial worker invocation:", err);
359
- }
360
- }
374
+ await this.setupPgCronJob();
361
375
  await this.updateInstallationComment(
362
376
  `${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_INSTALLED_SUFFIX}`
363
377
  );
@@ -370,14 +384,14 @@ var SupabaseSetupClient = class {
370
384
  }
371
385
  };
372
386
  async function install(params) {
373
- const { supabaseAccessToken, supabaseProjectRef, stripeKey } = params;
387
+ const { supabaseAccessToken, supabaseProjectRef, stripeKey, packageVersion } = params;
374
388
  const client = new SupabaseSetupClient({
375
389
  accessToken: supabaseAccessToken,
376
390
  projectRef: supabaseProjectRef,
377
391
  projectBaseUrl: params.baseProjectUrl,
378
392
  managementApiBaseUrl: params.baseManagementApiUrl
379
393
  });
380
- await client.install(stripeKey);
394
+ await client.install(stripeKey, packageVersion);
381
395
  }
382
396
  async function uninstall(params) {
383
397
  const { supabaseAccessToken, supabaseProjectRef, stripeKey } = params;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  package_default
3
- } from "./chunk-YXQZXR7S.js";
3
+ } from "./chunk-M3MYAMG2.js";
4
4
 
5
5
  // src/stripeSync.ts
6
6
  import Stripe2 from "stripe";
@@ -1607,19 +1607,8 @@ var StripeSync = class {
1607
1607
  if (params?.runStartedAt) {
1608
1608
  runStartedAt = params.runStartedAt;
1609
1609
  } else {
1610
- const runKey = await this.postgresClient.getOrCreateSyncRun(
1611
- accountId,
1612
- params?.triggeredBy ?? "processNext"
1613
- );
1614
- if (!runKey) {
1615
- const activeRun = await this.postgresClient.getActiveSyncRun(accountId);
1616
- if (!activeRun) {
1617
- throw new Error("Failed to get or create sync run");
1618
- }
1619
- runStartedAt = activeRun.runStartedAt;
1620
- } else {
1621
- runStartedAt = runKey.runStartedAt;
1622
- }
1610
+ const { runKey } = await this.joinOrCreateSyncRun(params?.triggeredBy ?? "processNext");
1611
+ runStartedAt = runKey.runStartedAt;
1623
1612
  }
1624
1613
  await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
1625
1614
  const objRun = await this.postgresClient.getObjectRun(accountId, runStartedAt, resourceName);
@@ -1782,18 +1771,39 @@ var StripeSync = class {
1782
1771
  }
1783
1772
  return { synced: totalSynced };
1784
1773
  }
1785
- async processUntilDone(params) {
1786
- const { object } = params ?? { object: "all" };
1774
+ /**
1775
+ * Join existing sync run or create a new one.
1776
+ * Returns sync run key and list of supported objects to sync.
1777
+ *
1778
+ * Cooperative behavior: If a sync run already exists, joins it instead of failing.
1779
+ * This is used by workers and background processes that should cooperate.
1780
+ *
1781
+ * @param triggeredBy - What triggered this sync (for observability)
1782
+ * @returns Run key and list of objects to sync
1783
+ */
1784
+ async joinOrCreateSyncRun(triggeredBy = "worker") {
1787
1785
  await this.getCurrentAccount();
1788
1786
  const accountId = await this.getAccountId();
1789
- const runKey = await this.postgresClient.getOrCreateSyncRun(accountId, "processUntilDone");
1790
- if (!runKey) {
1787
+ const result = await this.postgresClient.getOrCreateSyncRun(accountId, triggeredBy);
1788
+ if (!result) {
1791
1789
  const activeRun = await this.postgresClient.getActiveSyncRun(accountId);
1792
1790
  if (!activeRun) {
1793
1791
  throw new Error("Failed to get or create sync run");
1794
1792
  }
1795
- return this.processUntilDoneWithRun(activeRun.runStartedAt, object, params);
1793
+ return {
1794
+ runKey: { accountId: activeRun.accountId, runStartedAt: activeRun.runStartedAt },
1795
+ objects: this.getSupportedSyncObjects()
1796
+ };
1796
1797
  }
1798
+ const { accountId: runAccountId, runStartedAt } = result;
1799
+ return {
1800
+ runKey: { accountId: runAccountId, runStartedAt },
1801
+ objects: this.getSupportedSyncObjects()
1802
+ };
1803
+ }
1804
+ async processUntilDone(params) {
1805
+ const { object } = params ?? { object: "all" };
1806
+ const { runKey } = await this.joinOrCreateSyncRun("processUntilDone");
1797
1807
  return this.processUntilDoneWithRun(runKey.runStartedAt, object, params);
1798
1808
  }
1799
1809
  /**
@@ -2408,14 +2418,14 @@ var StripeSync = class {
2408
2418
  throw error;
2409
2419
  }
2410
2420
  }
2411
- async fetchAndUpsert(fetch, upsert, accountId, resourceName, runStartedAt) {
2421
+ async fetchAndUpsert(fetch2, upsert, accountId, resourceName, runStartedAt) {
2412
2422
  const CHECKPOINT_SIZE = 100;
2413
2423
  let totalSynced = 0;
2414
2424
  let currentBatch = [];
2415
2425
  try {
2416
2426
  this.config.logger?.info("Fetching items to sync from Stripe");
2417
2427
  try {
2418
- for await (const item of fetch()) {
2428
+ for await (const item of fetch2()) {
2419
2429
  currentBatch.push(item);
2420
2430
  if (currentBatch.length >= CHECKPOINT_SIZE) {
2421
2431
  this.config.logger?.info(`Upserting batch of ${currentBatch.length} items`);
@@ -3141,11 +3151,11 @@ var StripeSync = class {
3141
3151
  }
3142
3152
  }
3143
3153
  }
3144
- async fetchMissingEntities(ids, fetch) {
3154
+ async fetchMissingEntities(ids, fetch2) {
3145
3155
  if (!ids.length) return [];
3146
3156
  const entities = [];
3147
3157
  for (const id of ids) {
3148
- const entity = await fetch(id);
3158
+ const entity = await fetch2(id);
3149
3159
  entities.push(entity);
3150
3160
  }
3151
3161
  return entities;
@@ -3252,6 +3262,167 @@ async function runMigrations(config) {
3252
3262
  }
3253
3263
  }
3254
3264
 
3265
+ // src/websocket-client.ts
3266
+ import WebSocket from "ws";
3267
+ var CLI_VERSION = "1.33.0";
3268
+ var PONG_WAIT = 10 * 1e3;
3269
+ var PING_PERIOD = PONG_WAIT * 2 / 10;
3270
+ function getClientUserAgent() {
3271
+ return JSON.stringify({
3272
+ name: "stripe-cli",
3273
+ version: CLI_VERSION,
3274
+ publisher: "stripe",
3275
+ os: process.platform
3276
+ });
3277
+ }
3278
+ async function createCliSession(stripeApiKey) {
3279
+ const params = new URLSearchParams();
3280
+ params.append("device_name", "stripe-sync-engine");
3281
+ params.append("websocket_features[]", "webhooks");
3282
+ const response = await fetch("https://api.stripe.com/v1/stripecli/sessions", {
3283
+ method: "POST",
3284
+ headers: {
3285
+ Authorization: `Bearer ${stripeApiKey}`,
3286
+ "Content-Type": "application/x-www-form-urlencoded",
3287
+ "User-Agent": `Stripe/v1 stripe-cli/${CLI_VERSION}`,
3288
+ "X-Stripe-Client-User-Agent": getClientUserAgent()
3289
+ },
3290
+ body: params.toString()
3291
+ });
3292
+ if (!response.ok) {
3293
+ const error = await response.text();
3294
+ throw new Error(`Failed to create CLI session: ${error}`);
3295
+ }
3296
+ return await response.json();
3297
+ }
3298
+ async function createStripeWebSocketClient(options) {
3299
+ const { stripeApiKey, onEvent, onReady, onError, onClose } = options;
3300
+ const session = await createCliSession(stripeApiKey);
3301
+ let ws = null;
3302
+ let pingInterval = null;
3303
+ let connected = false;
3304
+ let shouldReconnect = true;
3305
+ function connect() {
3306
+ const wsUrl = `${session.websocket_url}?websocket_feature=${encodeURIComponent(session.websocket_authorized_feature)}`;
3307
+ ws = new WebSocket(wsUrl, {
3308
+ headers: {
3309
+ "Accept-Encoding": "identity",
3310
+ "User-Agent": `Stripe/v1 stripe-cli/${CLI_VERSION}`,
3311
+ "X-Stripe-Client-User-Agent": getClientUserAgent(),
3312
+ "Websocket-Id": session.websocket_id
3313
+ }
3314
+ });
3315
+ ws.on("pong", () => {
3316
+ });
3317
+ ws.on("open", () => {
3318
+ connected = true;
3319
+ pingInterval = setInterval(() => {
3320
+ if (ws && ws.readyState === WebSocket.OPEN) {
3321
+ ws.ping();
3322
+ }
3323
+ }, PING_PERIOD);
3324
+ if (onReady) {
3325
+ onReady(session.secret);
3326
+ }
3327
+ });
3328
+ ws.on("message", async (data) => {
3329
+ try {
3330
+ const message = JSON.parse(data.toString());
3331
+ const ack = {
3332
+ type: "event_ack",
3333
+ event_id: message.webhook_id,
3334
+ webhook_conversation_id: message.webhook_conversation_id,
3335
+ webhook_id: message.webhook_id
3336
+ };
3337
+ if (ws && ws.readyState === WebSocket.OPEN) {
3338
+ ws.send(JSON.stringify(ack));
3339
+ }
3340
+ let response;
3341
+ try {
3342
+ const result = await onEvent(message);
3343
+ response = {
3344
+ type: "webhook_response",
3345
+ webhook_id: message.webhook_id,
3346
+ webhook_conversation_id: message.webhook_conversation_id,
3347
+ forward_url: "stripe-sync-engine",
3348
+ status: result?.status ?? 200,
3349
+ http_headers: {},
3350
+ body: JSON.stringify({
3351
+ event_type: result?.event_type,
3352
+ event_id: result?.event_id,
3353
+ database_url: result?.databaseUrl,
3354
+ error: result?.error
3355
+ }),
3356
+ request_headers: message.http_headers,
3357
+ request_body: message.event_payload,
3358
+ notification_id: message.webhook_id
3359
+ };
3360
+ } catch (err) {
3361
+ const errorMessage = err instanceof Error ? err.message : String(err);
3362
+ response = {
3363
+ type: "webhook_response",
3364
+ webhook_id: message.webhook_id,
3365
+ webhook_conversation_id: message.webhook_conversation_id,
3366
+ forward_url: "stripe-sync-engine",
3367
+ status: 500,
3368
+ http_headers: {},
3369
+ body: JSON.stringify({ error: errorMessage }),
3370
+ request_headers: message.http_headers,
3371
+ request_body: message.event_payload,
3372
+ notification_id: message.webhook_id
3373
+ };
3374
+ if (onError) {
3375
+ onError(err instanceof Error ? err : new Error(errorMessage));
3376
+ }
3377
+ }
3378
+ if (ws && ws.readyState === WebSocket.OPEN) {
3379
+ ws.send(JSON.stringify(response));
3380
+ }
3381
+ } catch (err) {
3382
+ if (onError) {
3383
+ onError(err instanceof Error ? err : new Error(String(err)));
3384
+ }
3385
+ }
3386
+ });
3387
+ ws.on("error", (error) => {
3388
+ if (onError) {
3389
+ onError(error);
3390
+ }
3391
+ });
3392
+ ws.on("close", (code, reason) => {
3393
+ connected = false;
3394
+ if (pingInterval) {
3395
+ clearInterval(pingInterval);
3396
+ pingInterval = null;
3397
+ }
3398
+ if (onClose) {
3399
+ onClose(code, reason.toString());
3400
+ }
3401
+ if (shouldReconnect) {
3402
+ const delay = (session.reconnect_delay || 5) * 1e3;
3403
+ setTimeout(() => {
3404
+ connect();
3405
+ }, delay);
3406
+ }
3407
+ });
3408
+ }
3409
+ connect();
3410
+ return {
3411
+ close: () => {
3412
+ shouldReconnect = false;
3413
+ if (pingInterval) {
3414
+ clearInterval(pingInterval);
3415
+ pingInterval = null;
3416
+ }
3417
+ if (ws) {
3418
+ ws.close(1e3, "Connection Done");
3419
+ ws = null;
3420
+ }
3421
+ },
3422
+ isConnected: () => connected
3423
+ };
3424
+ }
3425
+
3255
3426
  // src/index.ts
3256
3427
  var VERSION = package_default.version;
3257
3428
 
@@ -3260,5 +3431,6 @@ export {
3260
3431
  hashApiKey,
3261
3432
  StripeSync,
3262
3433
  runMigrations,
3434
+ createStripeWebSocketClient,
3263
3435
  VERSION
3264
3436
  };
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "stripe-experiment-sync",
4
- version: "1.0.7",
4
+ version: "1.0.8-beta.1765872477",
5
5
  private: false,
6
6
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
7
7
  type: "module",
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  StripeSync,
3
+ createStripeWebSocketClient,
3
4
  runMigrations
4
- } from "./chunk-7JWRDXNB.js";
5
+ } from "./chunk-E7LIOBPP.js";
5
6
  import {
6
7
  install,
7
8
  uninstall
8
- } from "./chunk-SX3HLE4H.js";
9
+ } from "./chunk-E2HSDYSR.js";
9
10
 
10
11
  // src/cli/config.ts
11
12
  import dotenv from "dotenv";
@@ -35,20 +36,6 @@ async function loadConfig(options) {
35
36
  }
36
37
  });
37
38
  }
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
39
  if (!config.databaseUrl) {
53
40
  questions.push({
54
41
  type: "password",
@@ -299,10 +286,19 @@ async function syncCommand(options) {
299
286
  let tunnel = null;
300
287
  let server = null;
301
288
  let webhookId = null;
289
+ let wsClient = null;
302
290
  const cleanup = async (signal) => {
303
291
  console.log(chalk3.blue(`
304
292
 
305
293
  Cleaning up... (signal: ${signal || "manual"})`));
294
+ if (wsClient) {
295
+ try {
296
+ wsClient.close();
297
+ console.log(chalk3.green("\u2713 WebSocket closed"));
298
+ } catch {
299
+ console.log(chalk3.yellow("\u26A0 Could not close WebSocket"));
300
+ }
301
+ }
306
302
  const keepWebhooksOnShutdown = process.env.KEEP_WEBHOOKS_ON_SHUTDOWN === "true";
307
303
  if (webhookId && stripeSync && !keepWebhooksOnShutdown) {
308
304
  try {
@@ -346,7 +342,12 @@ Cleaning up... (signal: ${signal || "manual"})`));
346
342
  process.on("SIGTERM", () => cleanup("SIGTERM"));
347
343
  try {
348
344
  const config = await loadConfig(options);
349
- console.log(chalk3.gray(`$ stripe-sync start ${config.databaseUrl}`));
345
+ const useWebSocketMode = !config.ngrokAuthToken;
346
+ const modeLabel = useWebSocketMode ? "WebSocket" : "Webhook (ngrok)";
347
+ console.log(chalk3.blue(`
348
+ Mode: ${modeLabel}`));
349
+ const maskedDbUrl = config.databaseUrl.replace(/:[^:@]+@/, ":****@");
350
+ console.log(chalk3.gray(`Database: ${maskedDbUrl}`));
350
351
  try {
351
352
  await runMigrations({
352
353
  databaseUrl: config.databaseUrl
@@ -371,53 +372,90 @@ Cleaning up... (signal: ${signal || "manual"})`));
371
372
  backfillRelatedEntities: process.env.BACKFILL_RELATED_ENTITIES !== "false",
372
373
  poolConfig
373
374
  });
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(`
375
+ const databaseUrlWithoutPassword = config.databaseUrl.replace(/:[^:@]+@/, ":****@");
376
+ if (useWebSocketMode) {
377
+ console.log(chalk3.blue("\nConnecting to Stripe WebSocket..."));
378
+ wsClient = await createStripeWebSocketClient({
379
+ stripeApiKey: config.stripeApiKey,
380
+ onEvent: async (event) => {
381
+ try {
382
+ const payload = JSON.parse(event.event_payload);
383
+ console.log(chalk3.cyan(`\u2190 ${payload.type}`) + chalk3.gray(` (${payload.id})`));
384
+ if (stripeSync) {
385
+ await stripeSync.processEvent(payload);
386
+ return {
387
+ status: 200,
388
+ event_type: payload.type,
389
+ event_id: payload.id,
390
+ databaseUrl: databaseUrlWithoutPassword
391
+ };
392
+ }
393
+ } catch (err) {
394
+ console.error(chalk3.red("Error processing event:"), err);
395
+ return {
396
+ status: 500,
397
+ databaseUrl: databaseUrlWithoutPassword,
398
+ error: err instanceof Error ? err.message : String(err)
399
+ };
400
+ }
401
+ },
402
+ onReady: (secret) => {
403
+ console.log(chalk3.green("\u2713 Connected to Stripe WebSocket"));
404
+ const maskedSecret = secret.length > 14 ? `${secret.slice(0, 10)}...${secret.slice(-4)}` : "****";
405
+ console.log(chalk3.gray(` Webhook secret: ${maskedSecret}`));
406
+ },
407
+ onError: (error) => {
408
+ console.error(chalk3.red("WebSocket error:"), error.message);
409
+ },
410
+ onClose: (code, reason) => {
411
+ console.log(chalk3.yellow(`WebSocket closed: ${code} - ${reason}`));
412
+ }
413
+ });
414
+ } else {
415
+ const port = 3e3;
416
+ tunnel = await createTunnel(port, config.ngrokAuthToken);
417
+ const webhookPath = process.env.WEBHOOK_PATH || "/stripe-webhooks";
418
+ console.log(chalk3.blue("\nCreating Stripe webhook endpoint..."));
419
+ const webhook = await stripeSync.findOrCreateManagedWebhook(`${tunnel.url}${webhookPath}`);
420
+ webhookId = webhook.id;
421
+ const eventCount = webhook.enabled_events?.length || 0;
422
+ console.log(chalk3.green(`\u2713 Webhook created: ${webhook.id}`));
423
+ console.log(chalk3.cyan(` URL: ${webhook.url}`));
424
+ console.log(chalk3.cyan(` Events: ${eventCount} supported events`));
425
+ const app = express();
426
+ const webhookRoute = webhookPath;
427
+ app.use(webhookRoute, express.raw({ type: "application/json" }));
428
+ app.post(webhookRoute, async (req, res) => {
429
+ const sig = req.headers["stripe-signature"];
430
+ if (!sig || typeof sig !== "string") {
431
+ console.error("[Webhook] Missing stripe-signature header");
432
+ return res.status(400).send({ error: "Missing stripe-signature header" });
433
+ }
434
+ const rawBody = req.body;
435
+ if (!rawBody || !Buffer.isBuffer(rawBody)) {
436
+ console.error("[Webhook] Body is not a Buffer!");
437
+ return res.status(400).send({ error: "Missing raw body for signature verification" });
438
+ }
439
+ try {
440
+ await stripeSync.processWebhook(rawBody, sig);
441
+ return res.status(200).send({ received: true });
442
+ } catch (error) {
443
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
444
+ console.error("[Webhook] Processing error:", errorMessage);
445
+ return res.status(400).send({ error: errorMessage });
446
+ }
447
+ });
448
+ app.use(express.json());
449
+ app.use(express.urlencoded({ extended: false }));
450
+ app.get("/health", async (req, res) => res.status(200).json({ status: "ok" }));
451
+ console.log(chalk3.blue(`
413
452
  Starting server on port ${port}...`));
414
- await new Promise((resolve, reject) => {
415
- server = app.listen(port, "0.0.0.0", () => {
416
- resolve();
453
+ await new Promise((resolve, reject) => {
454
+ server = app.listen(port, "0.0.0.0", () => resolve());
455
+ server.on("error", reject);
417
456
  });
418
- server.on("error", reject);
419
- });
420
- console.log(chalk3.green(`\u2713 Server started on port ${port}`));
457
+ console.log(chalk3.green(`\u2713 Server started on port ${port}`));
458
+ }
421
459
  if (process.env.SKIP_BACKFILL !== "true") {
422
460
  console.log(chalk3.blue("\nStarting initial sync of all Stripe data..."));
423
461
  const syncResult = await stripeSync.processUntilDone();
@@ -494,7 +532,8 @@ async function installCommand(options) {
494
532
  await install({
495
533
  supabaseAccessToken: accessToken,
496
534
  supabaseProjectRef: projectRef,
497
- stripeKey
535
+ stripeKey,
536
+ packageVersion: options.packageVersion
498
537
  });
499
538
  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
539
  console.log(chalk3.cyan.bold(" Installation Complete!"));