stripe-experiment-sync 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -33,24 +33,25 @@ __export(index_exports, {
33
33
  PostgresClient: () => PostgresClient,
34
34
  StripeSync: () => StripeSync,
35
35
  VERSION: () => VERSION,
36
+ createStripeWebSocketClient: () => createStripeWebSocketClient,
36
37
  hashApiKey: () => hashApiKey,
37
38
  runMigrations: () => runMigrations
38
39
  });
39
40
  module.exports = __toCommonJS(index_exports);
40
41
 
41
- // ../../node_modules/.pnpm/tsup@8.5.0_postcss@8.5.6_tsx@4.20.6_typescript@5.9.3_yaml@2.8.1/node_modules/tsup/assets/cjs_shims.js
42
+ // ../../node_modules/.pnpm/tsup@8.5.0_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3_yaml@2.8.1/node_modules/tsup/assets/cjs_shims.js
42
43
  var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.src || new URL("main.js", document.baseURI).href;
43
44
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
44
45
 
45
46
  // package.json
46
47
  var package_default = {
47
48
  name: "stripe-experiment-sync",
48
- version: "1.0.6",
49
+ version: "1.0.8-beta.1765856228",
49
50
  private: false,
50
51
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
51
52
  type: "module",
52
53
  main: "./dist/index.cjs",
53
- bin: "./dist/cli/index.cjs",
54
+ bin: "./dist/cli/index.js",
54
55
  exports: {
55
56
  ".": {
56
57
  types: "./dist/index.d.ts",
@@ -61,12 +62,17 @@ var package_default = {
61
62
  types: "./dist/supabase/index.d.ts",
62
63
  import: "./dist/supabase/index.js",
63
64
  require: "./dist/supabase/index.cjs"
65
+ },
66
+ "./cli": {
67
+ types: "./dist/cli/lib.d.ts",
68
+ import: "./dist/cli/lib.js",
69
+ require: "./dist/cli/lib.cjs"
64
70
  }
65
71
  },
66
72
  scripts: {
67
73
  clean: "rimraf dist",
68
74
  prebuild: "npm run clean",
69
- build: "tsup src/index.ts src/supabase/index.ts --format esm,cjs --dts --shims && cp -r src/database/migrations dist/migrations",
75
+ build: "tsup src/index.ts src/supabase/index.ts src/cli/index.ts src/cli/lib.ts --format esm,cjs --dts --shims && cp -r src/database/migrations dist/migrations",
70
76
  lint: "eslint src --ext .ts",
71
77
  test: "vitest"
72
78
  },
@@ -74,21 +80,28 @@ var package_default = {
74
80
  "dist"
75
81
  ],
76
82
  dependencies: {
83
+ "@ngrok/ngrok": "^1.4.1",
84
+ chalk: "^5.3.0",
85
+ commander: "^12.1.0",
86
+ dotenv: "^16.4.7",
87
+ express: "^4.18.2",
88
+ inquirer: "^12.3.0",
77
89
  pg: "^8.16.3",
78
90
  "pg-node-migrations": "0.0.8",
91
+ stripe: "^17.7.0",
79
92
  "supabase-management-js": "^0.1.6",
80
93
  ws: "^8.18.0",
81
94
  yesql: "^7.0.0"
82
95
  },
83
- peerDependencies: {
84
- stripe: "> 11"
85
- },
86
96
  devDependencies: {
97
+ "@types/express": "^4.17.21",
98
+ "@types/inquirer": "^9.0.7",
87
99
  "@types/node": "^24.10.1",
88
100
  "@types/pg": "^8.15.5",
89
101
  "@types/ws": "^8.5.13",
90
102
  "@types/yesql": "^4.1.4",
91
103
  "@vitest/ui": "^4.0.9",
104
+ tsx: "^4.19.2",
92
105
  vitest: "^3.2.4"
93
106
  },
94
107
  repository: {
@@ -143,9 +156,9 @@ var ORDERED_STRIPE_TABLES = [
143
156
  "reviews",
144
157
  "_managed_webhooks",
145
158
  "customers",
146
- "_sync_obj_run",
147
- // Must be deleted before _sync_run (foreign key)
148
- "_sync_run"
159
+ "_sync_obj_runs",
160
+ // Must be deleted before _sync_runs (foreign key)
161
+ "_sync_runs"
149
162
  ];
150
163
  var TABLES_WITH_ACCOUNT_ID = /* @__PURE__ */ new Set(["_managed_webhooks"]);
151
164
  var PostgresClient = class {
@@ -436,7 +449,7 @@ var PostgresClient = class {
436
449
  // Observable Sync System Methods
437
450
  // =============================================================================
438
451
  // These methods support long-running syncs with full observability.
439
- // Uses two tables: _sync_run (parent) and _sync_obj_run (children)
452
+ // Uses two tables: _sync_runs (parent) and _sync_obj_runs (children)
440
453
  // RunKey = (accountId, runStartedAt) - natural composite key
441
454
  /**
442
455
  * Cancel stale runs (running but no object updated in 5 minutes).
@@ -446,7 +459,7 @@ var PostgresClient = class {
446
459
  */
447
460
  async cancelStaleRuns(accountId) {
448
461
  await this.query(
449
- `UPDATE "${this.config.schema}"."_sync_obj_run" o
462
+ `UPDATE "${this.config.schema}"."_sync_obj_runs" o
450
463
  SET status = 'error',
451
464
  error_message = 'Auto-cancelled: stale (no update in 5 min)',
452
465
  completed_at = now()
@@ -456,17 +469,17 @@ var PostgresClient = class {
456
469
  [accountId]
457
470
  );
458
471
  await this.query(
459
- `UPDATE "${this.config.schema}"."_sync_run" r
472
+ `UPDATE "${this.config.schema}"."_sync_runs" r
460
473
  SET closed_at = now()
461
474
  WHERE r."_account_id" = $1
462
475
  AND r.closed_at IS NULL
463
476
  AND EXISTS (
464
- SELECT 1 FROM "${this.config.schema}"."_sync_obj_run" o
477
+ SELECT 1 FROM "${this.config.schema}"."_sync_obj_runs" o
465
478
  WHERE o."_account_id" = r."_account_id"
466
479
  AND o.run_started_at = r.started_at
467
480
  )
468
481
  AND NOT EXISTS (
469
- SELECT 1 FROM "${this.config.schema}"."_sync_obj_run" o
482
+ SELECT 1 FROM "${this.config.schema}"."_sync_obj_runs" o
470
483
  WHERE o."_account_id" = r."_account_id"
471
484
  AND o.run_started_at = r.started_at
472
485
  AND o.status IN ('pending', 'running')
@@ -484,7 +497,7 @@ var PostgresClient = class {
484
497
  async getOrCreateSyncRun(accountId, triggeredBy) {
485
498
  await this.cancelStaleRuns(accountId);
486
499
  const existing = await this.query(
487
- `SELECT "_account_id", started_at FROM "${this.config.schema}"."_sync_run"
500
+ `SELECT "_account_id", started_at FROM "${this.config.schema}"."_sync_runs"
488
501
  WHERE "_account_id" = $1 AND closed_at IS NULL`,
489
502
  [accountId]
490
503
  );
@@ -494,7 +507,7 @@ var PostgresClient = class {
494
507
  }
495
508
  try {
496
509
  const result = await this.query(
497
- `INSERT INTO "${this.config.schema}"."_sync_run" ("_account_id", triggered_by, started_at)
510
+ `INSERT INTO "${this.config.schema}"."_sync_runs" ("_account_id", triggered_by, started_at)
498
511
  VALUES ($1, $2, date_trunc('milliseconds', now()))
499
512
  RETURNING "_account_id", started_at`,
500
513
  [accountId, triggeredBy ?? null]
@@ -513,7 +526,7 @@ var PostgresClient = class {
513
526
  */
514
527
  async getActiveSyncRun(accountId) {
515
528
  const result = await this.query(
516
- `SELECT "_account_id", started_at FROM "${this.config.schema}"."_sync_run"
529
+ `SELECT "_account_id", started_at FROM "${this.config.schema}"."_sync_runs"
517
530
  WHERE "_account_id" = $1 AND closed_at IS NULL`,
518
531
  [accountId]
519
532
  );
@@ -523,12 +536,12 @@ var PostgresClient = class {
523
536
  }
524
537
  /**
525
538
  * Get sync run config (for concurrency control).
526
- * Status is derived from sync_dashboard view.
539
+ * Status is derived from sync_runs view.
527
540
  */
528
541
  async getSyncRun(accountId, runStartedAt) {
529
542
  const result = await this.query(
530
543
  `SELECT "_account_id", started_at, max_concurrent, closed_at
531
- FROM "${this.config.schema}"."_sync_run"
544
+ FROM "${this.config.schema}"."_sync_runs"
532
545
  WHERE "_account_id" = $1 AND started_at = $2`,
533
546
  [accountId, runStartedAt]
534
547
  );
@@ -547,7 +560,7 @@ var PostgresClient = class {
547
560
  */
548
561
  async closeSyncRun(accountId, runStartedAt) {
549
562
  await this.query(
550
- `UPDATE "${this.config.schema}"."_sync_run"
563
+ `UPDATE "${this.config.schema}"."_sync_runs"
551
564
  SET closed_at = now()
552
565
  WHERE "_account_id" = $1 AND started_at = $2 AND closed_at IS NULL`,
553
566
  [accountId, runStartedAt]
@@ -561,7 +574,7 @@ var PostgresClient = class {
561
574
  if (objects.length === 0) return;
562
575
  const values = objects.map((_, i) => `($1, $2, $${i + 3})`).join(", ");
563
576
  await this.query(
564
- `INSERT INTO "${this.config.schema}"."_sync_obj_run" ("_account_id", run_started_at, object)
577
+ `INSERT INTO "${this.config.schema}"."_sync_obj_runs" ("_account_id", run_started_at, object)
565
578
  VALUES ${values}
566
579
  ON CONFLICT ("_account_id", run_started_at, object) DO NOTHING`,
567
580
  [accountId, runStartedAt, ...objects]
@@ -580,7 +593,7 @@ var PostgresClient = class {
580
593
  const runningCount = await this.countRunningObjects(accountId, runStartedAt);
581
594
  if (runningCount >= run.maxConcurrent) return false;
582
595
  const result = await this.query(
583
- `UPDATE "${this.config.schema}"."_sync_obj_run"
596
+ `UPDATE "${this.config.schema}"."_sync_obj_runs"
584
597
  SET status = 'running', started_at = now(), updated_at = now()
585
598
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3 AND status = 'pending'
586
599
  RETURNING *`,
@@ -594,7 +607,7 @@ var PostgresClient = class {
594
607
  async getObjectRun(accountId, runStartedAt, object) {
595
608
  const result = await this.query(
596
609
  `SELECT object, status, processed_count, cursor
597
- FROM "${this.config.schema}"."_sync_obj_run"
610
+ FROM "${this.config.schema}"."_sync_obj_runs"
598
611
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
599
612
  [accountId, runStartedAt, object]
600
613
  );
@@ -613,7 +626,7 @@ var PostgresClient = class {
613
626
  */
614
627
  async incrementObjectProgress(accountId, runStartedAt, object, count) {
615
628
  await this.query(
616
- `UPDATE "${this.config.schema}"."_sync_obj_run"
629
+ `UPDATE "${this.config.schema}"."_sync_obj_runs"
617
630
  SET processed_count = processed_count + $4, updated_at = now()
618
631
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
619
632
  [accountId, runStartedAt, object, count]
@@ -629,7 +642,7 @@ var PostgresClient = class {
629
642
  const isNumeric = cursor !== null && /^\d+$/.test(cursor);
630
643
  if (isNumeric) {
631
644
  await this.query(
632
- `UPDATE "${this.config.schema}"."_sync_obj_run"
645
+ `UPDATE "${this.config.schema}"."_sync_obj_runs"
633
646
  SET cursor = GREATEST(COALESCE(cursor::bigint, 0), $4::bigint)::text,
634
647
  updated_at = now()
635
648
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
@@ -637,7 +650,7 @@ var PostgresClient = class {
637
650
  );
638
651
  } else {
639
652
  await this.query(
640
- `UPDATE "${this.config.schema}"."_sync_obj_run"
653
+ `UPDATE "${this.config.schema}"."_sync_obj_runs"
641
654
  SET cursor = $4, updated_at = now()
642
655
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
643
656
  [accountId, runStartedAt, object, cursor]
@@ -653,7 +666,7 @@ var PostgresClient = class {
653
666
  async getLastCompletedCursor(accountId, object) {
654
667
  const result = await this.query(
655
668
  `SELECT MAX(o.cursor::bigint)::text as cursor
656
- FROM "${this.config.schema}"."_sync_obj_run" o
669
+ FROM "${this.config.schema}"."_sync_obj_runs" o
657
670
  WHERE o."_account_id" = $1
658
671
  AND o.object = $2
659
672
  AND o.cursor IS NOT NULL`,
@@ -667,10 +680,10 @@ var PostgresClient = class {
667
680
  */
668
681
  async deleteSyncRuns(accountId) {
669
682
  await this.query(
670
- `DELETE FROM "${this.config.schema}"."_sync_obj_run" WHERE "_account_id" = $1`,
683
+ `DELETE FROM "${this.config.schema}"."_sync_obj_runs" WHERE "_account_id" = $1`,
671
684
  [accountId]
672
685
  );
673
- await this.query(`DELETE FROM "${this.config.schema}"."_sync_run" WHERE "_account_id" = $1`, [
686
+ await this.query(`DELETE FROM "${this.config.schema}"."_sync_runs" WHERE "_account_id" = $1`, [
674
687
  accountId
675
688
  ]);
676
689
  }
@@ -680,7 +693,7 @@ var PostgresClient = class {
680
693
  */
681
694
  async completeObjectSync(accountId, runStartedAt, object) {
682
695
  await this.query(
683
- `UPDATE "${this.config.schema}"."_sync_obj_run"
696
+ `UPDATE "${this.config.schema}"."_sync_obj_runs"
684
697
  SET status = 'complete', completed_at = now()
685
698
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
686
699
  [accountId, runStartedAt, object]
@@ -696,7 +709,7 @@ var PostgresClient = class {
696
709
  */
697
710
  async failObjectSync(accountId, runStartedAt, object, errorMessage) {
698
711
  await this.query(
699
- `UPDATE "${this.config.schema}"."_sync_obj_run"
712
+ `UPDATE "${this.config.schema}"."_sync_obj_runs"
700
713
  SET status = 'error', error_message = $4, completed_at = now()
701
714
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
702
715
  [accountId, runStartedAt, object, errorMessage]
@@ -711,7 +724,7 @@ var PostgresClient = class {
711
724
  */
712
725
  async hasAnyObjectErrors(accountId, runStartedAt) {
713
726
  const result = await this.query(
714
- `SELECT COUNT(*) as count FROM "${this.config.schema}"."_sync_obj_run"
727
+ `SELECT COUNT(*) as count FROM "${this.config.schema}"."_sync_obj_runs"
715
728
  WHERE "_account_id" = $1 AND run_started_at = $2 AND status = 'error'`,
716
729
  [accountId, runStartedAt]
717
730
  );
@@ -722,7 +735,7 @@ var PostgresClient = class {
722
735
  */
723
736
  async countRunningObjects(accountId, runStartedAt) {
724
737
  const result = await this.query(
725
- `SELECT COUNT(*) as count FROM "${this.config.schema}"."_sync_obj_run"
738
+ `SELECT COUNT(*) as count FROM "${this.config.schema}"."_sync_obj_runs"
726
739
  WHERE "_account_id" = $1 AND run_started_at = $2 AND status = 'running'`,
727
740
  [accountId, runStartedAt]
728
741
  );
@@ -738,7 +751,7 @@ var PostgresClient = class {
738
751
  const runningCount = await this.countRunningObjects(accountId, runStartedAt);
739
752
  if (runningCount >= run.maxConcurrent) return null;
740
753
  const result = await this.query(
741
- `SELECT object FROM "${this.config.schema}"."_sync_obj_run"
754
+ `SELECT object FROM "${this.config.schema}"."_sync_obj_runs"
742
755
  WHERE "_account_id" = $1 AND run_started_at = $2 AND status = 'pending'
743
756
  ORDER BY object
744
757
  LIMIT 1`,
@@ -751,7 +764,7 @@ var PostgresClient = class {
751
764
  */
752
765
  async areAllObjectsComplete(accountId, runStartedAt) {
753
766
  const result = await this.query(
754
- `SELECT COUNT(*) as count FROM "${this.config.schema}"."_sync_obj_run"
767
+ `SELECT COUNT(*) as count FROM "${this.config.schema}"."_sync_obj_runs"
755
768
  WHERE "_account_id" = $1 AND run_started_at = $2 AND status IN ('pending', 'running')`,
756
769
  [accountId, runStartedAt]
757
770
  );
@@ -1717,19 +1730,8 @@ var StripeSync = class {
1717
1730
  if (params?.runStartedAt) {
1718
1731
  runStartedAt = params.runStartedAt;
1719
1732
  } else {
1720
- const runKey = await this.postgresClient.getOrCreateSyncRun(
1721
- accountId,
1722
- params?.triggeredBy ?? "processNext"
1723
- );
1724
- if (!runKey) {
1725
- const activeRun = await this.postgresClient.getActiveSyncRun(accountId);
1726
- if (!activeRun) {
1727
- throw new Error("Failed to get or create sync run");
1728
- }
1729
- runStartedAt = activeRun.runStartedAt;
1730
- } else {
1731
- runStartedAt = runKey.runStartedAt;
1732
- }
1733
+ const { runKey } = await this.joinOrCreateSyncRun(params?.triggeredBy ?? "processNext");
1734
+ runStartedAt = runKey.runStartedAt;
1733
1735
  }
1734
1736
  await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
1735
1737
  const objRun = await this.postgresClient.getObjectRun(accountId, runStartedAt, resourceName);
@@ -1892,18 +1894,39 @@ var StripeSync = class {
1892
1894
  }
1893
1895
  return { synced: totalSynced };
1894
1896
  }
1895
- async processUntilDone(params) {
1896
- const { object } = params ?? { object: "all" };
1897
+ /**
1898
+ * Join existing sync run or create a new one.
1899
+ * Returns sync run key and list of supported objects to sync.
1900
+ *
1901
+ * Cooperative behavior: If a sync run already exists, joins it instead of failing.
1902
+ * This is used by workers and background processes that should cooperate.
1903
+ *
1904
+ * @param triggeredBy - What triggered this sync (for observability)
1905
+ * @returns Run key and list of objects to sync
1906
+ */
1907
+ async joinOrCreateSyncRun(triggeredBy = "worker") {
1897
1908
  await this.getCurrentAccount();
1898
1909
  const accountId = await this.getAccountId();
1899
- const runKey = await this.postgresClient.getOrCreateSyncRun(accountId, "processUntilDone");
1900
- if (!runKey) {
1910
+ const result = await this.postgresClient.getOrCreateSyncRun(accountId, triggeredBy);
1911
+ if (!result) {
1901
1912
  const activeRun = await this.postgresClient.getActiveSyncRun(accountId);
1902
1913
  if (!activeRun) {
1903
1914
  throw new Error("Failed to get or create sync run");
1904
1915
  }
1905
- return this.processUntilDoneWithRun(activeRun.runStartedAt, object, params);
1916
+ return {
1917
+ runKey: { accountId: activeRun.accountId, runStartedAt: activeRun.runStartedAt },
1918
+ objects: this.getSupportedSyncObjects()
1919
+ };
1906
1920
  }
1921
+ const { accountId: runAccountId, runStartedAt } = result;
1922
+ return {
1923
+ runKey: { accountId: runAccountId, runStartedAt },
1924
+ objects: this.getSupportedSyncObjects()
1925
+ };
1926
+ }
1927
+ async processUntilDone(params) {
1928
+ const { object } = params ?? { object: "all" };
1929
+ const { runKey } = await this.joinOrCreateSyncRun("processUntilDone");
1907
1930
  return this.processUntilDoneWithRun(runKey.runStartedAt, object, params);
1908
1931
  }
1909
1932
  /**
@@ -2518,14 +2541,14 @@ var StripeSync = class {
2518
2541
  throw error;
2519
2542
  }
2520
2543
  }
2521
- async fetchAndUpsert(fetch, upsert, accountId, resourceName, runStartedAt) {
2544
+ async fetchAndUpsert(fetch2, upsert, accountId, resourceName, runStartedAt) {
2522
2545
  const CHECKPOINT_SIZE = 100;
2523
2546
  let totalSynced = 0;
2524
2547
  let currentBatch = [];
2525
2548
  try {
2526
2549
  this.config.logger?.info("Fetching items to sync from Stripe");
2527
2550
  try {
2528
- for await (const item of fetch()) {
2551
+ for await (const item of fetch2()) {
2529
2552
  currentBatch.push(item);
2530
2553
  if (currentBatch.length >= CHECKPOINT_SIZE) {
2531
2554
  this.config.logger?.info(`Upserting batch of ${currentBatch.length} items`);
@@ -3251,11 +3274,11 @@ var StripeSync = class {
3251
3274
  }
3252
3275
  }
3253
3276
  }
3254
- async fetchMissingEntities(ids, fetch) {
3277
+ async fetchMissingEntities(ids, fetch2) {
3255
3278
  if (!ids.length) return [];
3256
3279
  const entities = [];
3257
3280
  for (const id of ids) {
3258
- const entity = await fetch(id);
3281
+ const entity = await fetch2(id);
3259
3282
  entities.push(entity);
3260
3283
  }
3261
3284
  return entities;
@@ -3362,6 +3385,167 @@ async function runMigrations(config) {
3362
3385
  }
3363
3386
  }
3364
3387
 
3388
+ // src/websocket-client.ts
3389
+ var import_ws = __toESM(require("ws"), 1);
3390
+ var CLI_VERSION = "1.33.0";
3391
+ var PONG_WAIT = 10 * 1e3;
3392
+ var PING_PERIOD = PONG_WAIT * 2 / 10;
3393
+ function getClientUserAgent() {
3394
+ return JSON.stringify({
3395
+ name: "stripe-cli",
3396
+ version: CLI_VERSION,
3397
+ publisher: "stripe",
3398
+ os: process.platform
3399
+ });
3400
+ }
3401
+ async function createCliSession(stripeApiKey) {
3402
+ const params = new URLSearchParams();
3403
+ params.append("device_name", "stripe-sync-engine");
3404
+ params.append("websocket_features[]", "webhooks");
3405
+ const response = await fetch("https://api.stripe.com/v1/stripecli/sessions", {
3406
+ method: "POST",
3407
+ headers: {
3408
+ Authorization: `Bearer ${stripeApiKey}`,
3409
+ "Content-Type": "application/x-www-form-urlencoded",
3410
+ "User-Agent": `Stripe/v1 stripe-cli/${CLI_VERSION}`,
3411
+ "X-Stripe-Client-User-Agent": getClientUserAgent()
3412
+ },
3413
+ body: params.toString()
3414
+ });
3415
+ if (!response.ok) {
3416
+ const error = await response.text();
3417
+ throw new Error(`Failed to create CLI session: ${error}`);
3418
+ }
3419
+ return await response.json();
3420
+ }
3421
+ async function createStripeWebSocketClient(options) {
3422
+ const { stripeApiKey, onEvent, onReady, onError, onClose } = options;
3423
+ const session = await createCliSession(stripeApiKey);
3424
+ let ws = null;
3425
+ let pingInterval = null;
3426
+ let connected = false;
3427
+ let shouldReconnect = true;
3428
+ function connect() {
3429
+ const wsUrl = `${session.websocket_url}?websocket_feature=${encodeURIComponent(session.websocket_authorized_feature)}`;
3430
+ ws = new import_ws.default(wsUrl, {
3431
+ headers: {
3432
+ "Accept-Encoding": "identity",
3433
+ "User-Agent": `Stripe/v1 stripe-cli/${CLI_VERSION}`,
3434
+ "X-Stripe-Client-User-Agent": getClientUserAgent(),
3435
+ "Websocket-Id": session.websocket_id
3436
+ }
3437
+ });
3438
+ ws.on("pong", () => {
3439
+ });
3440
+ ws.on("open", () => {
3441
+ connected = true;
3442
+ pingInterval = setInterval(() => {
3443
+ if (ws && ws.readyState === import_ws.default.OPEN) {
3444
+ ws.ping();
3445
+ }
3446
+ }, PING_PERIOD);
3447
+ if (onReady) {
3448
+ onReady(session.secret);
3449
+ }
3450
+ });
3451
+ ws.on("message", async (data) => {
3452
+ try {
3453
+ const message = JSON.parse(data.toString());
3454
+ const ack = {
3455
+ type: "event_ack",
3456
+ event_id: message.webhook_id,
3457
+ webhook_conversation_id: message.webhook_conversation_id,
3458
+ webhook_id: message.webhook_id
3459
+ };
3460
+ if (ws && ws.readyState === import_ws.default.OPEN) {
3461
+ ws.send(JSON.stringify(ack));
3462
+ }
3463
+ let response;
3464
+ try {
3465
+ const result = await onEvent(message);
3466
+ response = {
3467
+ type: "webhook_response",
3468
+ webhook_id: message.webhook_id,
3469
+ webhook_conversation_id: message.webhook_conversation_id,
3470
+ forward_url: "stripe-sync-engine",
3471
+ status: result?.status ?? 200,
3472
+ http_headers: {},
3473
+ body: JSON.stringify({
3474
+ event_type: result?.event_type,
3475
+ event_id: result?.event_id,
3476
+ database_url: result?.databaseUrl,
3477
+ error: result?.error
3478
+ }),
3479
+ request_headers: message.http_headers,
3480
+ request_body: message.event_payload,
3481
+ notification_id: message.webhook_id
3482
+ };
3483
+ } catch (err) {
3484
+ const errorMessage = err instanceof Error ? err.message : String(err);
3485
+ response = {
3486
+ type: "webhook_response",
3487
+ webhook_id: message.webhook_id,
3488
+ webhook_conversation_id: message.webhook_conversation_id,
3489
+ forward_url: "stripe-sync-engine",
3490
+ status: 500,
3491
+ http_headers: {},
3492
+ body: JSON.stringify({ error: errorMessage }),
3493
+ request_headers: message.http_headers,
3494
+ request_body: message.event_payload,
3495
+ notification_id: message.webhook_id
3496
+ };
3497
+ if (onError) {
3498
+ onError(err instanceof Error ? err : new Error(errorMessage));
3499
+ }
3500
+ }
3501
+ if (ws && ws.readyState === import_ws.default.OPEN) {
3502
+ ws.send(JSON.stringify(response));
3503
+ }
3504
+ } catch (err) {
3505
+ if (onError) {
3506
+ onError(err instanceof Error ? err : new Error(String(err)));
3507
+ }
3508
+ }
3509
+ });
3510
+ ws.on("error", (error) => {
3511
+ if (onError) {
3512
+ onError(error);
3513
+ }
3514
+ });
3515
+ ws.on("close", (code, reason) => {
3516
+ connected = false;
3517
+ if (pingInterval) {
3518
+ clearInterval(pingInterval);
3519
+ pingInterval = null;
3520
+ }
3521
+ if (onClose) {
3522
+ onClose(code, reason.toString());
3523
+ }
3524
+ if (shouldReconnect) {
3525
+ const delay = (session.reconnect_delay || 5) * 1e3;
3526
+ setTimeout(() => {
3527
+ connect();
3528
+ }, delay);
3529
+ }
3530
+ });
3531
+ }
3532
+ connect();
3533
+ return {
3534
+ close: () => {
3535
+ shouldReconnect = false;
3536
+ if (pingInterval) {
3537
+ clearInterval(pingInterval);
3538
+ pingInterval = null;
3539
+ }
3540
+ if (ws) {
3541
+ ws.close(1e3, "Connection Done");
3542
+ ws = null;
3543
+ }
3544
+ },
3545
+ isConnected: () => connected
3546
+ };
3547
+ }
3548
+
3365
3549
  // src/index.ts
3366
3550
  var VERSION = package_default.version;
3367
3551
  // Annotate the CommonJS export names for ESM import in node:
@@ -3369,6 +3553,7 @@ var VERSION = package_default.version;
3369
3553
  PostgresClient,
3370
3554
  StripeSync,
3371
3555
  VERSION,
3556
+ createStripeWebSocketClient,
3372
3557
  hashApiKey,
3373
3558
  runMigrations
3374
3559
  });
package/dist/index.d.cts CHANGED
@@ -105,7 +105,7 @@ declare class PostgresClient {
105
105
  } | null>;
106
106
  /**
107
107
  * Get sync run config (for concurrency control).
108
- * Status is derived from sync_dashboard view.
108
+ * Status is derived from sync_runs view.
109
109
  */
110
110
  getSyncRun(accountId: string, runStartedAt: Date): Promise<{
111
111
  accountId: string;
@@ -335,6 +335,13 @@ interface ProcessNextParams extends SyncParams {
335
335
  triggeredBy?: string;
336
336
  }
337
337
 
338
+ /**
339
+ * Identifies a specific sync run.
340
+ */
341
+ type RunKey = {
342
+ accountId: string;
343
+ runStartedAt: Date;
344
+ };
338
345
  declare class StripeSync {
339
346
  private config;
340
347
  stripe: Stripe;
@@ -470,6 +477,20 @@ declare class StripeSync {
470
477
  * @returns Sync result with count of synced items
471
478
  */
472
479
  private processObjectUntilDone;
480
+ /**
481
+ * Join existing sync run or create a new one.
482
+ * Returns sync run key and list of supported objects to sync.
483
+ *
484
+ * Cooperative behavior: If a sync run already exists, joins it instead of failing.
485
+ * This is used by workers and background processes that should cooperate.
486
+ *
487
+ * @param triggeredBy - What triggered this sync (for observability)
488
+ * @returns Run key and list of objects to sync
489
+ */
490
+ joinOrCreateSyncRun(triggeredBy?: string): Promise<{
491
+ runKey: RunKey;
492
+ objects: Exclude<SyncObject, 'all' | 'customer_with_entitlements'>[];
493
+ }>;
473
494
  processUntilDone(params?: SyncParams): Promise<SyncBackfill>;
474
495
  /**
475
496
  * Internal implementation of processUntilDone with an existing run.
@@ -600,6 +621,37 @@ declare function runMigrations(config: MigrationConfig): Promise<void>;
600
621
  */
601
622
  declare function hashApiKey(apiKey: string): string;
602
623
 
624
+ interface WebhookProcessingResult {
625
+ status: number;
626
+ databaseUrl: string;
627
+ event_type?: string;
628
+ event_id?: string;
629
+ error?: string;
630
+ }
631
+ interface StripeWebSocketOptions {
632
+ stripeApiKey: string;
633
+ onEvent: (event: StripeWebhookEvent) => Promise<WebhookProcessingResult | void> | WebhookProcessingResult | void;
634
+ onReady?: (secret: string) => void;
635
+ onError?: (error: Error) => void;
636
+ onClose?: (code: number, reason: string) => void;
637
+ }
638
+ interface StripeWebSocketClient {
639
+ close: () => void;
640
+ isConnected: () => boolean;
641
+ }
642
+ interface StripeWebhookEvent {
643
+ type: string;
644
+ webhook_id: string;
645
+ webhook_conversation_id: string;
646
+ event_payload: string;
647
+ http_headers: Record<string, string>;
648
+ endpoint: {
649
+ url: string;
650
+ status: string;
651
+ };
652
+ }
653
+ declare function createStripeWebSocketClient(options: StripeWebSocketOptions): Promise<StripeWebSocketClient>;
654
+
603
655
  declare const VERSION: string;
604
656
 
605
- export { type Logger, PostgresClient, type ProcessNextParams, type ProcessNextResult, type RevalidateEntity, StripeSync, type StripeSyncConfig, type Sync, type SyncBackfill, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, type SyncParams, VERSION, hashApiKey, runMigrations };
657
+ export { type Logger, PostgresClient, type ProcessNextParams, type ProcessNextResult, type RevalidateEntity, StripeSync, type StripeSyncConfig, type StripeWebSocketClient, type StripeWebSocketOptions, type StripeWebhookEvent, type Sync, type SyncBackfill, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, type SyncParams, VERSION, type WebhookProcessingResult, createStripeWebSocketClient, hashApiKey, runMigrations };