stripe-experiment-sync 1.0.13 → 1.0.15-beta.1766078819

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/cli/lib.cjs CHANGED
@@ -59,6 +59,7 @@ async function loadConfig(options) {
59
59
  config.stripeApiKey = options.stripeKey || process.env.STRIPE_API_KEY || "";
60
60
  config.ngrokAuthToken = options.ngrokToken || process.env.NGROK_AUTH_TOKEN || "";
61
61
  config.databaseUrl = options.databaseUrl || process.env.DATABASE_URL || "";
62
+ config.enableSigma = options.enableSigma ?? (process.env.ENABLE_SIGMA !== void 0 ? process.env.ENABLE_SIGMA === "true" : void 0);
62
63
  const questions = [];
63
64
  if (!config.stripeApiKey) {
64
65
  questions.push({
@@ -70,8 +71,8 @@ async function loadConfig(options) {
70
71
  if (!input || input.trim() === "") {
71
72
  return "Stripe API key is required";
72
73
  }
73
- if (!input.startsWith("sk_")) {
74
- return 'Stripe API key should start with "sk_"';
74
+ if (!input.startsWith("sk_") && !input.startsWith("rk_")) {
75
+ return 'Stripe API key should start with "sk_" or "rk_"';
75
76
  }
76
77
  return true;
77
78
  }
@@ -94,18 +95,29 @@ async function loadConfig(options) {
94
95
  }
95
96
  });
96
97
  }
98
+ if (config.enableSigma === void 0) {
99
+ questions.push({
100
+ type: "confirm",
101
+ name: "enableSigma",
102
+ message: "Enable Sigma sync? (Requires Sigma access in Stripe API key)",
103
+ default: false
104
+ });
105
+ }
97
106
  if (questions.length > 0) {
98
- console.log(import_chalk.default.yellow("\nMissing required configuration. Please provide:"));
107
+ console.log(import_chalk.default.yellow("\nMissing configuration. Please provide:"));
99
108
  const answers = await import_inquirer.default.prompt(questions);
100
109
  Object.assign(config, answers);
101
110
  }
111
+ if (config.enableSigma === void 0) {
112
+ config.enableSigma = false;
113
+ }
102
114
  return config;
103
115
  }
104
116
 
105
117
  // package.json
106
118
  var package_default = {
107
119
  name: "stripe-experiment-sync",
108
- version: "1.0.13",
120
+ version: "1.0.15-beta.1766078819",
109
121
  private: false,
110
122
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
111
123
  type: "module",
@@ -145,6 +157,7 @@ var package_default = {
145
157
  dotenv: "^16.4.7",
146
158
  express: "^4.18.2",
147
159
  inquirer: "^12.3.0",
160
+ papaparse: "5.4.1",
148
161
  pg: "^8.16.3",
149
162
  "pg-node-migrations": "0.0.8",
150
163
  stripe: "^17.7.0",
@@ -156,6 +169,7 @@ var package_default = {
156
169
  "@types/express": "^4.17.21",
157
170
  "@types/inquirer": "^9.0.7",
158
171
  "@types/node": "^24.10.1",
172
+ "@types/papaparse": "5.3.16",
159
173
  "@types/pg": "^8.15.5",
160
174
  "@types/ws": "^8.5.13",
161
175
  "@types/yesql": "^4.1.4",
@@ -185,14 +199,60 @@ var package_default = {
185
199
  };
186
200
 
187
201
  // src/stripeSync.ts
188
- var import_stripe2 = __toESM(require("stripe"), 1);
202
+ var import_stripe3 = __toESM(require("stripe"), 1);
189
203
  var import_yesql2 = require("yesql");
190
204
 
191
205
  // src/database/postgres.ts
192
206
  var import_pg = __toESM(require("pg"), 1);
193
207
  var import_yesql = require("yesql");
208
+
209
+ // src/database/QueryUtils.ts
210
+ var QueryUtils = class _QueryUtils {
211
+ constructor() {
212
+ }
213
+ static quoteIdent(name) {
214
+ return `"${name}"`;
215
+ }
216
+ static quotedList(names) {
217
+ return names.map(_QueryUtils.quoteIdent).join(", ");
218
+ }
219
+ static buildInsertParts(columns) {
220
+ const columnsSql = columns.map((c) => _QueryUtils.quoteIdent(c.column)).join(", ");
221
+ const valuesSql = columns.map((c, i) => {
222
+ const placeholder = `$${i + 1}`;
223
+ return `${placeholder}::${c.pgType}`;
224
+ }).join(", ");
225
+ const params = columns.map((c) => c.value);
226
+ return { columnsSql, valuesSql, params };
227
+ }
228
+ static buildRawJsonUpsertQuery(schema, table, columns, conflictTarget) {
229
+ const { columnsSql, valuesSql, params } = _QueryUtils.buildInsertParts(columns);
230
+ const conflictSql = _QueryUtils.quotedList(conflictTarget);
231
+ const tsParamIdx = columns.findIndex((c) => c.column === "_last_synced_at") + 1;
232
+ if (tsParamIdx <= 0) {
233
+ throw new Error("buildRawJsonUpsertQuery requires _last_synced_at column");
234
+ }
235
+ const sql3 = `
236
+ INSERT INTO ${_QueryUtils.quoteIdent(schema)}.${_QueryUtils.quoteIdent(table)} (${columnsSql})
237
+ VALUES (${valuesSql})
238
+ ON CONFLICT (${conflictSql})
239
+ DO UPDATE SET
240
+ "_raw_data" = EXCLUDED."_raw_data",
241
+ "_last_synced_at" = $${tsParamIdx},
242
+ "_account_id" = EXCLUDED."_account_id"
243
+ WHERE ${_QueryUtils.quoteIdent(table)}."_last_synced_at" IS NULL
244
+ OR ${_QueryUtils.quoteIdent(table)}."_last_synced_at" < $${tsParamIdx}
245
+ RETURNING *
246
+ `;
247
+ return { sql: sql3, params };
248
+ }
249
+ };
250
+
251
+ // src/database/postgres.ts
194
252
  var ORDERED_STRIPE_TABLES = [
253
+ "exchange_rates_from_usd",
195
254
  "subscription_items",
255
+ "subscription_item_change_events_v2_beta",
196
256
  "subscriptions",
197
257
  "subscription_schedules",
198
258
  "checkout_session_line_items",
@@ -262,7 +322,7 @@ var PostgresClient = class {
262
322
  }
263
323
  return results.flatMap((it) => it.rows);
264
324
  }
265
- async upsertManyWithTimestampProtection(entries, table, accountId, syncTimestamp) {
325
+ async upsertManyWithTimestampProtection(entries, table, accountId, syncTimestamp, upsertOptions) {
266
326
  const timestamp = syncTimestamp || (/* @__PURE__ */ new Date()).toISOString();
267
327
  if (!entries.length) return [];
268
328
  const chunkSize = 5;
@@ -297,20 +357,33 @@ var PostgresClient = class {
297
357
  const prepared = (0, import_yesql.pg)(upsertSql, { useNullForMissing: true })(cleansed);
298
358
  queries.push(this.pool.query(prepared.text, prepared.values));
299
359
  } else {
300
- const rawData = JSON.stringify(entry);
301
- const upsertSql = `
302
- INSERT INTO "${this.config.schema}"."${table}" ("_raw_data", "_last_synced_at", "_account_id")
303
- VALUES ($1::jsonb, $2, $3)
304
- ON CONFLICT (id)
305
- DO UPDATE SET
306
- "_raw_data" = EXCLUDED."_raw_data",
307
- "_last_synced_at" = $2,
308
- "_account_id" = EXCLUDED."_account_id"
309
- WHERE "${table}"."_last_synced_at" IS NULL
310
- OR "${table}"."_last_synced_at" < $2
311
- RETURNING *
312
- `;
313
- queries.push(this.pool.query(upsertSql, [rawData, timestamp, accountId]));
360
+ const conflictTarget = upsertOptions?.conflictTarget ?? ["id"];
361
+ const extraColumns = upsertOptions?.extraColumns ?? [];
362
+ if (!conflictTarget.length) {
363
+ throw new Error(`Invalid upsert config for ${table}: conflictTarget must be non-empty`);
364
+ }
365
+ const columns = [
366
+ { column: "_raw_data", pgType: "jsonb", value: JSON.stringify(entry) },
367
+ ...extraColumns.map((c) => ({
368
+ column: c.column,
369
+ pgType: c.pgType,
370
+ value: entry[c.entryKey]
371
+ })),
372
+ { column: "_last_synced_at", pgType: "timestamptz", value: timestamp },
373
+ { column: "_account_id", pgType: "text", value: accountId }
374
+ ];
375
+ for (const c of columns) {
376
+ if (c.value === void 0) {
377
+ throw new Error(`Missing required value for ${table}.${c.column}`);
378
+ }
379
+ }
380
+ const { sql: upsertSql, params } = QueryUtils.buildRawJsonUpsertQuery(
381
+ this.config.schema,
382
+ table,
383
+ columns,
384
+ conflictTarget
385
+ );
386
+ queries.push(this.pool.query(upsertSql, params));
314
387
  }
315
388
  });
316
389
  results.push(...await Promise.all(queries));
@@ -710,7 +783,12 @@ var PostgresClient = class {
710
783
  } else {
711
784
  await this.query(
712
785
  `UPDATE "${this.config.schema}"."_sync_obj_runs"
713
- SET cursor = $4, updated_at = now()
786
+ SET cursor = CASE
787
+ WHEN cursor IS NULL THEN $4
788
+ WHEN (cursor COLLATE "C") < ($4::text COLLATE "C") THEN $4
789
+ ELSE cursor
790
+ END,
791
+ updated_at = now()
714
792
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
715
793
  [accountId, runStartedAt, object, cursor]
716
794
  );
@@ -721,10 +799,17 @@ var PostgresClient = class {
721
799
  * This considers completed, error, AND running runs to ensure recovery syncs
722
800
  * don't re-process data that was already synced before a crash.
723
801
  * A 'running' status with a cursor means the process was killed mid-sync.
802
+ *
803
+ * Handles two cursor formats:
804
+ * - Numeric: compared as bigint for correct ordering
805
+ * - Composite cursors: compared as strings with COLLATE "C"
724
806
  */
725
807
  async getLastCompletedCursor(accountId, object) {
726
808
  const result = await this.query(
727
- `SELECT MAX(o.cursor::bigint)::text as cursor
809
+ `SELECT CASE
810
+ WHEN BOOL_OR(o.cursor !~ '^\\d+$') THEN MAX(o.cursor COLLATE "C")
811
+ ELSE MAX(CASE WHEN o.cursor ~ '^\\d+$' THEN o.cursor::bigint END)::text
812
+ END as cursor
728
813
  FROM "${this.config.schema}"."_sync_obj_runs" o
729
814
  WHERE o."_account_id" = $1
730
815
  AND o.object = $2
@@ -1009,6 +1094,269 @@ function hashApiKey(apiKey) {
1009
1094
  return (0, import_crypto.createHash)("sha256").update(apiKey).digest("hex");
1010
1095
  }
1011
1096
 
1097
+ // src/sigma/sigmaApi.ts
1098
+ var import_papaparse = __toESM(require("papaparse"), 1);
1099
+ var import_stripe2 = __toESM(require("stripe"), 1);
1100
+ var STRIPE_FILES_BASE = "https://files.stripe.com/v1";
1101
+ function sleep2(ms) {
1102
+ return new Promise((resolve) => setTimeout(resolve, ms));
1103
+ }
1104
+ function parseCsvObjects(csv) {
1105
+ const input = csv.replace(/^\uFEFF/, "");
1106
+ const parsed = import_papaparse.default.parse(input, {
1107
+ header: true,
1108
+ skipEmptyLines: "greedy"
1109
+ });
1110
+ if (parsed.errors.length > 0) {
1111
+ throw new Error(`Failed to parse Sigma CSV: ${parsed.errors[0]?.message ?? "unknown error"}`);
1112
+ }
1113
+ return parsed.data.filter((row) => row && Object.keys(row).length > 0).map(
1114
+ (row) => Object.fromEntries(
1115
+ Object.entries(row).map(([k, v]) => [k, v == null || v === "" ? null : String(v)])
1116
+ )
1117
+ );
1118
+ }
1119
+ function normalizeSigmaTimestampToIso(value) {
1120
+ const v = value.trim();
1121
+ if (!v) return null;
1122
+ const hasExplicitTz = /z$|[+-]\d{2}:?\d{2}$/i.test(v);
1123
+ const isoish = v.includes("T") ? v : v.replace(" ", "T");
1124
+ const candidate = hasExplicitTz ? isoish : `${isoish}Z`;
1125
+ const d = new Date(candidate);
1126
+ if (Number.isNaN(d.getTime())) return null;
1127
+ return d.toISOString();
1128
+ }
1129
+ async function fetchStripeText(url, apiKey, options) {
1130
+ const res = await fetch(url, {
1131
+ ...options,
1132
+ headers: {
1133
+ ...options.headers ?? {},
1134
+ Authorization: `Bearer ${apiKey}`
1135
+ }
1136
+ });
1137
+ const text = await res.text();
1138
+ if (!res.ok) {
1139
+ throw new Error(`Sigma file download error (${res.status}) for ${url}: ${text}`);
1140
+ }
1141
+ return text;
1142
+ }
1143
+ async function runSigmaQueryAndDownloadCsv(params) {
1144
+ const pollTimeoutMs = params.pollTimeoutMs ?? 5 * 60 * 1e3;
1145
+ const pollIntervalMs = params.pollIntervalMs ?? 2e3;
1146
+ const stripe = new import_stripe2.default(params.apiKey);
1147
+ const created = await stripe.rawRequest("POST", "/v1/sigma/query_runs", {
1148
+ sql: params.sql
1149
+ });
1150
+ const queryRunId = created.id;
1151
+ const start = Date.now();
1152
+ let current = created;
1153
+ while (current.status === "running") {
1154
+ if (Date.now() - start > pollTimeoutMs) {
1155
+ throw new Error(`Sigma query run timed out after ${pollTimeoutMs}ms: ${queryRunId}`);
1156
+ }
1157
+ await sleep2(pollIntervalMs);
1158
+ current = await stripe.rawRequest(
1159
+ "GET",
1160
+ `/v1/sigma/query_runs/${queryRunId}`,
1161
+ {}
1162
+ );
1163
+ }
1164
+ if (current.status !== "succeeded") {
1165
+ throw new Error(
1166
+ `Sigma query run did not succeed (status=${current.status}) id=${queryRunId} error=${JSON.stringify(
1167
+ current.error
1168
+ )}`
1169
+ );
1170
+ }
1171
+ const fileId = current.result?.file;
1172
+ if (!fileId) {
1173
+ throw new Error(`Sigma query run succeeded but result.file is missing (id=${queryRunId})`);
1174
+ }
1175
+ const csv = await fetchStripeText(
1176
+ `${STRIPE_FILES_BASE}/files/${fileId}/contents`,
1177
+ params.apiKey,
1178
+ { method: "GET" }
1179
+ );
1180
+ return { queryRunId, fileId, csv };
1181
+ }
1182
+
1183
+ // src/sigma/sigmaIngestionConfigs.ts
1184
+ var SIGMA_INGESTION_CONFIGS = {
1185
+ subscription_item_change_events_v2_beta: {
1186
+ sigmaTable: "subscription_item_change_events_v2_beta",
1187
+ destinationTable: "subscription_item_change_events_v2_beta",
1188
+ pageSize: 1e4,
1189
+ cursor: {
1190
+ version: 1,
1191
+ columns: [
1192
+ { column: "event_timestamp", type: "timestamp" },
1193
+ { column: "event_type", type: "string" },
1194
+ { column: "subscription_item_id", type: "string" }
1195
+ ]
1196
+ },
1197
+ upsert: {
1198
+ conflictTarget: ["_account_id", "event_timestamp", "event_type", "subscription_item_id"],
1199
+ extraColumns: [
1200
+ { column: "event_timestamp", pgType: "timestamptz", entryKey: "event_timestamp" },
1201
+ { column: "event_type", pgType: "text", entryKey: "event_type" },
1202
+ { column: "subscription_item_id", pgType: "text", entryKey: "subscription_item_id" }
1203
+ ]
1204
+ }
1205
+ },
1206
+ exchange_rates_from_usd: {
1207
+ sigmaTable: "exchange_rates_from_usd",
1208
+ destinationTable: "exchange_rates_from_usd",
1209
+ pageSize: 1e4,
1210
+ cursor: {
1211
+ version: 1,
1212
+ columns: [
1213
+ { column: "date", type: "string" },
1214
+ { column: "sell_currency", type: "string" }
1215
+ ]
1216
+ },
1217
+ upsert: {
1218
+ conflictTarget: ["_account_id", "date", "sell_currency"],
1219
+ extraColumns: [
1220
+ { column: "date", pgType: "date", entryKey: "date" },
1221
+ { column: "sell_currency", pgType: "text", entryKey: "sell_currency" }
1222
+ ]
1223
+ }
1224
+ }
1225
+ };
1226
+
1227
+ // src/sigma/sigmaIngestion.ts
1228
+ var SIGMA_CURSOR_DELIM = "";
1229
+ function escapeSigmaSqlStringLiteral(value) {
1230
+ return value.replace(/'/g, "''");
1231
+ }
1232
+ function formatSigmaTimestampForSqlLiteral(date) {
1233
+ return date.toISOString().replace("T", " ").replace("Z", "");
1234
+ }
1235
+ function decodeSigmaCursorValues(spec, cursor) {
1236
+ const prefix = `v${spec.version}${SIGMA_CURSOR_DELIM}`;
1237
+ if (!cursor.startsWith(prefix)) {
1238
+ throw new Error(
1239
+ `Unrecognized Sigma cursor format (expected prefix ${JSON.stringify(prefix)}): ${cursor}`
1240
+ );
1241
+ }
1242
+ const parts = cursor.split(SIGMA_CURSOR_DELIM);
1243
+ const expected = 1 + spec.columns.length;
1244
+ if (parts.length !== expected) {
1245
+ throw new Error(`Malformed Sigma cursor: expected ${expected} parts, got ${parts.length}`);
1246
+ }
1247
+ return parts.slice(1);
1248
+ }
1249
+ function encodeSigmaCursor(spec, values) {
1250
+ if (values.length !== spec.columns.length) {
1251
+ throw new Error(
1252
+ `Cannot encode Sigma cursor: expected ${spec.columns.length} values, got ${values.length}`
1253
+ );
1254
+ }
1255
+ for (const v of values) {
1256
+ if (v.includes(SIGMA_CURSOR_DELIM)) {
1257
+ throw new Error("Cannot encode Sigma cursor: value contains delimiter character");
1258
+ }
1259
+ }
1260
+ return [`v${spec.version}`, ...values].join(SIGMA_CURSOR_DELIM);
1261
+ }
1262
+ function sigmaSqlLiteralForCursorValue(spec, rawValue) {
1263
+ switch (spec.type) {
1264
+ case "timestamp": {
1265
+ const d = new Date(rawValue);
1266
+ if (Number.isNaN(d.getTime())) {
1267
+ throw new Error(`Invalid timestamp cursor value for ${spec.column}: ${rawValue}`);
1268
+ }
1269
+ return `timestamp '${formatSigmaTimestampForSqlLiteral(d)}'`;
1270
+ }
1271
+ case "number": {
1272
+ if (!/^-?\d+(\.\d+)?$/.test(rawValue)) {
1273
+ throw new Error(`Invalid numeric cursor value for ${spec.column}: ${rawValue}`);
1274
+ }
1275
+ return rawValue;
1276
+ }
1277
+ case "string":
1278
+ return `'${escapeSigmaSqlStringLiteral(rawValue)}'`;
1279
+ }
1280
+ }
1281
+ function buildSigmaCursorWhereClause(spec, cursorValues) {
1282
+ if (cursorValues.length !== spec.columns.length) {
1283
+ throw new Error(
1284
+ `Cannot build Sigma cursor predicate: expected ${spec.columns.length} values, got ${cursorValues.length}`
1285
+ );
1286
+ }
1287
+ const cols = spec.columns.map((c) => c.column);
1288
+ const lits = spec.columns.map((c, i) => sigmaSqlLiteralForCursorValue(c, cursorValues[i] ?? ""));
1289
+ const ors = [];
1290
+ for (let i = 0; i < cols.length; i++) {
1291
+ const ands = [];
1292
+ for (let j = 0; j < i; j++) {
1293
+ ands.push(`${cols[j]} = ${lits[j]}`);
1294
+ }
1295
+ ands.push(`${cols[i]} > ${lits[i]}`);
1296
+ ors.push(`(${ands.join(" AND ")})`);
1297
+ }
1298
+ return ors.join(" OR ");
1299
+ }
1300
+ function buildSigmaQuery(config, cursor) {
1301
+ const select = config.select === void 0 || config.select === "*" ? "*" : config.select.join(", ");
1302
+ const whereParts = [];
1303
+ if (config.additionalWhere) {
1304
+ whereParts.push(`(${config.additionalWhere})`);
1305
+ }
1306
+ if (cursor) {
1307
+ const values = decodeSigmaCursorValues(config.cursor, cursor);
1308
+ const predicate = buildSigmaCursorWhereClause(config.cursor, values);
1309
+ whereParts.push(`(${predicate})`);
1310
+ }
1311
+ const whereClause = whereParts.length > 0 ? `WHERE ${whereParts.join(" AND ")}` : "";
1312
+ const orderBy = config.cursor.columns.map((c) => c.column).join(", ");
1313
+ return [
1314
+ `SELECT ${select} FROM ${config.sigmaTable}`,
1315
+ whereClause,
1316
+ `ORDER BY ${orderBy} ASC`,
1317
+ `LIMIT ${config.pageSize}`
1318
+ ].filter(Boolean).join(" ");
1319
+ }
1320
+ function defaultSigmaRowToEntry(config, row) {
1321
+ const out = { ...row };
1322
+ for (const col of config.cursor.columns) {
1323
+ const raw = row[col.column];
1324
+ if (raw == null) {
1325
+ throw new Error(`Sigma row missing required cursor column: ${col.column}`);
1326
+ }
1327
+ if (col.type === "timestamp") {
1328
+ const normalized = normalizeSigmaTimestampToIso(raw);
1329
+ if (!normalized) {
1330
+ throw new Error(`Sigma row has invalid timestamp for ${col.column}: ${raw}`);
1331
+ }
1332
+ out[col.column] = normalized;
1333
+ } else if (col.type === "string") {
1334
+ const v = raw.trim();
1335
+ if (!v) {
1336
+ throw new Error(`Sigma row has empty string for required cursor column: ${col.column}`);
1337
+ }
1338
+ out[col.column] = v;
1339
+ } else {
1340
+ const v = raw.trim();
1341
+ if (!v) {
1342
+ throw new Error(`Sigma row has empty value for required cursor column: ${col.column}`);
1343
+ }
1344
+ out[col.column] = v;
1345
+ }
1346
+ }
1347
+ return out;
1348
+ }
1349
+ function sigmaCursorFromEntry(config, entry) {
1350
+ const values = config.cursor.columns.map((c) => {
1351
+ const raw = entry[c.column];
1352
+ if (raw == null) {
1353
+ throw new Error(`Cannot build cursor: entry missing ${c.column}`);
1354
+ }
1355
+ return String(raw);
1356
+ });
1357
+ return encodeSigmaCursor(config.cursor, values);
1358
+ }
1359
+
1012
1360
  // src/stripeSync.ts
1013
1361
  function getUniqueIds(entries, key) {
1014
1362
  const set = new Set(
@@ -1019,7 +1367,7 @@ function getUniqueIds(entries, key) {
1019
1367
  var StripeSync = class {
1020
1368
  constructor(config) {
1021
1369
  this.config = config;
1022
- const baseStripe = new import_stripe2.default(config.stripeSecretKey, {
1370
+ const baseStripe = new import_stripe3.default(config.stripeSecretKey, {
1023
1371
  // https://github.com/stripe/stripe-node#configuration
1024
1372
  // @ts-ignore
1025
1373
  apiVersion: config.stripeApiVersion,
@@ -1420,6 +1768,17 @@ var StripeSync = class {
1420
1768
  listFn: (p) => this.stripe.checkout.sessions.list(p),
1421
1769
  upsertFn: (items, id) => this.upsertCheckoutSessions(items, id),
1422
1770
  supportsCreatedFilter: true
1771
+ },
1772
+ // Sigma-backed resources
1773
+ subscription_item_change_events_v2_beta: {
1774
+ order: 18,
1775
+ supportsCreatedFilter: false,
1776
+ sigma: SIGMA_INGESTION_CONFIGS.subscription_item_change_events_v2_beta
1777
+ },
1778
+ exchange_rates_from_usd: {
1779
+ order: 19,
1780
+ supportsCreatedFilter: false,
1781
+ sigma: SIGMA_INGESTION_CONFIGS.exchange_rates_from_usd
1423
1782
  }
1424
1783
  };
1425
1784
  async processEvent(event) {
@@ -1452,7 +1811,13 @@ var StripeSync = class {
1452
1811
  * Order is determined by the `order` field in resourceRegistry.
1453
1812
  */
1454
1813
  getSupportedSyncObjects() {
1455
- return Object.entries(this.resourceRegistry).sort(([, a], [, b]) => a.order - b.order).map(([key]) => key);
1814
+ const all = Object.entries(this.resourceRegistry).sort(([, a], [, b]) => a.order - b.order).map(([key]) => key);
1815
+ if (!this.config.enableSigma) {
1816
+ return all.filter(
1817
+ (o) => o !== "subscription_item_change_events_v2_beta" && o !== "exchange_rates_from_usd"
1818
+ );
1819
+ }
1820
+ return all;
1456
1821
  }
1457
1822
  // Event handler methods
1458
1823
  async handleChargeEvent(event, accountId) {
@@ -1531,7 +1896,7 @@ var StripeSync = class {
1531
1896
  );
1532
1897
  await this.upsertProducts([product], accountId, this.getSyncTimestamp(event, refetched));
1533
1898
  } catch (err) {
1534
- if (err instanceof import_stripe2.default.errors.StripeAPIError && err.code === "resource_missing") {
1899
+ if (err instanceof import_stripe3.default.errors.StripeAPIError && err.code === "resource_missing") {
1535
1900
  const product = event.data.object;
1536
1901
  await this.deleteProduct(product.id);
1537
1902
  } else {
@@ -1551,7 +1916,7 @@ var StripeSync = class {
1551
1916
  );
1552
1917
  await this.upsertPrices([price], accountId, false, this.getSyncTimestamp(event, refetched));
1553
1918
  } catch (err) {
1554
- if (err instanceof import_stripe2.default.errors.StripeAPIError && err.code === "resource_missing") {
1919
+ if (err instanceof import_stripe3.default.errors.StripeAPIError && err.code === "resource_missing") {
1555
1920
  const price = event.data.object;
1556
1921
  await this.deletePrice(price.id);
1557
1922
  } else {
@@ -1571,7 +1936,7 @@ var StripeSync = class {
1571
1936
  );
1572
1937
  await this.upsertPlans([plan], accountId, false, this.getSyncTimestamp(event, refetched));
1573
1938
  } catch (err) {
1574
- if (err instanceof import_stripe2.default.errors.StripeAPIError && err.code === "resource_missing") {
1939
+ if (err instanceof import_stripe3.default.errors.StripeAPIError && err.code === "resource_missing") {
1575
1940
  const plan = event.data.object;
1576
1941
  await this.deletePlan(plan.id);
1577
1942
  } else {
@@ -1818,10 +2183,10 @@ var StripeSync = class {
1818
2183
  let cursor = null;
1819
2184
  if (!params?.created) {
1820
2185
  if (objRun?.cursor) {
1821
- cursor = parseInt(objRun.cursor);
2186
+ cursor = objRun.cursor;
1822
2187
  } else {
1823
2188
  const lastCursor = await this.postgresClient.getLastCompletedCursor(accountId, resourceName);
1824
- cursor = lastCursor ? parseInt(lastCursor) : null;
2189
+ cursor = lastCursor ?? null;
1825
2190
  }
1826
2191
  }
1827
2192
  const result = await this.fetchOnePage(
@@ -1876,9 +2241,18 @@ var StripeSync = class {
1876
2241
  throw new Error(`Unsupported object type for processNext: ${object}`);
1877
2242
  }
1878
2243
  try {
2244
+ if (config.sigma) {
2245
+ return await this.fetchOneSigmaPage(
2246
+ accountId,
2247
+ resourceName,
2248
+ runStartedAt,
2249
+ cursor,
2250
+ config.sigma
2251
+ );
2252
+ }
1879
2253
  const listParams = { limit };
1880
2254
  if (config.supportsCreatedFilter) {
1881
- const created = params?.created ?? (cursor ? { gte: cursor } : void 0);
2255
+ const created = params?.created ?? (cursor && /^\d+$/.test(cursor) ? { gte: Number.parseInt(cursor, 10) } : void 0);
1882
2256
  if (created) {
1883
2257
  listParams.created = created;
1884
2258
  }
@@ -1923,6 +2297,97 @@ var StripeSync = class {
1923
2297
  throw error;
1924
2298
  }
1925
2299
  }
2300
+ async getSigmaFallbackCursorFromDestination(accountId, sigmaConfig) {
2301
+ const cursorCols = sigmaConfig.cursor.columns;
2302
+ const selectCols = cursorCols.map((c) => `"${c.column}"`).join(", ");
2303
+ const orderBy = cursorCols.map((c) => `"${c.column}" DESC`).join(", ");
2304
+ const result = await this.postgresClient.query(
2305
+ `SELECT ${selectCols}
2306
+ FROM "stripe"."${sigmaConfig.destinationTable}"
2307
+ WHERE "_account_id" = $1
2308
+ ORDER BY ${orderBy}
2309
+ LIMIT 1`,
2310
+ [accountId]
2311
+ );
2312
+ if (result.rows.length === 0) return null;
2313
+ const row = result.rows[0];
2314
+ const entryForCursor = {};
2315
+ for (const c of cursorCols) {
2316
+ const v = row[c.column];
2317
+ if (v == null) {
2318
+ throw new Error(
2319
+ `Sigma fallback cursor query returned null for ${sigmaConfig.destinationTable}.${c.column}`
2320
+ );
2321
+ }
2322
+ if (c.type === "timestamp") {
2323
+ const d = v instanceof Date ? v : new Date(String(v));
2324
+ if (Number.isNaN(d.getTime())) {
2325
+ throw new Error(
2326
+ `Sigma fallback cursor query returned invalid timestamp for ${sigmaConfig.destinationTable}.${c.column}: ${String(
2327
+ v
2328
+ )}`
2329
+ );
2330
+ }
2331
+ entryForCursor[c.column] = d.toISOString();
2332
+ } else {
2333
+ entryForCursor[c.column] = String(v);
2334
+ }
2335
+ }
2336
+ return sigmaCursorFromEntry(sigmaConfig, entryForCursor);
2337
+ }
2338
+ async fetchOneSigmaPage(accountId, resourceName, runStartedAt, cursor, sigmaConfig) {
2339
+ if (!this.config.stripeSecretKey) {
2340
+ throw new Error("Sigma sync requested but stripeSecretKey is not configured.");
2341
+ }
2342
+ if (resourceName !== sigmaConfig.destinationTable) {
2343
+ throw new Error(
2344
+ `Sigma sync config mismatch: resourceName=${resourceName} destinationTable=${sigmaConfig.destinationTable}`
2345
+ );
2346
+ }
2347
+ const effectiveCursor = cursor ?? await this.getSigmaFallbackCursorFromDestination(accountId, sigmaConfig);
2348
+ const sigmaSql = buildSigmaQuery(sigmaConfig, effectiveCursor);
2349
+ this.config.logger?.info(
2350
+ { object: resourceName, pageSize: sigmaConfig.pageSize, hasCursor: Boolean(effectiveCursor) },
2351
+ "Sigma sync: running query"
2352
+ );
2353
+ const { queryRunId, fileId, csv } = await runSigmaQueryAndDownloadCsv({
2354
+ apiKey: this.config.stripeSecretKey,
2355
+ sql: sigmaSql,
2356
+ logger: this.config.logger
2357
+ });
2358
+ const rows = parseCsvObjects(csv);
2359
+ if (rows.length === 0) {
2360
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
2361
+ return { processed: 0, hasMore: false, runStartedAt };
2362
+ }
2363
+ const entries = rows.map(
2364
+ (row) => defaultSigmaRowToEntry(sigmaConfig, row)
2365
+ );
2366
+ this.config.logger?.info(
2367
+ { object: resourceName, rows: entries.length, queryRunId, fileId },
2368
+ "Sigma sync: upserting rows"
2369
+ );
2370
+ await this.postgresClient.upsertManyWithTimestampProtection(
2371
+ entries,
2372
+ resourceName,
2373
+ accountId,
2374
+ void 0,
2375
+ sigmaConfig.upsert
2376
+ );
2377
+ await this.postgresClient.incrementObjectProgress(
2378
+ accountId,
2379
+ runStartedAt,
2380
+ resourceName,
2381
+ entries.length
2382
+ );
2383
+ const newCursor = sigmaCursorFromEntry(sigmaConfig, entries[entries.length - 1]);
2384
+ await this.postgresClient.updateObjectCursor(accountId, runStartedAt, resourceName, newCursor);
2385
+ const hasMore = rows.length === sigmaConfig.pageSize;
2386
+ if (!hasMore) {
2387
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
2388
+ }
2389
+ return { processed: entries.length, hasMore, runStartedAt };
2390
+ }
1926
2391
  /**
1927
2392
  * Process all pages for all (or specified) object types until complete.
1928
2393
  *
@@ -2051,6 +2516,12 @@ var StripeSync = class {
2051
2516
  case "checkout_sessions":
2052
2517
  results.checkoutSessions = result;
2053
2518
  break;
2519
+ case "subscription_item_change_events_v2_beta":
2520
+ results.subscriptionItemChangeEventsV2Beta = result;
2521
+ break;
2522
+ case "exchange_rates_from_usd":
2523
+ results.exchangeRatesFromUsd = result;
2524
+ break;
2054
2525
  }
2055
2526
  }
2056
2527
  }
@@ -2076,30 +2547,41 @@ var StripeSync = class {
2076
2547
  const customerIds = await this.postgresClient.query(prepared.text, prepared.values).then(({ rows }) => rows.map((it) => it.id));
2077
2548
  this.config.logger?.info(`Getting payment methods for ${customerIds.length} customers`);
2078
2549
  let synced = 0;
2079
- for (const customerIdChunk of chunkArray(customerIds, 10)) {
2550
+ const chunkSize = this.config.maxConcurrentCustomers ?? 10;
2551
+ for (const customerIdChunk of chunkArray(customerIds, chunkSize)) {
2080
2552
  await Promise.all(
2081
2553
  customerIdChunk.map(async (customerId) => {
2082
2554
  const CHECKPOINT_SIZE = 100;
2083
2555
  let currentBatch = [];
2084
- for await (const item of this.stripe.paymentMethods.list({
2085
- limit: 100,
2086
- customer: customerId
2087
- })) {
2088
- currentBatch.push(item);
2089
- if (currentBatch.length >= CHECKPOINT_SIZE) {
2090
- await this.upsertPaymentMethods(
2091
- currentBatch,
2092
- accountId,
2093
- syncParams?.backfillRelatedEntities
2094
- );
2095
- synced += currentBatch.length;
2096
- await this.postgresClient.incrementObjectProgress(
2097
- accountId,
2098
- runStartedAt,
2099
- resourceName,
2100
- currentBatch.length
2101
- );
2102
- currentBatch = [];
2556
+ let hasMore = true;
2557
+ let startingAfter = void 0;
2558
+ while (hasMore) {
2559
+ const response = await this.stripe.paymentMethods.list({
2560
+ limit: 100,
2561
+ customer: customerId,
2562
+ ...startingAfter ? { starting_after: startingAfter } : {}
2563
+ });
2564
+ for (const item of response.data) {
2565
+ currentBatch.push(item);
2566
+ if (currentBatch.length >= CHECKPOINT_SIZE) {
2567
+ await this.upsertPaymentMethods(
2568
+ currentBatch,
2569
+ accountId,
2570
+ syncParams?.backfillRelatedEntities
2571
+ );
2572
+ synced += currentBatch.length;
2573
+ await this.postgresClient.incrementObjectProgress(
2574
+ accountId,
2575
+ runStartedAt,
2576
+ resourceName,
2577
+ currentBatch.length
2578
+ );
2579
+ currentBatch = [];
2580
+ }
2581
+ }
2582
+ hasMore = response.has_more;
2583
+ if (response.data.length > 0) {
2584
+ startingAfter = response.data[response.data.length - 1].id;
2103
2585
  }
2104
2586
  }
2105
2587
  if (currentBatch.length > 0) {
@@ -2143,7 +2625,7 @@ var StripeSync = class {
2143
2625
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2144
2626
  }
2145
2627
  return this.fetchAndUpsert(
2146
- () => this.stripe.products.list(params),
2628
+ (pagination) => this.stripe.products.list({ ...params, ...pagination }),
2147
2629
  (products) => this.upsertProducts(products, accountId),
2148
2630
  accountId,
2149
2631
  "products",
@@ -2163,7 +2645,7 @@ var StripeSync = class {
2163
2645
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2164
2646
  }
2165
2647
  return this.fetchAndUpsert(
2166
- () => this.stripe.prices.list(params),
2648
+ (pagination) => this.stripe.prices.list({ ...params, ...pagination }),
2167
2649
  (prices) => this.upsertPrices(prices, accountId, syncParams?.backfillRelatedEntities),
2168
2650
  accountId,
2169
2651
  "prices",
@@ -2183,7 +2665,7 @@ var StripeSync = class {
2183
2665
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2184
2666
  }
2185
2667
  return this.fetchAndUpsert(
2186
- () => this.stripe.plans.list(params),
2668
+ (pagination) => this.stripe.plans.list({ ...params, ...pagination }),
2187
2669
  (plans) => this.upsertPlans(plans, accountId, syncParams?.backfillRelatedEntities),
2188
2670
  accountId,
2189
2671
  "plans",
@@ -2203,7 +2685,7 @@ var StripeSync = class {
2203
2685
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2204
2686
  }
2205
2687
  return this.fetchAndUpsert(
2206
- () => this.stripe.customers.list(params),
2688
+ (pagination) => this.stripe.customers.list({ ...params, ...pagination }),
2207
2689
  // @ts-expect-error
2208
2690
  (items) => this.upsertCustomers(items, accountId),
2209
2691
  accountId,
@@ -2224,7 +2706,7 @@ var StripeSync = class {
2224
2706
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2225
2707
  }
2226
2708
  return this.fetchAndUpsert(
2227
- () => this.stripe.subscriptions.list(params),
2709
+ (pagination) => this.stripe.subscriptions.list({ ...params, ...pagination }),
2228
2710
  (items) => this.upsertSubscriptions(items, accountId, syncParams?.backfillRelatedEntities),
2229
2711
  accountId,
2230
2712
  "subscriptions",
@@ -2247,7 +2729,7 @@ var StripeSync = class {
2247
2729
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2248
2730
  }
2249
2731
  return this.fetchAndUpsert(
2250
- () => this.stripe.subscriptionSchedules.list(params),
2732
+ (pagination) => this.stripe.subscriptionSchedules.list({ ...params, ...pagination }),
2251
2733
  (items) => this.upsertSubscriptionSchedules(items, accountId, syncParams?.backfillRelatedEntities),
2252
2734
  accountId,
2253
2735
  "subscription_schedules",
@@ -2268,7 +2750,7 @@ var StripeSync = class {
2268
2750
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2269
2751
  }
2270
2752
  return this.fetchAndUpsert(
2271
- () => this.stripe.invoices.list(params),
2753
+ (pagination) => this.stripe.invoices.list({ ...params, ...pagination }),
2272
2754
  (items) => this.upsertInvoices(items, accountId, syncParams?.backfillRelatedEntities),
2273
2755
  accountId,
2274
2756
  "invoices",
@@ -2288,7 +2770,7 @@ var StripeSync = class {
2288
2770
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2289
2771
  }
2290
2772
  return this.fetchAndUpsert(
2291
- () => this.stripe.charges.list(params),
2773
+ (pagination) => this.stripe.charges.list({ ...params, ...pagination }),
2292
2774
  (items) => this.upsertCharges(items, accountId, syncParams?.backfillRelatedEntities),
2293
2775
  accountId,
2294
2776
  "charges",
@@ -2308,7 +2790,7 @@ var StripeSync = class {
2308
2790
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2309
2791
  }
2310
2792
  return this.fetchAndUpsert(
2311
- () => this.stripe.setupIntents.list(params),
2793
+ (pagination) => this.stripe.setupIntents.list({ ...params, ...pagination }),
2312
2794
  (items) => this.upsertSetupIntents(items, accountId, syncParams?.backfillRelatedEntities),
2313
2795
  accountId,
2314
2796
  "setup_intents",
@@ -2331,7 +2813,7 @@ var StripeSync = class {
2331
2813
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2332
2814
  }
2333
2815
  return this.fetchAndUpsert(
2334
- () => this.stripe.paymentIntents.list(params),
2816
+ (pagination) => this.stripe.paymentIntents.list({ ...params, ...pagination }),
2335
2817
  (items) => this.upsertPaymentIntents(items, accountId, syncParams?.backfillRelatedEntities),
2336
2818
  accountId,
2337
2819
  "payment_intents",
@@ -2346,7 +2828,7 @@ var StripeSync = class {
2346
2828
  const accountId = await this.getAccountId();
2347
2829
  const params = { limit: 100 };
2348
2830
  return this.fetchAndUpsert(
2349
- () => this.stripe.taxIds.list(params),
2831
+ (pagination) => this.stripe.taxIds.list({ ...params, ...pagination }),
2350
2832
  (items) => this.upsertTaxIds(items, accountId, syncParams?.backfillRelatedEntities),
2351
2833
  accountId,
2352
2834
  "tax_ids",
@@ -2367,30 +2849,41 @@ var StripeSync = class {
2367
2849
  const customerIds = await this.postgresClient.query(prepared.text, prepared.values).then(({ rows }) => rows.map((it) => it.id));
2368
2850
  this.config.logger?.info(`Getting payment methods for ${customerIds.length} customers`);
2369
2851
  let synced = 0;
2370
- for (const customerIdChunk of chunkArray(customerIds, 10)) {
2852
+ const chunkSize = this.config.maxConcurrentCustomers ?? 10;
2853
+ for (const customerIdChunk of chunkArray(customerIds, chunkSize)) {
2371
2854
  await Promise.all(
2372
2855
  customerIdChunk.map(async (customerId) => {
2373
2856
  const CHECKPOINT_SIZE = 100;
2374
2857
  let currentBatch = [];
2375
- for await (const item of this.stripe.paymentMethods.list({
2376
- limit: 100,
2377
- customer: customerId
2378
- })) {
2379
- currentBatch.push(item);
2380
- if (currentBatch.length >= CHECKPOINT_SIZE) {
2381
- await this.upsertPaymentMethods(
2382
- currentBatch,
2383
- accountId,
2384
- syncParams?.backfillRelatedEntities
2385
- );
2386
- synced += currentBatch.length;
2387
- await this.postgresClient.incrementObjectProgress(
2388
- accountId,
2389
- runStartedAt,
2390
- "payment_methods",
2391
- currentBatch.length
2392
- );
2393
- currentBatch = [];
2858
+ let hasMore = true;
2859
+ let startingAfter = void 0;
2860
+ while (hasMore) {
2861
+ const response = await this.stripe.paymentMethods.list({
2862
+ limit: 100,
2863
+ customer: customerId,
2864
+ ...startingAfter ? { starting_after: startingAfter } : {}
2865
+ });
2866
+ for (const item of response.data) {
2867
+ currentBatch.push(item);
2868
+ if (currentBatch.length >= CHECKPOINT_SIZE) {
2869
+ await this.upsertPaymentMethods(
2870
+ currentBatch,
2871
+ accountId,
2872
+ syncParams?.backfillRelatedEntities
2873
+ );
2874
+ synced += currentBatch.length;
2875
+ await this.postgresClient.incrementObjectProgress(
2876
+ accountId,
2877
+ runStartedAt,
2878
+ "payment_methods",
2879
+ currentBatch.length
2880
+ );
2881
+ currentBatch = [];
2882
+ }
2883
+ }
2884
+ hasMore = response.has_more;
2885
+ if (response.data.length > 0) {
2886
+ startingAfter = response.data[response.data.length - 1].id;
2394
2887
  }
2395
2888
  }
2396
2889
  if (currentBatch.length > 0) {
@@ -2427,7 +2920,7 @@ var StripeSync = class {
2427
2920
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2428
2921
  }
2429
2922
  return this.fetchAndUpsert(
2430
- () => this.stripe.disputes.list(params),
2923
+ (pagination) => this.stripe.disputes.list({ ...params, ...pagination }),
2431
2924
  (items) => this.upsertDisputes(items, accountId, syncParams?.backfillRelatedEntities),
2432
2925
  accountId,
2433
2926
  "disputes",
@@ -2450,7 +2943,7 @@ var StripeSync = class {
2450
2943
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2451
2944
  }
2452
2945
  return this.fetchAndUpsert(
2453
- () => this.stripe.radar.earlyFraudWarnings.list(params),
2946
+ (pagination) => this.stripe.radar.earlyFraudWarnings.list({ ...params, ...pagination }),
2454
2947
  (items) => this.upsertEarlyFraudWarning(items, accountId, syncParams?.backfillRelatedEntities),
2455
2948
  accountId,
2456
2949
  "early_fraud_warnings",
@@ -2471,7 +2964,7 @@ var StripeSync = class {
2471
2964
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2472
2965
  }
2473
2966
  return this.fetchAndUpsert(
2474
- () => this.stripe.refunds.list(params),
2967
+ (pagination) => this.stripe.refunds.list({ ...params, ...pagination }),
2475
2968
  (items) => this.upsertRefunds(items, accountId, syncParams?.backfillRelatedEntities),
2476
2969
  accountId,
2477
2970
  "refunds",
@@ -2491,7 +2984,7 @@ var StripeSync = class {
2491
2984
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2492
2985
  }
2493
2986
  return this.fetchAndUpsert(
2494
- () => this.stripe.creditNotes.list(params),
2987
+ (pagination) => this.stripe.creditNotes.list({ ...params, ...pagination }),
2495
2988
  (creditNotes) => this.upsertCreditNotes(creditNotes, accountId),
2496
2989
  accountId,
2497
2990
  "credit_notes",
@@ -2553,7 +3046,7 @@ var StripeSync = class {
2553
3046
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2554
3047
  }
2555
3048
  return this.fetchAndUpsert(
2556
- () => this.stripe.checkout.sessions.list(params),
3049
+ (pagination) => this.stripe.checkout.sessions.list({ ...params, ...pagination }),
2557
3050
  (items) => this.upsertCheckoutSessions(items, accountId, syncParams?.backfillRelatedEntities),
2558
3051
  accountId,
2559
3052
  "checkout_sessions",
@@ -2607,31 +3100,42 @@ var StripeSync = class {
2607
3100
  try {
2608
3101
  this.config.logger?.info("Fetching items to sync from Stripe");
2609
3102
  try {
2610
- for await (const item of fetch2()) {
2611
- currentBatch.push(item);
2612
- if (currentBatch.length >= CHECKPOINT_SIZE) {
2613
- this.config.logger?.info(`Upserting batch of ${currentBatch.length} items`);
2614
- await upsert(currentBatch, accountId);
2615
- totalSynced += currentBatch.length;
2616
- await this.postgresClient.incrementObjectProgress(
2617
- accountId,
2618
- runStartedAt,
2619
- resourceName,
2620
- currentBatch.length
2621
- );
2622
- const maxCreated = Math.max(
2623
- ...currentBatch.map((i) => i.created || 0)
2624
- );
2625
- if (maxCreated > 0) {
2626
- await this.postgresClient.updateObjectCursor(
3103
+ let hasMore = true;
3104
+ let startingAfter = void 0;
3105
+ while (hasMore) {
3106
+ const response = await fetch2(
3107
+ startingAfter ? { starting_after: startingAfter } : void 0
3108
+ );
3109
+ for (const item of response.data) {
3110
+ currentBatch.push(item);
3111
+ if (currentBatch.length >= CHECKPOINT_SIZE) {
3112
+ this.config.logger?.info(`Upserting batch of ${currentBatch.length} items`);
3113
+ await upsert(currentBatch, accountId);
3114
+ totalSynced += currentBatch.length;
3115
+ await this.postgresClient.incrementObjectProgress(
2627
3116
  accountId,
2628
3117
  runStartedAt,
2629
3118
  resourceName,
2630
- String(maxCreated)
3119
+ currentBatch.length
3120
+ );
3121
+ const maxCreated = Math.max(
3122
+ ...currentBatch.map((i) => i.created || 0)
2631
3123
  );
2632
- this.config.logger?.info(`Checkpoint: cursor updated to ${maxCreated}`);
3124
+ if (maxCreated > 0) {
3125
+ await this.postgresClient.updateObjectCursor(
3126
+ accountId,
3127
+ runStartedAt,
3128
+ resourceName,
3129
+ String(maxCreated)
3130
+ );
3131
+ this.config.logger?.info(`Checkpoint: cursor updated to ${maxCreated}`);
3132
+ }
3133
+ currentBatch = [];
2633
3134
  }
2634
- currentBatch = [];
3135
+ }
3136
+ hasMore = response.has_more;
3137
+ if (response.data.length > 0) {
3138
+ startingAfter = response.data[response.data.length - 1].id;
2635
3139
  }
2636
3140
  }
2637
3141
  if (currentBatch.length > 0) {
@@ -2999,10 +3503,18 @@ var StripeSync = class {
2999
3503
  async fillCheckoutSessionsLineItems(checkoutSessionIds, accountId, syncTimestamp) {
3000
3504
  for (const checkoutSessionId of checkoutSessionIds) {
3001
3505
  const lineItemResponses = [];
3002
- for await (const lineItem of this.stripe.checkout.sessions.listLineItems(checkoutSessionId, {
3003
- limit: 100
3004
- })) {
3005
- lineItemResponses.push(lineItem);
3506
+ let hasMore = true;
3507
+ let startingAfter = void 0;
3508
+ while (hasMore) {
3509
+ const response = await this.stripe.checkout.sessions.listLineItems(checkoutSessionId, {
3510
+ limit: 100,
3511
+ ...startingAfter ? { starting_after: startingAfter } : {}
3512
+ });
3513
+ lineItemResponses.push(...response.data);
3514
+ hasMore = response.has_more;
3515
+ if (response.data.length > 0) {
3516
+ startingAfter = response.data[response.data.length - 1].id;
3517
+ }
3006
3518
  }
3007
3519
  await this.upsertCheckoutSessionLineItems(
3008
3520
  lineItemResponses,
@@ -3316,14 +3828,25 @@ var StripeSync = class {
3316
3828
  };
3317
3829
  /**
3318
3830
  * Stripe only sends the first 10 entries by default, the option will actively fetch all entries.
3831
+ * Uses manual pagination - each fetch() gets automatic retry protection.
3319
3832
  */
3320
3833
  async expandEntity(entities, property, listFn) {
3321
3834
  if (!this.config.autoExpandLists) return;
3322
3835
  for (const entity of entities) {
3323
3836
  if (entity[property]?.has_more) {
3324
3837
  const allData = [];
3325
- for await (const fetchedEntity of listFn(entity.id)) {
3326
- allData.push(fetchedEntity);
3838
+ let hasMore = true;
3839
+ let startingAfter = void 0;
3840
+ while (hasMore) {
3841
+ const response = await listFn(
3842
+ entity.id,
3843
+ startingAfter ? { starting_after: startingAfter } : void 0
3844
+ );
3845
+ allData.push(...response.data);
3846
+ hasMore = response.has_more;
3847
+ if (response.data.length > 0) {
3848
+ startingAfter = response.data[response.data.length - 1].id;
3849
+ }
3327
3850
  }
3328
3851
  entity[property] = {
3329
3852
  ...entity[property],
@@ -3448,7 +3971,9 @@ async function runMigrations(config) {
3448
3971
  var import_ws = __toESM(require("ws"), 1);
3449
3972
  var CLI_VERSION = "1.33.0";
3450
3973
  var PONG_WAIT = 10 * 1e3;
3451
- var PING_PERIOD = PONG_WAIT * 2 / 10;
3974
+ var PING_PERIOD = PONG_WAIT * 9 / 10;
3975
+ var CONNECT_ATTEMPT_WAIT = 10 * 1e3;
3976
+ var DEFAULT_RECONNECT_INTERVAL = 60 * 1e3;
3452
3977
  function getClientUserAgent() {
3453
3978
  return JSON.stringify({
3454
3979
  name: "stripe-cli",
@@ -3477,129 +4002,215 @@ async function createCliSession(stripeApiKey) {
3477
4002
  }
3478
4003
  return await response.json();
3479
4004
  }
4005
+ function sleep3(ms) {
4006
+ return new Promise((resolve) => setTimeout(resolve, ms));
4007
+ }
3480
4008
  async function createStripeWebSocketClient(options) {
3481
4009
  const { stripeApiKey, onEvent, onReady, onError, onClose } = options;
3482
4010
  const session = await createCliSession(stripeApiKey);
4011
+ const reconnectInterval = session.reconnect_delay ? session.reconnect_delay * 1e3 : DEFAULT_RECONNECT_INTERVAL;
3483
4012
  let ws = null;
3484
4013
  let pingInterval = null;
4014
+ let reconnectTimer = null;
3485
4015
  let connected = false;
3486
- let shouldReconnect = true;
3487
- function connect() {
3488
- const wsUrl = `${session.websocket_url}?websocket_feature=${encodeURIComponent(session.websocket_authorized_feature)}`;
3489
- ws = new import_ws.default(wsUrl, {
3490
- headers: {
3491
- "Accept-Encoding": "identity",
3492
- "User-Agent": `Stripe/v1 stripe-cli/${CLI_VERSION}`,
3493
- "X-Stripe-Client-User-Agent": getClientUserAgent(),
3494
- "Websocket-Id": session.websocket_id
4016
+ let shouldRun = true;
4017
+ let lastPongReceived = Date.now();
4018
+ let notifyCloseResolve = null;
4019
+ let stopResolve = null;
4020
+ function cleanupConnection() {
4021
+ if (pingInterval) {
4022
+ clearInterval(pingInterval);
4023
+ pingInterval = null;
4024
+ }
4025
+ if (reconnectTimer) {
4026
+ clearTimeout(reconnectTimer);
4027
+ reconnectTimer = null;
4028
+ }
4029
+ if (ws) {
4030
+ ws.removeAllListeners();
4031
+ if (ws.readyState === import_ws.default.OPEN || ws.readyState === import_ws.default.CONNECTING) {
4032
+ ws.close(1e3, "Resetting connection");
3495
4033
  }
3496
- });
3497
- ws.on("pong", () => {
3498
- });
3499
- ws.on("open", () => {
3500
- connected = true;
3501
- pingInterval = setInterval(() => {
3502
- if (ws && ws.readyState === import_ws.default.OPEN) {
3503
- ws.ping();
4034
+ ws = null;
4035
+ }
4036
+ connected = false;
4037
+ }
4038
+ function setupWebSocket() {
4039
+ return new Promise((resolve, reject) => {
4040
+ lastPongReceived = Date.now();
4041
+ const wsUrl = `${session.websocket_url}?websocket_feature=${encodeURIComponent(session.websocket_authorized_feature)}`;
4042
+ ws = new import_ws.default(wsUrl, {
4043
+ headers: {
4044
+ "Accept-Encoding": "identity",
4045
+ "User-Agent": `Stripe/v1 stripe-cli/${CLI_VERSION}`,
4046
+ "X-Stripe-Client-User-Agent": getClientUserAgent(),
4047
+ "Websocket-Id": session.websocket_id
3504
4048
  }
3505
- }, PING_PERIOD);
3506
- if (onReady) {
3507
- onReady(session.secret);
3508
- }
3509
- });
3510
- ws.on("message", async (data) => {
3511
- try {
3512
- const message = JSON.parse(data.toString());
3513
- const ack = {
3514
- type: "event_ack",
3515
- event_id: message.webhook_id,
3516
- webhook_conversation_id: message.webhook_conversation_id,
3517
- webhook_id: message.webhook_id
3518
- };
3519
- if (ws && ws.readyState === import_ws.default.OPEN) {
3520
- ws.send(JSON.stringify(ack));
4049
+ });
4050
+ const connectionTimeout = setTimeout(() => {
4051
+ if (ws && ws.readyState === import_ws.default.CONNECTING) {
4052
+ ws.terminate();
4053
+ reject(new Error("WebSocket connection timeout"));
3521
4054
  }
3522
- let response;
4055
+ }, CONNECT_ATTEMPT_WAIT);
4056
+ ws.on("pong", () => {
4057
+ lastPongReceived = Date.now();
4058
+ });
4059
+ ws.on("open", () => {
4060
+ clearTimeout(connectionTimeout);
4061
+ connected = true;
4062
+ pingInterval = setInterval(() => {
4063
+ if (ws && ws.readyState === import_ws.default.OPEN) {
4064
+ const timeSinceLastPong = Date.now() - lastPongReceived;
4065
+ if (timeSinceLastPong > PONG_WAIT) {
4066
+ if (onError) {
4067
+ onError(new Error(`WebSocket stale: no pong in ${timeSinceLastPong}ms`));
4068
+ }
4069
+ if (notifyCloseResolve) {
4070
+ notifyCloseResolve();
4071
+ notifyCloseResolve = null;
4072
+ }
4073
+ ws.terminate();
4074
+ return;
4075
+ }
4076
+ ws.ping();
4077
+ }
4078
+ }, PING_PERIOD);
4079
+ if (onReady) {
4080
+ onReady(session.secret);
4081
+ }
4082
+ resolve();
4083
+ });
4084
+ ws.on("message", async (data) => {
3523
4085
  try {
3524
- const result = await onEvent(message);
3525
- response = {
3526
- type: "webhook_response",
3527
- webhook_id: message.webhook_id,
4086
+ const message = JSON.parse(data.toString());
4087
+ const ack = {
4088
+ type: "event_ack",
4089
+ event_id: message.webhook_id,
3528
4090
  webhook_conversation_id: message.webhook_conversation_id,
3529
- forward_url: "stripe-sync-engine",
3530
- status: result?.status ?? 200,
3531
- http_headers: {},
3532
- body: JSON.stringify({
3533
- event_type: result?.event_type,
3534
- event_id: result?.event_id,
3535
- database_url: result?.databaseUrl,
3536
- error: result?.error
3537
- }),
3538
- request_headers: message.http_headers,
3539
- request_body: message.event_payload,
3540
- notification_id: message.webhook_id
4091
+ webhook_id: message.webhook_id
3541
4092
  };
4093
+ if (ws && ws.readyState === import_ws.default.OPEN) {
4094
+ ws.send(JSON.stringify(ack));
4095
+ }
4096
+ let response;
4097
+ try {
4098
+ const result = await onEvent(message);
4099
+ response = {
4100
+ type: "webhook_response",
4101
+ webhook_id: message.webhook_id,
4102
+ webhook_conversation_id: message.webhook_conversation_id,
4103
+ forward_url: "stripe-sync-engine",
4104
+ status: result?.status ?? 200,
4105
+ http_headers: {},
4106
+ body: JSON.stringify({
4107
+ event_type: result?.event_type,
4108
+ event_id: result?.event_id,
4109
+ database_url: result?.databaseUrl,
4110
+ error: result?.error
4111
+ }),
4112
+ request_headers: message.http_headers,
4113
+ request_body: message.event_payload,
4114
+ notification_id: message.webhook_id
4115
+ };
4116
+ } catch (err) {
4117
+ const errorMessage = err instanceof Error ? err.message : String(err);
4118
+ response = {
4119
+ type: "webhook_response",
4120
+ webhook_id: message.webhook_id,
4121
+ webhook_conversation_id: message.webhook_conversation_id,
4122
+ forward_url: "stripe-sync-engine",
4123
+ status: 500,
4124
+ http_headers: {},
4125
+ body: JSON.stringify({ error: errorMessage }),
4126
+ request_headers: message.http_headers,
4127
+ request_body: message.event_payload,
4128
+ notification_id: message.webhook_id
4129
+ };
4130
+ if (onError) {
4131
+ onError(err instanceof Error ? err : new Error(errorMessage));
4132
+ }
4133
+ }
4134
+ if (ws && ws.readyState === import_ws.default.OPEN) {
4135
+ ws.send(JSON.stringify(response));
4136
+ }
3542
4137
  } catch (err) {
3543
- const errorMessage = err instanceof Error ? err.message : String(err);
3544
- response = {
3545
- type: "webhook_response",
3546
- webhook_id: message.webhook_id,
3547
- webhook_conversation_id: message.webhook_conversation_id,
3548
- forward_url: "stripe-sync-engine",
3549
- status: 500,
3550
- http_headers: {},
3551
- body: JSON.stringify({ error: errorMessage }),
3552
- request_headers: message.http_headers,
3553
- request_body: message.event_payload,
3554
- notification_id: message.webhook_id
3555
- };
3556
4138
  if (onError) {
3557
- onError(err instanceof Error ? err : new Error(errorMessage));
4139
+ onError(err instanceof Error ? err : new Error(String(err)));
3558
4140
  }
3559
4141
  }
3560
- if (ws && ws.readyState === import_ws.default.OPEN) {
3561
- ws.send(JSON.stringify(response));
3562
- }
3563
- } catch (err) {
4142
+ });
4143
+ ws.on("error", (error) => {
4144
+ clearTimeout(connectionTimeout);
3564
4145
  if (onError) {
3565
- onError(err instanceof Error ? err : new Error(String(err)));
4146
+ onError(error);
3566
4147
  }
3567
- }
3568
- });
3569
- ws.on("error", (error) => {
3570
- if (onError) {
3571
- onError(error);
3572
- }
4148
+ if (!connected) {
4149
+ reject(error);
4150
+ }
4151
+ });
4152
+ ws.on("close", (code, reason) => {
4153
+ clearTimeout(connectionTimeout);
4154
+ connected = false;
4155
+ if (pingInterval) {
4156
+ clearInterval(pingInterval);
4157
+ pingInterval = null;
4158
+ }
4159
+ if (onClose) {
4160
+ onClose(code, reason.toString());
4161
+ }
4162
+ if (notifyCloseResolve) {
4163
+ notifyCloseResolve();
4164
+ notifyCloseResolve = null;
4165
+ }
4166
+ });
3573
4167
  });
3574
- ws.on("close", (code, reason) => {
4168
+ }
4169
+ async function runLoop() {
4170
+ while (shouldRun) {
3575
4171
  connected = false;
3576
- if (pingInterval) {
3577
- clearInterval(pingInterval);
3578
- pingInterval = null;
3579
- }
3580
- if (onClose) {
3581
- onClose(code, reason.toString());
3582
- }
3583
- if (shouldReconnect) {
3584
- const delay = (session.reconnect_delay || 5) * 1e3;
3585
- setTimeout(() => {
3586
- connect();
3587
- }, delay);
4172
+ let connectError = null;
4173
+ do {
4174
+ try {
4175
+ await setupWebSocket();
4176
+ connectError = null;
4177
+ } catch (err) {
4178
+ connectError = err instanceof Error ? err : new Error(String(err));
4179
+ if (onError) {
4180
+ onError(connectError);
4181
+ }
4182
+ if (shouldRun) {
4183
+ await sleep3(CONNECT_ATTEMPT_WAIT);
4184
+ }
4185
+ }
4186
+ } while (connectError && shouldRun);
4187
+ if (!shouldRun) break;
4188
+ await new Promise((resolve) => {
4189
+ notifyCloseResolve = resolve;
4190
+ stopResolve = resolve;
4191
+ reconnectTimer = setTimeout(() => {
4192
+ cleanupConnection();
4193
+ resolve();
4194
+ }, reconnectInterval);
4195
+ });
4196
+ if (reconnectTimer) {
4197
+ clearTimeout(reconnectTimer);
4198
+ reconnectTimer = null;
3588
4199
  }
3589
- });
4200
+ notifyCloseResolve = null;
4201
+ stopResolve = null;
4202
+ }
4203
+ cleanupConnection();
3590
4204
  }
3591
- connect();
4205
+ runLoop();
3592
4206
  return {
3593
4207
  close: () => {
3594
- shouldReconnect = false;
3595
- if (pingInterval) {
3596
- clearInterval(pingInterval);
3597
- pingInterval = null;
3598
- }
3599
- if (ws) {
3600
- ws.close(1e3, "Connection Done");
3601
- ws = null;
4208
+ shouldRun = false;
4209
+ if (stopResolve) {
4210
+ stopResolve();
4211
+ stopResolve = null;
3602
4212
  }
4213
+ cleanupConnection();
3603
4214
  },
3604
4215
  isConnected: () => connected
3605
4216
  };
@@ -3645,13 +4256,13 @@ Creating ngrok tunnel for port ${port}...`));
3645
4256
  var import_supabase_management_js = require("supabase-management-js");
3646
4257
 
3647
4258
  // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
3648
- var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\n// Helper to delete edge function via Management API\nasync function deleteEdgeFunction(\n projectRef: string,\n functionSlug: string,\n accessToken: string\n): Promise<void> {\n const url = `https://api.supabase.com/v1/projects/${projectRef}/functions/${functionSlug}`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n throw new Error(`Failed to delete function ${functionSlug}: ${response.status} ${text}`)\n }\n}\n\n// Helper to delete secrets via Management API\nasync function deleteSecret(\n projectRef: string,\n secretName: string,\n accessToken: string\n): Promise<void> {\n const url = `https://api.supabase.com/v1/projects/${projectRef}/secrets`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify([secretName]),\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n console.warn(`Failed to delete secret ${secretName}: ${response.status} ${text}`)\n }\n}\n\nDeno.serve(async (req) => {\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle DELETE requests for uninstall\n if (req.method === 'DELETE') {\n let stripeSync = null\n try {\n // Parse request body\n const body = await req.json()\n const { supabase_access_token, supabase_project_ref } = body\n\n if (!supabase_access_token || !supabase_project_ref) {\n throw new Error(\n 'supabase_access_token and supabase_project_ref are required in request body'\n )\n }\n\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 // Stripe key is required for uninstall to delete webhooks\n const stripeKey = Deno.env.get('STRIPE_SECRET_KEY')\n if (!stripeKey) {\n throw new Error('STRIPE_SECRET_KEY environment variable is required for uninstall')\n }\n\n // Step 1: Delete Stripe webhooks and clean up database\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 },\n stripeSecretKey: stripeKey,\n })\n\n // Delete all managed webhooks\n const webhooks = await stripeSync.listManagedWebhooks()\n for (const webhook of webhooks) {\n try {\n await stripeSync.deleteManagedWebhook(webhook.id)\n console.log(`Deleted webhook: ${webhook.id}`)\n } catch (err) {\n console.warn(`Could not delete webhook ${webhook.id}:`, err)\n }\n }\n\n // Unschedule pg_cron job\n try {\n await stripeSync.postgresClient.query(`\n DO $$\n BEGIN\n IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN\n PERFORM cron.unschedule('stripe-sync-worker');\n END IF;\n END $$;\n `)\n } catch (err) {\n console.warn('Could not unschedule pg_cron job:', err)\n }\n\n // Delete vault secret\n try {\n await stripeSync.postgresClient.query(`\n DELETE FROM vault.secrets\n WHERE name = 'stripe_sync_service_role_key'\n `)\n } catch (err) {\n console.warn('Could not delete vault secret:', err)\n }\n\n // Terminate connections holding locks on stripe schema\n try {\n await stripeSync.postgresClient.query(`\n SELECT pg_terminate_backend(pid)\n FROM pg_locks l\n JOIN pg_class c ON l.relation = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname = 'stripe'\n AND l.pid != pg_backend_pid()\n `)\n } catch (err) {\n console.warn('Could not terminate connections:', err)\n }\n\n // Drop schema with retry\n let dropAttempts = 0\n const maxAttempts = 3\n while (dropAttempts < maxAttempts) {\n try {\n await stripeSync.postgresClient.query('DROP SCHEMA IF EXISTS stripe CASCADE')\n break // Success, exit loop\n } catch (err) {\n dropAttempts++\n if (dropAttempts >= maxAttempts) {\n throw new Error(\n `Failed to drop schema after ${maxAttempts} attempts. ` +\n `There may be active connections or locks on the stripe schema. ` +\n `Error: ${err.message}`\n )\n }\n // Wait 1 second before retrying\n await new Promise((resolve) => setTimeout(resolve, 1000))\n }\n }\n\n await stripeSync.postgresClient.pool.end()\n\n // Step 2: Delete Supabase secrets\n try {\n await deleteSecret(supabase_project_ref, 'STRIPE_SECRET_KEY', supabase_access_token)\n } catch (err) {\n console.warn('Could not delete STRIPE_SECRET_KEY secret:', err)\n }\n\n // Step 3: Delete Edge Functions\n try {\n await deleteEdgeFunction(supabase_project_ref, 'stripe-setup', supabase_access_token)\n } catch (err) {\n console.warn('Could not delete stripe-setup function:', err)\n }\n\n try {\n await deleteEdgeFunction(supabase_project_ref, 'stripe-webhook', supabase_access_token)\n } catch (err) {\n console.warn('Could not delete stripe-webhook function:', err)\n }\n\n try {\n await deleteEdgeFunction(supabase_project_ref, 'stripe-worker', supabase_access_token)\n } catch (err) {\n console.warn('Could not delete stripe-worker function:', err)\n }\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Uninstall complete',\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Uninstall error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\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\n // Handle POST requests for install\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\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";
4259
+ var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\n// Helper to validate accessToken against Management API\nasync function validateAccessToken(projectRef: string, accessToken: string): Promise<boolean> {\n // Try to fetch project details using the access token\n // This validates that the token is valid for the management API\n const url = `https://api.supabase.com/v1/projects/${projectRef}`\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n // If we can successfully get the project, the token is valid\n return response.ok\n}\n\n// Helper to delete edge function via Management API\nasync function deleteEdgeFunction(\n projectRef: string,\n functionSlug: string,\n accessToken: string\n): Promise<void> {\n const url = `https://api.supabase.com/v1/projects/${projectRef}/functions/${functionSlug}`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n throw new Error(`Failed to delete function ${functionSlug}: ${response.status} ${text}`)\n }\n}\n\n// Helper to delete secrets via Management API\nasync function deleteSecret(\n projectRef: string,\n secretName: string,\n accessToken: string\n): Promise<void> {\n const url = `https://api.supabase.com/v1/projects/${projectRef}/secrets`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify([secretName]),\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n console.warn(`Failed to delete secret ${secretName}: ${response.status} ${text}`)\n }\n}\n\nDeno.serve(async (req) => {\n // Extract project ref from SUPABASE_URL (format: https://{projectRef}.{base})\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n const projectRef = new URL(supabaseUrl).hostname.split('.')[0]\n\n // Validate access token for all requests\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const accessToken = authHeader.substring(7) // Remove 'Bearer '\n const isValid = await validateAccessToken(projectRef, accessToken)\n if (!isValid) {\n return new Response('Forbidden: Invalid access token for this project', { status: 403 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle DELETE requests for uninstall\n if (req.method === 'DELETE') {\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 // Stripe key is required for uninstall to delete webhooks\n const stripeKey = Deno.env.get('STRIPE_SECRET_KEY')\n if (!stripeKey) {\n throw new Error('STRIPE_SECRET_KEY environment variable is required for uninstall')\n }\n\n // Step 1: Delete Stripe webhooks and clean up database\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 },\n stripeSecretKey: stripeKey,\n })\n\n // Delete all managed webhooks\n const webhooks = await stripeSync.listManagedWebhooks()\n for (const webhook of webhooks) {\n try {\n await stripeSync.deleteManagedWebhook(webhook.id)\n console.log(`Deleted webhook: ${webhook.id}`)\n } catch (err) {\n console.warn(`Could not delete webhook ${webhook.id}:`, err)\n }\n }\n\n // Unschedule pg_cron job\n try {\n await stripeSync.postgresClient.query(`\n DO $$\n BEGIN\n IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN\n PERFORM cron.unschedule('stripe-sync-worker');\n END IF;\n END $$;\n `)\n } catch (err) {\n console.warn('Could not unschedule pg_cron job:', err)\n }\n\n // Delete vault secret\n try {\n await stripeSync.postgresClient.query(`\n DELETE FROM vault.secrets\n WHERE name = 'stripe_sync_worker_secret'\n `)\n } catch (err) {\n console.warn('Could not delete vault secret:', err)\n }\n\n // Terminate connections holding locks on stripe schema\n try {\n await stripeSync.postgresClient.query(`\n SELECT pg_terminate_backend(pid)\n FROM pg_locks l\n JOIN pg_class c ON l.relation = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname = 'stripe'\n AND l.pid != pg_backend_pid()\n `)\n } catch (err) {\n console.warn('Could not terminate connections:', err)\n }\n\n // Drop schema with retry\n let dropAttempts = 0\n const maxAttempts = 3\n while (dropAttempts < maxAttempts) {\n try {\n await stripeSync.postgresClient.query('DROP SCHEMA IF EXISTS stripe CASCADE')\n break // Success, exit loop\n } catch (err) {\n dropAttempts++\n if (dropAttempts >= maxAttempts) {\n throw new Error(\n `Failed to drop schema after ${maxAttempts} attempts. ` +\n `There may be active connections or locks on the stripe schema. ` +\n `Error: ${err.message}`\n )\n }\n // Wait 1 second before retrying\n await new Promise((resolve) => setTimeout(resolve, 1000))\n }\n }\n\n await stripeSync.postgresClient.pool.end()\n\n // Step 2: Delete Supabase secrets\n try {\n await deleteSecret(projectRef, 'STRIPE_SECRET_KEY', accessToken)\n } catch (err) {\n console.warn('Could not delete STRIPE_SECRET_KEY secret:', err)\n }\n\n // Step 3: Delete Edge Functions\n try {\n await deleteEdgeFunction(projectRef, 'stripe-setup', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-setup function:', err)\n }\n\n try {\n await deleteEdgeFunction(projectRef, 'stripe-webhook', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-webhook function:', err)\n }\n\n try {\n await deleteEdgeFunction(projectRef, 'stripe-worker', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-worker function:', err)\n }\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Uninstall complete',\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Uninstall error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\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\n // Handle POST requests for install\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\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";
3649
4260
 
3650
4261
  // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
3651
4262
  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";
3652
4263
 
3653
4264
  // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
3654
- var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron at a configurable interval (default: 60 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";
4265
+ var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron at a configurable interval (default: 60 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 token = authHeader.substring(7) // Remove 'Bearer '\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 // Validate that the token matches the unique worker secret stored in vault\n const vaultResult = await sql`\n SELECT decrypted_secret\n FROM vault.decrypted_secrets\n WHERE name = 'stripe_sync_worker_secret'\n `\n\n if (vaultResult.length === 0) {\n await sql.end()\n return new Response('Worker secret not configured in vault', { status: 500 })\n }\n\n const storedSecret = vaultResult[0].decrypted_secret\n if (token !== storedSecret) {\n await sql.end()\n return new Response('Forbidden: Invalid worker secret', { status: 403 })\n }\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n enableSigma: (Deno.env.get('ENABLE_SIGMA') ?? 'false') === 'true',\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";
3655
4266
 
3656
4267
  // src/supabase/edge-function-code.ts
3657
4268
  var setupFunctionCode = stripe_setup_default;
@@ -3749,8 +4360,8 @@ var SupabaseSetupClient = class {
3749
4360
  `Invalid interval: ${intervalSeconds}. Must be either 1-59 seconds or a multiple of 60 (e.g., 60, 120, 180).`
3750
4361
  );
3751
4362
  }
3752
- const serviceRoleKey = await this.getServiceRoleKey();
3753
- const escapedServiceRoleKey = serviceRoleKey.replace(/'/g, "''");
4363
+ const workerSecret = crypto.randomUUID();
4364
+ const escapedWorkerSecret = workerSecret.replace(/'/g, "''");
3754
4365
  const sql3 = `
3755
4366
  -- Enable extensions
3756
4367
  CREATE EXTENSION IF NOT EXISTS pg_cron;
@@ -3763,10 +4374,10 @@ var SupabaseSetupClient = class {
3763
4374
  SELECT 1 FROM pgmq.list_queues() WHERE queue_name = 'stripe_sync_work'
3764
4375
  );
3765
4376
 
3766
- -- Store service role key in vault for pg_cron to use
4377
+ -- Store unique worker secret in vault for pg_cron to use
3767
4378
  -- Delete existing secret if it exists, then create new one
3768
- DELETE FROM vault.secrets WHERE name = 'stripe_sync_service_role_key';
3769
- SELECT vault.create_secret('${escapedServiceRoleKey}', 'stripe_sync_service_role_key');
4379
+ DELETE FROM vault.secrets WHERE name = 'stripe_sync_worker_secret';
4380
+ SELECT vault.create_secret('${escapedWorkerSecret}', 'stripe_sync_worker_secret');
3770
4381
 
3771
4382
  -- Delete existing jobs if they exist
3772
4383
  SELECT cron.unschedule('stripe-sync-worker') WHERE EXISTS (
@@ -3785,7 +4396,7 @@ var SupabaseSetupClient = class {
3785
4396
  SELECT net.http_post(
3786
4397
  url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
3787
4398
  headers := jsonb_build_object(
3788
- 'Authorization', 'Bearer ' || (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'stripe_sync_service_role_key')
4399
+ 'Authorization', 'Bearer ' || (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'stripe_sync_worker_secret')
3789
4400
  )
3790
4401
  )
3791
4402
  $$
@@ -3799,17 +4410,6 @@ var SupabaseSetupClient = class {
3799
4410
  getWebhookUrl() {
3800
4411
  return `https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-webhook`;
3801
4412
  }
3802
- /**
3803
- * Get the service role key for this project (needed to invoke Edge Functions)
3804
- */
3805
- async getServiceRoleKey() {
3806
- const apiKeys = await this.api.getProjectApiKeys(this.projectRef);
3807
- const serviceRoleKey = apiKeys?.find((k) => k.name === "service_role");
3808
- if (!serviceRoleKey) {
3809
- throw new Error("Could not find service_role API key");
3810
- }
3811
- return serviceRoleKey.api_key;
3812
- }
3813
4413
  /**
3814
4414
  * Get the anon key for this project (needed for Realtime subscriptions)
3815
4415
  */
@@ -3830,12 +4430,12 @@ var SupabaseSetupClient = class {
3830
4430
  /**
3831
4431
  * Invoke an Edge Function
3832
4432
  */
3833
- async invokeFunction(name, serviceRoleKey) {
4433
+ async invokeFunction(name, bearerToken) {
3834
4434
  const url = `https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/${name}`;
3835
4435
  const response = await fetch(url, {
3836
4436
  method: "POST",
3837
4437
  headers: {
3838
- Authorization: `Bearer ${serviceRoleKey}`,
4438
+ Authorization: `Bearer ${bearerToken}`,
3839
4439
  "Content-Type": "application/json"
3840
4440
  }
3841
4441
  });
@@ -3942,18 +4542,13 @@ var SupabaseSetupClient = class {
3942
4542
  */
3943
4543
  async uninstall() {
3944
4544
  try {
3945
- const serviceRoleKey = await this.getServiceRoleKey();
3946
4545
  const url = `https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-setup`;
3947
4546
  const response = await fetch(url, {
3948
4547
  method: "DELETE",
3949
4548
  headers: {
3950
- Authorization: `Bearer ${serviceRoleKey}`,
4549
+ Authorization: `Bearer ${this.accessToken}`,
3951
4550
  "Content-Type": "application/json"
3952
- },
3953
- body: JSON.stringify({
3954
- supabase_access_token: this.accessToken,
3955
- supabase_project_ref: this.projectRef
3956
- })
4551
+ }
3957
4552
  });
3958
4553
  if (!response.ok) {
3959
4554
  const text = await response.text();
@@ -3994,12 +4589,11 @@ var SupabaseSetupClient = class {
3994
4589
  const versionedSetup = this.injectPackageVersion(setupFunctionCode, version);
3995
4590
  const versionedWebhook = this.injectPackageVersion(webhookFunctionCode, version);
3996
4591
  const versionedWorker = this.injectPackageVersion(workerFunctionCode, version);
3997
- await this.deployFunction("stripe-setup", versionedSetup, true);
4592
+ await this.deployFunction("stripe-setup", versionedSetup, false);
3998
4593
  await this.deployFunction("stripe-webhook", versionedWebhook, false);
3999
- await this.deployFunction("stripe-worker", versionedWorker, true);
4594
+ await this.deployFunction("stripe-worker", versionedWorker, false);
4000
4595
  await this.setSecrets([{ name: "STRIPE_SECRET_KEY", value: trimmedStripeKey }]);
4001
- const serviceRoleKey = await this.getServiceRoleKey();
4002
- const setupResult = await this.invokeFunction("stripe-setup", serviceRoleKey);
4596
+ const setupResult = await this.invokeFunction("stripe-setup", this.accessToken);
4003
4597
  if (!setupResult.success) {
4004
4598
  throw new Error(`Setup failed: ${setupResult.error}`);
4005
4599
  }
@@ -4062,7 +4656,9 @@ var VALID_SYNC_OBJECTS = [
4062
4656
  "credit_note",
4063
4657
  "early_fraud_warning",
4064
4658
  "refund",
4065
- "checkout_sessions"
4659
+ "checkout_sessions",
4660
+ "subscription_item_change_events_v2_beta",
4661
+ "exchange_rates_from_usd"
4066
4662
  ];
4067
4663
  async function backfillCommand(options, entityName) {
4068
4664
  let stripeSync = null;
@@ -4091,8 +4687,8 @@ async function backfillCommand(options, entityName) {
4091
4687
  if (!input || input.trim() === "") {
4092
4688
  return "Stripe API key is required";
4093
4689
  }
4094
- if (!input.startsWith("sk_")) {
4095
- return 'Stripe API key should start with "sk_"';
4690
+ if (!input.startsWith("sk_") && !input.startsWith("rk_")) {
4691
+ return 'Stripe API key should start with "sk_" or "rk_"';
4096
4692
  }
4097
4693
  return true;
4098
4694
  }
@@ -4149,6 +4745,7 @@ async function backfillCommand(options, entityName) {
4149
4745
  stripeSync = new StripeSync({
4150
4746
  databaseUrl: config.databaseUrl,
4151
4747
  stripeSecretKey: config.stripeApiKey,
4748
+ enableSigma: process.env.ENABLE_SIGMA === "true",
4152
4749
  stripeApiVersion: process.env.STRIPE_API_VERSION || "2020-08-27",
4153
4750
  autoExpandLists: process.env.AUTO_EXPAND_LISTS === "true",
4154
4751
  backfillRelatedEntities: process.env.BACKFILL_RELATED_ENTITIES !== "false",
@@ -4287,7 +4884,7 @@ Cleaning up... (signal: ${signal || "manual"})`));
4287
4884
  process.on("SIGTERM", () => cleanup("SIGTERM"));
4288
4885
  try {
4289
4886
  const config = await loadConfig(options);
4290
- const useWebSocketMode = !config.ngrokAuthToken;
4887
+ const useWebSocketMode = process.env.USE_WEBSOCKET === "true" || !config.ngrokAuthToken;
4291
4888
  const modeLabel = useWebSocketMode ? "WebSocket" : "Webhook (ngrok)";
4292
4889
  console.log(import_chalk3.default.blue(`
4293
4890
  Mode: ${modeLabel}`));
@@ -4312,6 +4909,7 @@ Mode: ${modeLabel}`));
4312
4909
  stripeSync = new StripeSync({
4313
4910
  databaseUrl: config.databaseUrl,
4314
4911
  stripeSecretKey: config.stripeApiKey,
4912
+ enableSigma: config.enableSigma,
4315
4913
  stripeApiVersion: process.env.STRIPE_API_VERSION || "2020-08-27",
4316
4914
  autoExpandLists: process.env.AUTO_EXPAND_LISTS === "true",
4317
4915
  backfillRelatedEntities: process.env.BACKFILL_RELATED_ENTITIES !== "false",
@@ -4459,7 +5057,8 @@ async function installCommand(options) {
4459
5057
  mask: "*",
4460
5058
  validate: (input) => {
4461
5059
  if (!input.trim()) return "Stripe key is required";
4462
- if (!input.startsWith("sk_")) return 'Stripe key should start with "sk_"';
5060
+ if (!input.startsWith("sk_") && !input.startsWith("rk_"))
5061
+ return 'Stripe key should start with "sk_" or "rk_"';
4463
5062
  return true;
4464
5063
  }
4465
5064
  });