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.
@@ -33,7 +33,7 @@ var import_commander = require("commander");
33
33
  // package.json
34
34
  var package_default = {
35
35
  name: "stripe-experiment-sync",
36
- version: "1.0.13",
36
+ version: "1.0.15-beta.1766078819",
37
37
  private: false,
38
38
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
39
39
  type: "module",
@@ -73,6 +73,7 @@ var package_default = {
73
73
  dotenv: "^16.4.7",
74
74
  express: "^4.18.2",
75
75
  inquirer: "^12.3.0",
76
+ papaparse: "5.4.1",
76
77
  pg: "^8.16.3",
77
78
  "pg-node-migrations": "0.0.8",
78
79
  stripe: "^17.7.0",
@@ -84,6 +85,7 @@ var package_default = {
84
85
  "@types/express": "^4.17.21",
85
86
  "@types/inquirer": "^9.0.7",
86
87
  "@types/node": "^24.10.1",
88
+ "@types/papaparse": "5.3.16",
87
89
  "@types/pg": "^8.15.5",
88
90
  "@types/ws": "^8.5.13",
89
91
  "@types/yesql": "^4.1.4",
@@ -127,6 +129,7 @@ async function loadConfig(options) {
127
129
  config.stripeApiKey = options.stripeKey || process.env.STRIPE_API_KEY || "";
128
130
  config.ngrokAuthToken = options.ngrokToken || process.env.NGROK_AUTH_TOKEN || "";
129
131
  config.databaseUrl = options.databaseUrl || process.env.DATABASE_URL || "";
132
+ config.enableSigma = options.enableSigma ?? (process.env.ENABLE_SIGMA !== void 0 ? process.env.ENABLE_SIGMA === "true" : void 0);
130
133
  const questions = [];
131
134
  if (!config.stripeApiKey) {
132
135
  questions.push({
@@ -138,8 +141,8 @@ async function loadConfig(options) {
138
141
  if (!input || input.trim() === "") {
139
142
  return "Stripe API key is required";
140
143
  }
141
- if (!input.startsWith("sk_")) {
142
- return 'Stripe API key should start with "sk_"';
144
+ if (!input.startsWith("sk_") && !input.startsWith("rk_")) {
145
+ return 'Stripe API key should start with "sk_" or "rk_"';
143
146
  }
144
147
  return true;
145
148
  }
@@ -162,23 +165,80 @@ async function loadConfig(options) {
162
165
  }
163
166
  });
164
167
  }
168
+ if (config.enableSigma === void 0) {
169
+ questions.push({
170
+ type: "confirm",
171
+ name: "enableSigma",
172
+ message: "Enable Sigma sync? (Requires Sigma access in Stripe API key)",
173
+ default: false
174
+ });
175
+ }
165
176
  if (questions.length > 0) {
166
- console.log(import_chalk.default.yellow("\nMissing required configuration. Please provide:"));
177
+ console.log(import_chalk.default.yellow("\nMissing configuration. Please provide:"));
167
178
  const answers = await import_inquirer.default.prompt(questions);
168
179
  Object.assign(config, answers);
169
180
  }
181
+ if (config.enableSigma === void 0) {
182
+ config.enableSigma = false;
183
+ }
170
184
  return config;
171
185
  }
172
186
 
173
187
  // src/stripeSync.ts
174
- var import_stripe2 = __toESM(require("stripe"), 1);
188
+ var import_stripe3 = __toESM(require("stripe"), 1);
175
189
  var import_yesql2 = require("yesql");
176
190
 
177
191
  // src/database/postgres.ts
178
192
  var import_pg = __toESM(require("pg"), 1);
179
193
  var import_yesql = require("yesql");
194
+
195
+ // src/database/QueryUtils.ts
196
+ var QueryUtils = class _QueryUtils {
197
+ constructor() {
198
+ }
199
+ static quoteIdent(name) {
200
+ return `"${name}"`;
201
+ }
202
+ static quotedList(names) {
203
+ return names.map(_QueryUtils.quoteIdent).join(", ");
204
+ }
205
+ static buildInsertParts(columns) {
206
+ const columnsSql = columns.map((c) => _QueryUtils.quoteIdent(c.column)).join(", ");
207
+ const valuesSql = columns.map((c, i) => {
208
+ const placeholder = `$${i + 1}`;
209
+ return `${placeholder}::${c.pgType}`;
210
+ }).join(", ");
211
+ const params = columns.map((c) => c.value);
212
+ return { columnsSql, valuesSql, params };
213
+ }
214
+ static buildRawJsonUpsertQuery(schema, table, columns, conflictTarget) {
215
+ const { columnsSql, valuesSql, params } = _QueryUtils.buildInsertParts(columns);
216
+ const conflictSql = _QueryUtils.quotedList(conflictTarget);
217
+ const tsParamIdx = columns.findIndex((c) => c.column === "_last_synced_at") + 1;
218
+ if (tsParamIdx <= 0) {
219
+ throw new Error("buildRawJsonUpsertQuery requires _last_synced_at column");
220
+ }
221
+ const sql3 = `
222
+ INSERT INTO ${_QueryUtils.quoteIdent(schema)}.${_QueryUtils.quoteIdent(table)} (${columnsSql})
223
+ VALUES (${valuesSql})
224
+ ON CONFLICT (${conflictSql})
225
+ DO UPDATE SET
226
+ "_raw_data" = EXCLUDED."_raw_data",
227
+ "_last_synced_at" = $${tsParamIdx},
228
+ "_account_id" = EXCLUDED."_account_id"
229
+ WHERE ${_QueryUtils.quoteIdent(table)}."_last_synced_at" IS NULL
230
+ OR ${_QueryUtils.quoteIdent(table)}."_last_synced_at" < $${tsParamIdx}
231
+ RETURNING *
232
+ `;
233
+ return { sql: sql3, params };
234
+ }
235
+ };
236
+
237
+ // src/database/postgres.ts
180
238
  var ORDERED_STRIPE_TABLES = [
239
+ "exchange_rates_from_usd",
181
240
  "subscription_items",
241
+ "subscription_item_change_events_v2_beta",
182
242
  "subscriptions",
183
243
  "subscription_schedules",
184
244
  "checkout_session_line_items",
@@ -248,7 +308,7 @@ var PostgresClient = class {
248
308
  }
249
309
  return results.flatMap((it) => it.rows);
250
310
  }
251
- async upsertManyWithTimestampProtection(entries, table, accountId, syncTimestamp) {
311
+ async upsertManyWithTimestampProtection(entries, table, accountId, syncTimestamp, upsertOptions) {
252
312
  const timestamp = syncTimestamp || (/* @__PURE__ */ new Date()).toISOString();
253
313
  if (!entries.length) return [];
254
314
  const chunkSize = 5;
@@ -283,20 +343,33 @@ var PostgresClient = class {
283
343
  const prepared = (0, import_yesql.pg)(upsertSql, { useNullForMissing: true })(cleansed);
284
344
  queries.push(this.pool.query(prepared.text, prepared.values));
285
345
  } else {
286
- const rawData = JSON.stringify(entry);
287
- const upsertSql = `
288
- INSERT INTO "${this.config.schema}"."${table}" ("_raw_data", "_last_synced_at", "_account_id")
289
- VALUES ($1::jsonb, $2, $3)
290
- ON CONFLICT (id)
291
- DO UPDATE SET
292
- "_raw_data" = EXCLUDED."_raw_data",
293
- "_last_synced_at" = $2,
294
- "_account_id" = EXCLUDED."_account_id"
295
- WHERE "${table}"."_last_synced_at" IS NULL
296
- OR "${table}"."_last_synced_at" < $2
297
- RETURNING *
298
- `;
299
- queries.push(this.pool.query(upsertSql, [rawData, timestamp, accountId]));
346
+ const conflictTarget = upsertOptions?.conflictTarget ?? ["id"];
347
+ const extraColumns = upsertOptions?.extraColumns ?? [];
348
+ if (!conflictTarget.length) {
349
+ throw new Error(`Invalid upsert config for ${table}: conflictTarget must be non-empty`);
350
+ }
351
+ const columns = [
352
+ { column: "_raw_data", pgType: "jsonb", value: JSON.stringify(entry) },
353
+ ...extraColumns.map((c) => ({
354
+ column: c.column,
355
+ pgType: c.pgType,
356
+ value: entry[c.entryKey]
357
+ })),
358
+ { column: "_last_synced_at", pgType: "timestamptz", value: timestamp },
359
+ { column: "_account_id", pgType: "text", value: accountId }
360
+ ];
361
+ for (const c of columns) {
362
+ if (c.value === void 0) {
363
+ throw new Error(`Missing required value for ${table}.${c.column}`);
364
+ }
365
+ }
366
+ const { sql: upsertSql, params } = QueryUtils.buildRawJsonUpsertQuery(
367
+ this.config.schema,
368
+ table,
369
+ columns,
370
+ conflictTarget
371
+ );
372
+ queries.push(this.pool.query(upsertSql, params));
300
373
  }
301
374
  });
302
375
  results.push(...await Promise.all(queries));
@@ -696,7 +769,12 @@ var PostgresClient = class {
696
769
  } else {
697
770
  await this.query(
698
771
  `UPDATE "${this.config.schema}"."_sync_obj_runs"
699
- SET cursor = $4, updated_at = now()
772
+ SET cursor = CASE
773
+ WHEN cursor IS NULL THEN $4
774
+ WHEN (cursor COLLATE "C") < ($4::text COLLATE "C") THEN $4
775
+ ELSE cursor
776
+ END,
777
+ updated_at = now()
700
778
  WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
701
779
  [accountId, runStartedAt, object, cursor]
702
780
  );
@@ -707,10 +785,17 @@ var PostgresClient = class {
707
785
  * This considers completed, error, AND running runs to ensure recovery syncs
708
786
  * don't re-process data that was already synced before a crash.
709
787
  * A 'running' status with a cursor means the process was killed mid-sync.
788
+ *
789
+ * Handles two cursor formats:
790
+ * - Numeric: compared as bigint for correct ordering
791
+ * - Composite cursors: compared as strings with COLLATE "C"
710
792
  */
711
793
  async getLastCompletedCursor(accountId, object) {
712
794
  const result = await this.query(
713
- `SELECT MAX(o.cursor::bigint)::text as cursor
795
+ `SELECT CASE
796
+ WHEN BOOL_OR(o.cursor !~ '^\\d+$') THEN MAX(o.cursor COLLATE "C")
797
+ ELSE MAX(CASE WHEN o.cursor ~ '^\\d+$' THEN o.cursor::bigint END)::text
798
+ END as cursor
714
799
  FROM "${this.config.schema}"."_sync_obj_runs" o
715
800
  WHERE o."_account_id" = $1
716
801
  AND o.object = $2
@@ -995,6 +1080,269 @@ function hashApiKey(apiKey) {
995
1080
  return (0, import_crypto.createHash)("sha256").update(apiKey).digest("hex");
996
1081
  }
997
1082
 
1083
+ // src/sigma/sigmaApi.ts
1084
+ var import_papaparse = __toESM(require("papaparse"), 1);
1085
+ var import_stripe2 = __toESM(require("stripe"), 1);
1086
+ var STRIPE_FILES_BASE = "https://files.stripe.com/v1";
1087
+ function sleep2(ms) {
1088
+ return new Promise((resolve) => setTimeout(resolve, ms));
1089
+ }
1090
+ function parseCsvObjects(csv) {
1091
+ const input = csv.replace(/^\uFEFF/, "");
1092
+ const parsed = import_papaparse.default.parse(input, {
1093
+ header: true,
1094
+ skipEmptyLines: "greedy"
1095
+ });
1096
+ if (parsed.errors.length > 0) {
1097
+ throw new Error(`Failed to parse Sigma CSV: ${parsed.errors[0]?.message ?? "unknown error"}`);
1098
+ }
1099
+ return parsed.data.filter((row) => row && Object.keys(row).length > 0).map(
1100
+ (row) => Object.fromEntries(
1101
+ Object.entries(row).map(([k, v]) => [k, v == null || v === "" ? null : String(v)])
1102
+ )
1103
+ );
1104
+ }
1105
+ function normalizeSigmaTimestampToIso(value) {
1106
+ const v = value.trim();
1107
+ if (!v) return null;
1108
+ const hasExplicitTz = /z$|[+-]\d{2}:?\d{2}$/i.test(v);
1109
+ const isoish = v.includes("T") ? v : v.replace(" ", "T");
1110
+ const candidate = hasExplicitTz ? isoish : `${isoish}Z`;
1111
+ const d = new Date(candidate);
1112
+ if (Number.isNaN(d.getTime())) return null;
1113
+ return d.toISOString();
1114
+ }
1115
+ async function fetchStripeText(url, apiKey, options) {
1116
+ const res = await fetch(url, {
1117
+ ...options,
1118
+ headers: {
1119
+ ...options.headers ?? {},
1120
+ Authorization: `Bearer ${apiKey}`
1121
+ }
1122
+ });
1123
+ const text = await res.text();
1124
+ if (!res.ok) {
1125
+ throw new Error(`Sigma file download error (${res.status}) for ${url}: ${text}`);
1126
+ }
1127
+ return text;
1128
+ }
1129
+ async function runSigmaQueryAndDownloadCsv(params) {
1130
+ const pollTimeoutMs = params.pollTimeoutMs ?? 5 * 60 * 1e3;
1131
+ const pollIntervalMs = params.pollIntervalMs ?? 2e3;
1132
+ const stripe = new import_stripe2.default(params.apiKey);
1133
+ const created = await stripe.rawRequest("POST", "/v1/sigma/query_runs", {
1134
+ sql: params.sql
1135
+ });
1136
+ const queryRunId = created.id;
1137
+ const start = Date.now();
1138
+ let current = created;
1139
+ while (current.status === "running") {
1140
+ if (Date.now() - start > pollTimeoutMs) {
1141
+ throw new Error(`Sigma query run timed out after ${pollTimeoutMs}ms: ${queryRunId}`);
1142
+ }
1143
+ await sleep2(pollIntervalMs);
1144
+ current = await stripe.rawRequest(
1145
+ "GET",
1146
+ `/v1/sigma/query_runs/${queryRunId}`,
1147
+ {}
1148
+ );
1149
+ }
1150
+ if (current.status !== "succeeded") {
1151
+ throw new Error(
1152
+ `Sigma query run did not succeed (status=${current.status}) id=${queryRunId} error=${JSON.stringify(
1153
+ current.error
1154
+ )}`
1155
+ );
1156
+ }
1157
+ const fileId = current.result?.file;
1158
+ if (!fileId) {
1159
+ throw new Error(`Sigma query run succeeded but result.file is missing (id=${queryRunId})`);
1160
+ }
1161
+ const csv = await fetchStripeText(
1162
+ `${STRIPE_FILES_BASE}/files/${fileId}/contents`,
1163
+ params.apiKey,
1164
+ { method: "GET" }
1165
+ );
1166
+ return { queryRunId, fileId, csv };
1167
+ }
1168
+
1169
+ // src/sigma/sigmaIngestionConfigs.ts
1170
+ var SIGMA_INGESTION_CONFIGS = {
1171
+ subscription_item_change_events_v2_beta: {
1172
+ sigmaTable: "subscription_item_change_events_v2_beta",
1173
+ destinationTable: "subscription_item_change_events_v2_beta",
1174
+ pageSize: 1e4,
1175
+ cursor: {
1176
+ version: 1,
1177
+ columns: [
1178
+ { column: "event_timestamp", type: "timestamp" },
1179
+ { column: "event_type", type: "string" },
1180
+ { column: "subscription_item_id", type: "string" }
1181
+ ]
1182
+ },
1183
+ upsert: {
1184
+ conflictTarget: ["_account_id", "event_timestamp", "event_type", "subscription_item_id"],
1185
+ extraColumns: [
1186
+ { column: "event_timestamp", pgType: "timestamptz", entryKey: "event_timestamp" },
1187
+ { column: "event_type", pgType: "text", entryKey: "event_type" },
1188
+ { column: "subscription_item_id", pgType: "text", entryKey: "subscription_item_id" }
1189
+ ]
1190
+ }
1191
+ },
1192
+ exchange_rates_from_usd: {
1193
+ sigmaTable: "exchange_rates_from_usd",
1194
+ destinationTable: "exchange_rates_from_usd",
1195
+ pageSize: 1e4,
1196
+ cursor: {
1197
+ version: 1,
1198
+ columns: [
1199
+ { column: "date", type: "string" },
1200
+ { column: "sell_currency", type: "string" }
1201
+ ]
1202
+ },
1203
+ upsert: {
1204
+ conflictTarget: ["_account_id", "date", "sell_currency"],
1205
+ extraColumns: [
1206
+ { column: "date", pgType: "date", entryKey: "date" },
1207
+ { column: "sell_currency", pgType: "text", entryKey: "sell_currency" }
1208
+ ]
1209
+ }
1210
+ }
1211
+ };
1212
+
1213
+ // src/sigma/sigmaIngestion.ts
1214
+ var SIGMA_CURSOR_DELIM = "";
1215
+ function escapeSigmaSqlStringLiteral(value) {
1216
+ return value.replace(/'/g, "''");
1217
+ }
1218
+ function formatSigmaTimestampForSqlLiteral(date) {
1219
+ return date.toISOString().replace("T", " ").replace("Z", "");
1220
+ }
1221
+ function decodeSigmaCursorValues(spec, cursor) {
1222
+ const prefix = `v${spec.version}${SIGMA_CURSOR_DELIM}`;
1223
+ if (!cursor.startsWith(prefix)) {
1224
+ throw new Error(
1225
+ `Unrecognized Sigma cursor format (expected prefix ${JSON.stringify(prefix)}): ${cursor}`
1226
+ );
1227
+ }
1228
+ const parts = cursor.split(SIGMA_CURSOR_DELIM);
1229
+ const expected = 1 + spec.columns.length;
1230
+ if (parts.length !== expected) {
1231
+ throw new Error(`Malformed Sigma cursor: expected ${expected} parts, got ${parts.length}`);
1232
+ }
1233
+ return parts.slice(1);
1234
+ }
1235
+ function encodeSigmaCursor(spec, values) {
1236
+ if (values.length !== spec.columns.length) {
1237
+ throw new Error(
1238
+ `Cannot encode Sigma cursor: expected ${spec.columns.length} values, got ${values.length}`
1239
+ );
1240
+ }
1241
+ for (const v of values) {
1242
+ if (v.includes(SIGMA_CURSOR_DELIM)) {
1243
+ throw new Error("Cannot encode Sigma cursor: value contains delimiter character");
1244
+ }
1245
+ }
1246
+ return [`v${spec.version}`, ...values].join(SIGMA_CURSOR_DELIM);
1247
+ }
1248
+ function sigmaSqlLiteralForCursorValue(spec, rawValue) {
1249
+ switch (spec.type) {
1250
+ case "timestamp": {
1251
+ const d = new Date(rawValue);
1252
+ if (Number.isNaN(d.getTime())) {
1253
+ throw new Error(`Invalid timestamp cursor value for ${spec.column}: ${rawValue}`);
1254
+ }
1255
+ return `timestamp '${formatSigmaTimestampForSqlLiteral(d)}'`;
1256
+ }
1257
+ case "number": {
1258
+ if (!/^-?\d+(\.\d+)?$/.test(rawValue)) {
1259
+ throw new Error(`Invalid numeric cursor value for ${spec.column}: ${rawValue}`);
1260
+ }
1261
+ return rawValue;
1262
+ }
1263
+ case "string":
1264
+ return `'${escapeSigmaSqlStringLiteral(rawValue)}'`;
1265
+ }
1266
+ }
1267
+ function buildSigmaCursorWhereClause(spec, cursorValues) {
1268
+ if (cursorValues.length !== spec.columns.length) {
1269
+ throw new Error(
1270
+ `Cannot build Sigma cursor predicate: expected ${spec.columns.length} values, got ${cursorValues.length}`
1271
+ );
1272
+ }
1273
+ const cols = spec.columns.map((c) => c.column);
1274
+ const lits = spec.columns.map((c, i) => sigmaSqlLiteralForCursorValue(c, cursorValues[i] ?? ""));
1275
+ const ors = [];
1276
+ for (let i = 0; i < cols.length; i++) {
1277
+ const ands = [];
1278
+ for (let j = 0; j < i; j++) {
1279
+ ands.push(`${cols[j]} = ${lits[j]}`);
1280
+ }
1281
+ ands.push(`${cols[i]} > ${lits[i]}`);
1282
+ ors.push(`(${ands.join(" AND ")})`);
1283
+ }
1284
+ return ors.join(" OR ");
1285
+ }
1286
+ function buildSigmaQuery(config, cursor) {
1287
+ const select = config.select === void 0 || config.select === "*" ? "*" : config.select.join(", ");
1288
+ const whereParts = [];
1289
+ if (config.additionalWhere) {
1290
+ whereParts.push(`(${config.additionalWhere})`);
1291
+ }
1292
+ if (cursor) {
1293
+ const values = decodeSigmaCursorValues(config.cursor, cursor);
1294
+ const predicate = buildSigmaCursorWhereClause(config.cursor, values);
1295
+ whereParts.push(`(${predicate})`);
1296
+ }
1297
+ const whereClause = whereParts.length > 0 ? `WHERE ${whereParts.join(" AND ")}` : "";
1298
+ const orderBy = config.cursor.columns.map((c) => c.column).join(", ");
1299
+ return [
1300
+ `SELECT ${select} FROM ${config.sigmaTable}`,
1301
+ whereClause,
1302
+ `ORDER BY ${orderBy} ASC`,
1303
+ `LIMIT ${config.pageSize}`
1304
+ ].filter(Boolean).join(" ");
1305
+ }
1306
+ function defaultSigmaRowToEntry(config, row) {
1307
+ const out = { ...row };
1308
+ for (const col of config.cursor.columns) {
1309
+ const raw = row[col.column];
1310
+ if (raw == null) {
1311
+ throw new Error(`Sigma row missing required cursor column: ${col.column}`);
1312
+ }
1313
+ if (col.type === "timestamp") {
1314
+ const normalized = normalizeSigmaTimestampToIso(raw);
1315
+ if (!normalized) {
1316
+ throw new Error(`Sigma row has invalid timestamp for ${col.column}: ${raw}`);
1317
+ }
1318
+ out[col.column] = normalized;
1319
+ } else if (col.type === "string") {
1320
+ const v = raw.trim();
1321
+ if (!v) {
1322
+ throw new Error(`Sigma row has empty string for required cursor column: ${col.column}`);
1323
+ }
1324
+ out[col.column] = v;
1325
+ } else {
1326
+ const v = raw.trim();
1327
+ if (!v) {
1328
+ throw new Error(`Sigma row has empty value for required cursor column: ${col.column}`);
1329
+ }
1330
+ out[col.column] = v;
1331
+ }
1332
+ }
1333
+ return out;
1334
+ }
1335
+ function sigmaCursorFromEntry(config, entry) {
1336
+ const values = config.cursor.columns.map((c) => {
1337
+ const raw = entry[c.column];
1338
+ if (raw == null) {
1339
+ throw new Error(`Cannot build cursor: entry missing ${c.column}`);
1340
+ }
1341
+ return String(raw);
1342
+ });
1343
+ return encodeSigmaCursor(config.cursor, values);
1344
+ }
1345
+
998
1346
  // src/stripeSync.ts
999
1347
  function getUniqueIds(entries, key) {
1000
1348
  const set = new Set(
@@ -1005,7 +1353,7 @@ function getUniqueIds(entries, key) {
1005
1353
  var StripeSync = class {
1006
1354
  constructor(config) {
1007
1355
  this.config = config;
1008
- const baseStripe = new import_stripe2.default(config.stripeSecretKey, {
1356
+ const baseStripe = new import_stripe3.default(config.stripeSecretKey, {
1009
1357
  // https://github.com/stripe/stripe-node#configuration
1010
1358
  // @ts-ignore
1011
1359
  apiVersion: config.stripeApiVersion,
@@ -1406,6 +1754,17 @@ var StripeSync = class {
1406
1754
  listFn: (p) => this.stripe.checkout.sessions.list(p),
1407
1755
  upsertFn: (items, id) => this.upsertCheckoutSessions(items, id),
1408
1756
  supportsCreatedFilter: true
1757
+ },
1758
+ // Sigma-backed resources
1759
+ subscription_item_change_events_v2_beta: {
1760
+ order: 18,
1761
+ supportsCreatedFilter: false,
1762
+ sigma: SIGMA_INGESTION_CONFIGS.subscription_item_change_events_v2_beta
1763
+ },
1764
+ exchange_rates_from_usd: {
1765
+ order: 19,
1766
+ supportsCreatedFilter: false,
1767
+ sigma: SIGMA_INGESTION_CONFIGS.exchange_rates_from_usd
1409
1768
  }
1410
1769
  };
1411
1770
  async processEvent(event) {
@@ -1438,7 +1797,13 @@ var StripeSync = class {
1438
1797
  * Order is determined by the `order` field in resourceRegistry.
1439
1798
  */
1440
1799
  getSupportedSyncObjects() {
1441
- return Object.entries(this.resourceRegistry).sort(([, a], [, b]) => a.order - b.order).map(([key]) => key);
1800
+ const all = Object.entries(this.resourceRegistry).sort(([, a], [, b]) => a.order - b.order).map(([key]) => key);
1801
+ if (!this.config.enableSigma) {
1802
+ return all.filter(
1803
+ (o) => o !== "subscription_item_change_events_v2_beta" && o !== "exchange_rates_from_usd"
1804
+ );
1805
+ }
1806
+ return all;
1442
1807
  }
1443
1808
  // Event handler methods
1444
1809
  async handleChargeEvent(event, accountId) {
@@ -1517,7 +1882,7 @@ var StripeSync = class {
1517
1882
  );
1518
1883
  await this.upsertProducts([product], accountId, this.getSyncTimestamp(event, refetched));
1519
1884
  } catch (err) {
1520
- if (err instanceof import_stripe2.default.errors.StripeAPIError && err.code === "resource_missing") {
1885
+ if (err instanceof import_stripe3.default.errors.StripeAPIError && err.code === "resource_missing") {
1521
1886
  const product = event.data.object;
1522
1887
  await this.deleteProduct(product.id);
1523
1888
  } else {
@@ -1537,7 +1902,7 @@ var StripeSync = class {
1537
1902
  );
1538
1903
  await this.upsertPrices([price], accountId, false, this.getSyncTimestamp(event, refetched));
1539
1904
  } catch (err) {
1540
- if (err instanceof import_stripe2.default.errors.StripeAPIError && err.code === "resource_missing") {
1905
+ if (err instanceof import_stripe3.default.errors.StripeAPIError && err.code === "resource_missing") {
1541
1906
  const price = event.data.object;
1542
1907
  await this.deletePrice(price.id);
1543
1908
  } else {
@@ -1557,7 +1922,7 @@ var StripeSync = class {
1557
1922
  );
1558
1923
  await this.upsertPlans([plan], accountId, false, this.getSyncTimestamp(event, refetched));
1559
1924
  } catch (err) {
1560
- if (err instanceof import_stripe2.default.errors.StripeAPIError && err.code === "resource_missing") {
1925
+ if (err instanceof import_stripe3.default.errors.StripeAPIError && err.code === "resource_missing") {
1561
1926
  const plan = event.data.object;
1562
1927
  await this.deletePlan(plan.id);
1563
1928
  } else {
@@ -1804,10 +2169,10 @@ var StripeSync = class {
1804
2169
  let cursor = null;
1805
2170
  if (!params?.created) {
1806
2171
  if (objRun?.cursor) {
1807
- cursor = parseInt(objRun.cursor);
2172
+ cursor = objRun.cursor;
1808
2173
  } else {
1809
2174
  const lastCursor = await this.postgresClient.getLastCompletedCursor(accountId, resourceName);
1810
- cursor = lastCursor ? parseInt(lastCursor) : null;
2175
+ cursor = lastCursor ?? null;
1811
2176
  }
1812
2177
  }
1813
2178
  const result = await this.fetchOnePage(
@@ -1862,9 +2227,18 @@ var StripeSync = class {
1862
2227
  throw new Error(`Unsupported object type for processNext: ${object}`);
1863
2228
  }
1864
2229
  try {
2230
+ if (config.sigma) {
2231
+ return await this.fetchOneSigmaPage(
2232
+ accountId,
2233
+ resourceName,
2234
+ runStartedAt,
2235
+ cursor,
2236
+ config.sigma
2237
+ );
2238
+ }
1865
2239
  const listParams = { limit };
1866
2240
  if (config.supportsCreatedFilter) {
1867
- const created = params?.created ?? (cursor ? { gte: cursor } : void 0);
2241
+ const created = params?.created ?? (cursor && /^\d+$/.test(cursor) ? { gte: Number.parseInt(cursor, 10) } : void 0);
1868
2242
  if (created) {
1869
2243
  listParams.created = created;
1870
2244
  }
@@ -1909,6 +2283,97 @@ var StripeSync = class {
1909
2283
  throw error;
1910
2284
  }
1911
2285
  }
2286
+ async getSigmaFallbackCursorFromDestination(accountId, sigmaConfig) {
2287
+ const cursorCols = sigmaConfig.cursor.columns;
2288
+ const selectCols = cursorCols.map((c) => `"${c.column}"`).join(", ");
2289
+ const orderBy = cursorCols.map((c) => `"${c.column}" DESC`).join(", ");
2290
+ const result = await this.postgresClient.query(
2291
+ `SELECT ${selectCols}
2292
+ FROM "stripe"."${sigmaConfig.destinationTable}"
2293
+ WHERE "_account_id" = $1
2294
+ ORDER BY ${orderBy}
2295
+ LIMIT 1`,
2296
+ [accountId]
2297
+ );
2298
+ if (result.rows.length === 0) return null;
2299
+ const row = result.rows[0];
2300
+ const entryForCursor = {};
2301
+ for (const c of cursorCols) {
2302
+ const v = row[c.column];
2303
+ if (v == null) {
2304
+ throw new Error(
2305
+ `Sigma fallback cursor query returned null for ${sigmaConfig.destinationTable}.${c.column}`
2306
+ );
2307
+ }
2308
+ if (c.type === "timestamp") {
2309
+ const d = v instanceof Date ? v : new Date(String(v));
2310
+ if (Number.isNaN(d.getTime())) {
2311
+ throw new Error(
2312
+ `Sigma fallback cursor query returned invalid timestamp for ${sigmaConfig.destinationTable}.${c.column}: ${String(
2313
+ v
2314
+ )}`
2315
+ );
2316
+ }
2317
+ entryForCursor[c.column] = d.toISOString();
2318
+ } else {
2319
+ entryForCursor[c.column] = String(v);
2320
+ }
2321
+ }
2322
+ return sigmaCursorFromEntry(sigmaConfig, entryForCursor);
2323
+ }
2324
+ async fetchOneSigmaPage(accountId, resourceName, runStartedAt, cursor, sigmaConfig) {
2325
+ if (!this.config.stripeSecretKey) {
2326
+ throw new Error("Sigma sync requested but stripeSecretKey is not configured.");
2327
+ }
2328
+ if (resourceName !== sigmaConfig.destinationTable) {
2329
+ throw new Error(
2330
+ `Sigma sync config mismatch: resourceName=${resourceName} destinationTable=${sigmaConfig.destinationTable}`
2331
+ );
2332
+ }
2333
+ const effectiveCursor = cursor ?? await this.getSigmaFallbackCursorFromDestination(accountId, sigmaConfig);
2334
+ const sigmaSql = buildSigmaQuery(sigmaConfig, effectiveCursor);
2335
+ this.config.logger?.info(
2336
+ { object: resourceName, pageSize: sigmaConfig.pageSize, hasCursor: Boolean(effectiveCursor) },
2337
+ "Sigma sync: running query"
2338
+ );
2339
+ const { queryRunId, fileId, csv } = await runSigmaQueryAndDownloadCsv({
2340
+ apiKey: this.config.stripeSecretKey,
2341
+ sql: sigmaSql,
2342
+ logger: this.config.logger
2343
+ });
2344
+ const rows = parseCsvObjects(csv);
2345
+ if (rows.length === 0) {
2346
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
2347
+ return { processed: 0, hasMore: false, runStartedAt };
2348
+ }
2349
+ const entries = rows.map(
2350
+ (row) => defaultSigmaRowToEntry(sigmaConfig, row)
2351
+ );
2352
+ this.config.logger?.info(
2353
+ { object: resourceName, rows: entries.length, queryRunId, fileId },
2354
+ "Sigma sync: upserting rows"
2355
+ );
2356
+ await this.postgresClient.upsertManyWithTimestampProtection(
2357
+ entries,
2358
+ resourceName,
2359
+ accountId,
2360
+ void 0,
2361
+ sigmaConfig.upsert
2362
+ );
2363
+ await this.postgresClient.incrementObjectProgress(
2364
+ accountId,
2365
+ runStartedAt,
2366
+ resourceName,
2367
+ entries.length
2368
+ );
2369
+ const newCursor = sigmaCursorFromEntry(sigmaConfig, entries[entries.length - 1]);
2370
+ await this.postgresClient.updateObjectCursor(accountId, runStartedAt, resourceName, newCursor);
2371
+ const hasMore = rows.length === sigmaConfig.pageSize;
2372
+ if (!hasMore) {
2373
+ await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
2374
+ }
2375
+ return { processed: entries.length, hasMore, runStartedAt };
2376
+ }
1912
2377
  /**
1913
2378
  * Process all pages for all (or specified) object types until complete.
1914
2379
  *
@@ -2037,6 +2502,12 @@ var StripeSync = class {
2037
2502
  case "checkout_sessions":
2038
2503
  results.checkoutSessions = result;
2039
2504
  break;
2505
+ case "subscription_item_change_events_v2_beta":
2506
+ results.subscriptionItemChangeEventsV2Beta = result;
2507
+ break;
2508
+ case "exchange_rates_from_usd":
2509
+ results.exchangeRatesFromUsd = result;
2510
+ break;
2040
2511
  }
2041
2512
  }
2042
2513
  }
@@ -2062,30 +2533,41 @@ var StripeSync = class {
2062
2533
  const customerIds = await this.postgresClient.query(prepared.text, prepared.values).then(({ rows }) => rows.map((it) => it.id));
2063
2534
  this.config.logger?.info(`Getting payment methods for ${customerIds.length} customers`);
2064
2535
  let synced = 0;
2065
- for (const customerIdChunk of chunkArray(customerIds, 10)) {
2536
+ const chunkSize = this.config.maxConcurrentCustomers ?? 10;
2537
+ for (const customerIdChunk of chunkArray(customerIds, chunkSize)) {
2066
2538
  await Promise.all(
2067
2539
  customerIdChunk.map(async (customerId) => {
2068
2540
  const CHECKPOINT_SIZE = 100;
2069
2541
  let currentBatch = [];
2070
- for await (const item of this.stripe.paymentMethods.list({
2071
- limit: 100,
2072
- customer: customerId
2073
- })) {
2074
- currentBatch.push(item);
2075
- if (currentBatch.length >= CHECKPOINT_SIZE) {
2076
- await this.upsertPaymentMethods(
2077
- currentBatch,
2078
- accountId,
2079
- syncParams?.backfillRelatedEntities
2080
- );
2081
- synced += currentBatch.length;
2082
- await this.postgresClient.incrementObjectProgress(
2083
- accountId,
2084
- runStartedAt,
2085
- resourceName,
2086
- currentBatch.length
2087
- );
2088
- currentBatch = [];
2542
+ let hasMore = true;
2543
+ let startingAfter = void 0;
2544
+ while (hasMore) {
2545
+ const response = await this.stripe.paymentMethods.list({
2546
+ limit: 100,
2547
+ customer: customerId,
2548
+ ...startingAfter ? { starting_after: startingAfter } : {}
2549
+ });
2550
+ for (const item of response.data) {
2551
+ currentBatch.push(item);
2552
+ if (currentBatch.length >= CHECKPOINT_SIZE) {
2553
+ await this.upsertPaymentMethods(
2554
+ currentBatch,
2555
+ accountId,
2556
+ syncParams?.backfillRelatedEntities
2557
+ );
2558
+ synced += currentBatch.length;
2559
+ await this.postgresClient.incrementObjectProgress(
2560
+ accountId,
2561
+ runStartedAt,
2562
+ resourceName,
2563
+ currentBatch.length
2564
+ );
2565
+ currentBatch = [];
2566
+ }
2567
+ }
2568
+ hasMore = response.has_more;
2569
+ if (response.data.length > 0) {
2570
+ startingAfter = response.data[response.data.length - 1].id;
2089
2571
  }
2090
2572
  }
2091
2573
  if (currentBatch.length > 0) {
@@ -2129,7 +2611,7 @@ var StripeSync = class {
2129
2611
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2130
2612
  }
2131
2613
  return this.fetchAndUpsert(
2132
- () => this.stripe.products.list(params),
2614
+ (pagination) => this.stripe.products.list({ ...params, ...pagination }),
2133
2615
  (products) => this.upsertProducts(products, accountId),
2134
2616
  accountId,
2135
2617
  "products",
@@ -2149,7 +2631,7 @@ var StripeSync = class {
2149
2631
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2150
2632
  }
2151
2633
  return this.fetchAndUpsert(
2152
- () => this.stripe.prices.list(params),
2634
+ (pagination) => this.stripe.prices.list({ ...params, ...pagination }),
2153
2635
  (prices) => this.upsertPrices(prices, accountId, syncParams?.backfillRelatedEntities),
2154
2636
  accountId,
2155
2637
  "prices",
@@ -2169,7 +2651,7 @@ var StripeSync = class {
2169
2651
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2170
2652
  }
2171
2653
  return this.fetchAndUpsert(
2172
- () => this.stripe.plans.list(params),
2654
+ (pagination) => this.stripe.plans.list({ ...params, ...pagination }),
2173
2655
  (plans) => this.upsertPlans(plans, accountId, syncParams?.backfillRelatedEntities),
2174
2656
  accountId,
2175
2657
  "plans",
@@ -2189,7 +2671,7 @@ var StripeSync = class {
2189
2671
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2190
2672
  }
2191
2673
  return this.fetchAndUpsert(
2192
- () => this.stripe.customers.list(params),
2674
+ (pagination) => this.stripe.customers.list({ ...params, ...pagination }),
2193
2675
  // @ts-expect-error
2194
2676
  (items) => this.upsertCustomers(items, accountId),
2195
2677
  accountId,
@@ -2210,7 +2692,7 @@ var StripeSync = class {
2210
2692
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2211
2693
  }
2212
2694
  return this.fetchAndUpsert(
2213
- () => this.stripe.subscriptions.list(params),
2695
+ (pagination) => this.stripe.subscriptions.list({ ...params, ...pagination }),
2214
2696
  (items) => this.upsertSubscriptions(items, accountId, syncParams?.backfillRelatedEntities),
2215
2697
  accountId,
2216
2698
  "subscriptions",
@@ -2233,7 +2715,7 @@ var StripeSync = class {
2233
2715
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2234
2716
  }
2235
2717
  return this.fetchAndUpsert(
2236
- () => this.stripe.subscriptionSchedules.list(params),
2718
+ (pagination) => this.stripe.subscriptionSchedules.list({ ...params, ...pagination }),
2237
2719
  (items) => this.upsertSubscriptionSchedules(items, accountId, syncParams?.backfillRelatedEntities),
2238
2720
  accountId,
2239
2721
  "subscription_schedules",
@@ -2254,7 +2736,7 @@ var StripeSync = class {
2254
2736
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2255
2737
  }
2256
2738
  return this.fetchAndUpsert(
2257
- () => this.stripe.invoices.list(params),
2739
+ (pagination) => this.stripe.invoices.list({ ...params, ...pagination }),
2258
2740
  (items) => this.upsertInvoices(items, accountId, syncParams?.backfillRelatedEntities),
2259
2741
  accountId,
2260
2742
  "invoices",
@@ -2274,7 +2756,7 @@ var StripeSync = class {
2274
2756
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2275
2757
  }
2276
2758
  return this.fetchAndUpsert(
2277
- () => this.stripe.charges.list(params),
2759
+ (pagination) => this.stripe.charges.list({ ...params, ...pagination }),
2278
2760
  (items) => this.upsertCharges(items, accountId, syncParams?.backfillRelatedEntities),
2279
2761
  accountId,
2280
2762
  "charges",
@@ -2294,7 +2776,7 @@ var StripeSync = class {
2294
2776
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2295
2777
  }
2296
2778
  return this.fetchAndUpsert(
2297
- () => this.stripe.setupIntents.list(params),
2779
+ (pagination) => this.stripe.setupIntents.list({ ...params, ...pagination }),
2298
2780
  (items) => this.upsertSetupIntents(items, accountId, syncParams?.backfillRelatedEntities),
2299
2781
  accountId,
2300
2782
  "setup_intents",
@@ -2317,7 +2799,7 @@ var StripeSync = class {
2317
2799
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2318
2800
  }
2319
2801
  return this.fetchAndUpsert(
2320
- () => this.stripe.paymentIntents.list(params),
2802
+ (pagination) => this.stripe.paymentIntents.list({ ...params, ...pagination }),
2321
2803
  (items) => this.upsertPaymentIntents(items, accountId, syncParams?.backfillRelatedEntities),
2322
2804
  accountId,
2323
2805
  "payment_intents",
@@ -2332,7 +2814,7 @@ var StripeSync = class {
2332
2814
  const accountId = await this.getAccountId();
2333
2815
  const params = { limit: 100 };
2334
2816
  return this.fetchAndUpsert(
2335
- () => this.stripe.taxIds.list(params),
2817
+ (pagination) => this.stripe.taxIds.list({ ...params, ...pagination }),
2336
2818
  (items) => this.upsertTaxIds(items, accountId, syncParams?.backfillRelatedEntities),
2337
2819
  accountId,
2338
2820
  "tax_ids",
@@ -2353,30 +2835,41 @@ var StripeSync = class {
2353
2835
  const customerIds = await this.postgresClient.query(prepared.text, prepared.values).then(({ rows }) => rows.map((it) => it.id));
2354
2836
  this.config.logger?.info(`Getting payment methods for ${customerIds.length} customers`);
2355
2837
  let synced = 0;
2356
- for (const customerIdChunk of chunkArray(customerIds, 10)) {
2838
+ const chunkSize = this.config.maxConcurrentCustomers ?? 10;
2839
+ for (const customerIdChunk of chunkArray(customerIds, chunkSize)) {
2357
2840
  await Promise.all(
2358
2841
  customerIdChunk.map(async (customerId) => {
2359
2842
  const CHECKPOINT_SIZE = 100;
2360
2843
  let currentBatch = [];
2361
- for await (const item of this.stripe.paymentMethods.list({
2362
- limit: 100,
2363
- customer: customerId
2364
- })) {
2365
- currentBatch.push(item);
2366
- if (currentBatch.length >= CHECKPOINT_SIZE) {
2367
- await this.upsertPaymentMethods(
2368
- currentBatch,
2369
- accountId,
2370
- syncParams?.backfillRelatedEntities
2371
- );
2372
- synced += currentBatch.length;
2373
- await this.postgresClient.incrementObjectProgress(
2374
- accountId,
2375
- runStartedAt,
2376
- "payment_methods",
2377
- currentBatch.length
2378
- );
2379
- currentBatch = [];
2844
+ let hasMore = true;
2845
+ let startingAfter = void 0;
2846
+ while (hasMore) {
2847
+ const response = await this.stripe.paymentMethods.list({
2848
+ limit: 100,
2849
+ customer: customerId,
2850
+ ...startingAfter ? { starting_after: startingAfter } : {}
2851
+ });
2852
+ for (const item of response.data) {
2853
+ currentBatch.push(item);
2854
+ if (currentBatch.length >= CHECKPOINT_SIZE) {
2855
+ await this.upsertPaymentMethods(
2856
+ currentBatch,
2857
+ accountId,
2858
+ syncParams?.backfillRelatedEntities
2859
+ );
2860
+ synced += currentBatch.length;
2861
+ await this.postgresClient.incrementObjectProgress(
2862
+ accountId,
2863
+ runStartedAt,
2864
+ "payment_methods",
2865
+ currentBatch.length
2866
+ );
2867
+ currentBatch = [];
2868
+ }
2869
+ }
2870
+ hasMore = response.has_more;
2871
+ if (response.data.length > 0) {
2872
+ startingAfter = response.data[response.data.length - 1].id;
2380
2873
  }
2381
2874
  }
2382
2875
  if (currentBatch.length > 0) {
@@ -2413,7 +2906,7 @@ var StripeSync = class {
2413
2906
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2414
2907
  }
2415
2908
  return this.fetchAndUpsert(
2416
- () => this.stripe.disputes.list(params),
2909
+ (pagination) => this.stripe.disputes.list({ ...params, ...pagination }),
2417
2910
  (items) => this.upsertDisputes(items, accountId, syncParams?.backfillRelatedEntities),
2418
2911
  accountId,
2419
2912
  "disputes",
@@ -2436,7 +2929,7 @@ var StripeSync = class {
2436
2929
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2437
2930
  }
2438
2931
  return this.fetchAndUpsert(
2439
- () => this.stripe.radar.earlyFraudWarnings.list(params),
2932
+ (pagination) => this.stripe.radar.earlyFraudWarnings.list({ ...params, ...pagination }),
2440
2933
  (items) => this.upsertEarlyFraudWarning(items, accountId, syncParams?.backfillRelatedEntities),
2441
2934
  accountId,
2442
2935
  "early_fraud_warnings",
@@ -2457,7 +2950,7 @@ var StripeSync = class {
2457
2950
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2458
2951
  }
2459
2952
  return this.fetchAndUpsert(
2460
- () => this.stripe.refunds.list(params),
2953
+ (pagination) => this.stripe.refunds.list({ ...params, ...pagination }),
2461
2954
  (items) => this.upsertRefunds(items, accountId, syncParams?.backfillRelatedEntities),
2462
2955
  accountId,
2463
2956
  "refunds",
@@ -2477,7 +2970,7 @@ var StripeSync = class {
2477
2970
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2478
2971
  }
2479
2972
  return this.fetchAndUpsert(
2480
- () => this.stripe.creditNotes.list(params),
2973
+ (pagination) => this.stripe.creditNotes.list({ ...params, ...pagination }),
2481
2974
  (creditNotes) => this.upsertCreditNotes(creditNotes, accountId),
2482
2975
  accountId,
2483
2976
  "credit_notes",
@@ -2539,7 +3032,7 @@ var StripeSync = class {
2539
3032
  this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
2540
3033
  }
2541
3034
  return this.fetchAndUpsert(
2542
- () => this.stripe.checkout.sessions.list(params),
3035
+ (pagination) => this.stripe.checkout.sessions.list({ ...params, ...pagination }),
2543
3036
  (items) => this.upsertCheckoutSessions(items, accountId, syncParams?.backfillRelatedEntities),
2544
3037
  accountId,
2545
3038
  "checkout_sessions",
@@ -2593,31 +3086,42 @@ var StripeSync = class {
2593
3086
  try {
2594
3087
  this.config.logger?.info("Fetching items to sync from Stripe");
2595
3088
  try {
2596
- for await (const item of fetch2()) {
2597
- currentBatch.push(item);
2598
- if (currentBatch.length >= CHECKPOINT_SIZE) {
2599
- this.config.logger?.info(`Upserting batch of ${currentBatch.length} items`);
2600
- await upsert(currentBatch, accountId);
2601
- totalSynced += currentBatch.length;
2602
- await this.postgresClient.incrementObjectProgress(
2603
- accountId,
2604
- runStartedAt,
2605
- resourceName,
2606
- currentBatch.length
2607
- );
2608
- const maxCreated = Math.max(
2609
- ...currentBatch.map((i) => i.created || 0)
2610
- );
2611
- if (maxCreated > 0) {
2612
- await this.postgresClient.updateObjectCursor(
3089
+ let hasMore = true;
3090
+ let startingAfter = void 0;
3091
+ while (hasMore) {
3092
+ const response = await fetch2(
3093
+ startingAfter ? { starting_after: startingAfter } : void 0
3094
+ );
3095
+ for (const item of response.data) {
3096
+ currentBatch.push(item);
3097
+ if (currentBatch.length >= CHECKPOINT_SIZE) {
3098
+ this.config.logger?.info(`Upserting batch of ${currentBatch.length} items`);
3099
+ await upsert(currentBatch, accountId);
3100
+ totalSynced += currentBatch.length;
3101
+ await this.postgresClient.incrementObjectProgress(
2613
3102
  accountId,
2614
3103
  runStartedAt,
2615
3104
  resourceName,
2616
- String(maxCreated)
3105
+ currentBatch.length
3106
+ );
3107
+ const maxCreated = Math.max(
3108
+ ...currentBatch.map((i) => i.created || 0)
2617
3109
  );
2618
- this.config.logger?.info(`Checkpoint: cursor updated to ${maxCreated}`);
3110
+ if (maxCreated > 0) {
3111
+ await this.postgresClient.updateObjectCursor(
3112
+ accountId,
3113
+ runStartedAt,
3114
+ resourceName,
3115
+ String(maxCreated)
3116
+ );
3117
+ this.config.logger?.info(`Checkpoint: cursor updated to ${maxCreated}`);
3118
+ }
3119
+ currentBatch = [];
2619
3120
  }
2620
- currentBatch = [];
3121
+ }
3122
+ hasMore = response.has_more;
3123
+ if (response.data.length > 0) {
3124
+ startingAfter = response.data[response.data.length - 1].id;
2621
3125
  }
2622
3126
  }
2623
3127
  if (currentBatch.length > 0) {
@@ -2985,10 +3489,18 @@ var StripeSync = class {
2985
3489
  async fillCheckoutSessionsLineItems(checkoutSessionIds, accountId, syncTimestamp) {
2986
3490
  for (const checkoutSessionId of checkoutSessionIds) {
2987
3491
  const lineItemResponses = [];
2988
- for await (const lineItem of this.stripe.checkout.sessions.listLineItems(checkoutSessionId, {
2989
- limit: 100
2990
- })) {
2991
- lineItemResponses.push(lineItem);
3492
+ let hasMore = true;
3493
+ let startingAfter = void 0;
3494
+ while (hasMore) {
3495
+ const response = await this.stripe.checkout.sessions.listLineItems(checkoutSessionId, {
3496
+ limit: 100,
3497
+ ...startingAfter ? { starting_after: startingAfter } : {}
3498
+ });
3499
+ lineItemResponses.push(...response.data);
3500
+ hasMore = response.has_more;
3501
+ if (response.data.length > 0) {
3502
+ startingAfter = response.data[response.data.length - 1].id;
3503
+ }
2992
3504
  }
2993
3505
  await this.upsertCheckoutSessionLineItems(
2994
3506
  lineItemResponses,
@@ -3302,14 +3814,25 @@ var StripeSync = class {
3302
3814
  };
3303
3815
  /**
3304
3816
  * Stripe only sends the first 10 entries by default, the option will actively fetch all entries.
3817
+ * Uses manual pagination - each fetch() gets automatic retry protection.
3305
3818
  */
3306
3819
  async expandEntity(entities, property, listFn) {
3307
3820
  if (!this.config.autoExpandLists) return;
3308
3821
  for (const entity of entities) {
3309
3822
  if (entity[property]?.has_more) {
3310
3823
  const allData = [];
3311
- for await (const fetchedEntity of listFn(entity.id)) {
3312
- allData.push(fetchedEntity);
3824
+ let hasMore = true;
3825
+ let startingAfter = void 0;
3826
+ while (hasMore) {
3827
+ const response = await listFn(
3828
+ entity.id,
3829
+ startingAfter ? { starting_after: startingAfter } : void 0
3830
+ );
3831
+ allData.push(...response.data);
3832
+ hasMore = response.has_more;
3833
+ if (response.data.length > 0) {
3834
+ startingAfter = response.data[response.data.length - 1].id;
3835
+ }
3313
3836
  }
3314
3837
  entity[property] = {
3315
3838
  ...entity[property],
@@ -3434,7 +3957,9 @@ async function runMigrations(config) {
3434
3957
  var import_ws = __toESM(require("ws"), 1);
3435
3958
  var CLI_VERSION = "1.33.0";
3436
3959
  var PONG_WAIT = 10 * 1e3;
3437
- var PING_PERIOD = PONG_WAIT * 2 / 10;
3960
+ var PING_PERIOD = PONG_WAIT * 9 / 10;
3961
+ var CONNECT_ATTEMPT_WAIT = 10 * 1e3;
3962
+ var DEFAULT_RECONNECT_INTERVAL = 60 * 1e3;
3438
3963
  function getClientUserAgent() {
3439
3964
  return JSON.stringify({
3440
3965
  name: "stripe-cli",
@@ -3463,129 +3988,215 @@ async function createCliSession(stripeApiKey) {
3463
3988
  }
3464
3989
  return await response.json();
3465
3990
  }
3991
+ function sleep3(ms) {
3992
+ return new Promise((resolve) => setTimeout(resolve, ms));
3993
+ }
3466
3994
  async function createStripeWebSocketClient(options) {
3467
3995
  const { stripeApiKey, onEvent, onReady, onError, onClose } = options;
3468
3996
  const session = await createCliSession(stripeApiKey);
3997
+ const reconnectInterval = session.reconnect_delay ? session.reconnect_delay * 1e3 : DEFAULT_RECONNECT_INTERVAL;
3469
3998
  let ws = null;
3470
3999
  let pingInterval = null;
4000
+ let reconnectTimer = null;
3471
4001
  let connected = false;
3472
- let shouldReconnect = true;
3473
- function connect() {
3474
- const wsUrl = `${session.websocket_url}?websocket_feature=${encodeURIComponent(session.websocket_authorized_feature)}`;
3475
- ws = new import_ws.default(wsUrl, {
3476
- headers: {
3477
- "Accept-Encoding": "identity",
3478
- "User-Agent": `Stripe/v1 stripe-cli/${CLI_VERSION}`,
3479
- "X-Stripe-Client-User-Agent": getClientUserAgent(),
3480
- "Websocket-Id": session.websocket_id
4002
+ let shouldRun = true;
4003
+ let lastPongReceived = Date.now();
4004
+ let notifyCloseResolve = null;
4005
+ let stopResolve = null;
4006
+ function cleanupConnection() {
4007
+ if (pingInterval) {
4008
+ clearInterval(pingInterval);
4009
+ pingInterval = null;
4010
+ }
4011
+ if (reconnectTimer) {
4012
+ clearTimeout(reconnectTimer);
4013
+ reconnectTimer = null;
4014
+ }
4015
+ if (ws) {
4016
+ ws.removeAllListeners();
4017
+ if (ws.readyState === import_ws.default.OPEN || ws.readyState === import_ws.default.CONNECTING) {
4018
+ ws.close(1e3, "Resetting connection");
3481
4019
  }
3482
- });
3483
- ws.on("pong", () => {
3484
- });
3485
- ws.on("open", () => {
3486
- connected = true;
3487
- pingInterval = setInterval(() => {
3488
- if (ws && ws.readyState === import_ws.default.OPEN) {
3489
- ws.ping();
4020
+ ws = null;
4021
+ }
4022
+ connected = false;
4023
+ }
4024
+ function setupWebSocket() {
4025
+ return new Promise((resolve, reject) => {
4026
+ lastPongReceived = Date.now();
4027
+ const wsUrl = `${session.websocket_url}?websocket_feature=${encodeURIComponent(session.websocket_authorized_feature)}`;
4028
+ ws = new import_ws.default(wsUrl, {
4029
+ headers: {
4030
+ "Accept-Encoding": "identity",
4031
+ "User-Agent": `Stripe/v1 stripe-cli/${CLI_VERSION}`,
4032
+ "X-Stripe-Client-User-Agent": getClientUserAgent(),
4033
+ "Websocket-Id": session.websocket_id
3490
4034
  }
3491
- }, PING_PERIOD);
3492
- if (onReady) {
3493
- onReady(session.secret);
3494
- }
3495
- });
3496
- ws.on("message", async (data) => {
3497
- try {
3498
- const message = JSON.parse(data.toString());
3499
- const ack = {
3500
- type: "event_ack",
3501
- event_id: message.webhook_id,
3502
- webhook_conversation_id: message.webhook_conversation_id,
3503
- webhook_id: message.webhook_id
3504
- };
3505
- if (ws && ws.readyState === import_ws.default.OPEN) {
3506
- ws.send(JSON.stringify(ack));
4035
+ });
4036
+ const connectionTimeout = setTimeout(() => {
4037
+ if (ws && ws.readyState === import_ws.default.CONNECTING) {
4038
+ ws.terminate();
4039
+ reject(new Error("WebSocket connection timeout"));
4040
+ }
4041
+ }, CONNECT_ATTEMPT_WAIT);
4042
+ ws.on("pong", () => {
4043
+ lastPongReceived = Date.now();
4044
+ });
4045
+ ws.on("open", () => {
4046
+ clearTimeout(connectionTimeout);
4047
+ connected = true;
4048
+ pingInterval = setInterval(() => {
4049
+ if (ws && ws.readyState === import_ws.default.OPEN) {
4050
+ const timeSinceLastPong = Date.now() - lastPongReceived;
4051
+ if (timeSinceLastPong > PONG_WAIT) {
4052
+ if (onError) {
4053
+ onError(new Error(`WebSocket stale: no pong in ${timeSinceLastPong}ms`));
4054
+ }
4055
+ if (notifyCloseResolve) {
4056
+ notifyCloseResolve();
4057
+ notifyCloseResolve = null;
4058
+ }
4059
+ ws.terminate();
4060
+ return;
4061
+ }
4062
+ ws.ping();
4063
+ }
4064
+ }, PING_PERIOD);
4065
+ if (onReady) {
4066
+ onReady(session.secret);
3507
4067
  }
3508
- let response;
4068
+ resolve();
4069
+ });
4070
+ ws.on("message", async (data) => {
3509
4071
  try {
3510
- const result = await onEvent(message);
3511
- response = {
3512
- type: "webhook_response",
3513
- webhook_id: message.webhook_id,
4072
+ const message = JSON.parse(data.toString());
4073
+ const ack = {
4074
+ type: "event_ack",
4075
+ event_id: message.webhook_id,
3514
4076
  webhook_conversation_id: message.webhook_conversation_id,
3515
- forward_url: "stripe-sync-engine",
3516
- status: result?.status ?? 200,
3517
- http_headers: {},
3518
- body: JSON.stringify({
3519
- event_type: result?.event_type,
3520
- event_id: result?.event_id,
3521
- database_url: result?.databaseUrl,
3522
- error: result?.error
3523
- }),
3524
- request_headers: message.http_headers,
3525
- request_body: message.event_payload,
3526
- notification_id: message.webhook_id
4077
+ webhook_id: message.webhook_id
3527
4078
  };
4079
+ if (ws && ws.readyState === import_ws.default.OPEN) {
4080
+ ws.send(JSON.stringify(ack));
4081
+ }
4082
+ let response;
4083
+ try {
4084
+ const result = await onEvent(message);
4085
+ response = {
4086
+ type: "webhook_response",
4087
+ webhook_id: message.webhook_id,
4088
+ webhook_conversation_id: message.webhook_conversation_id,
4089
+ forward_url: "stripe-sync-engine",
4090
+ status: result?.status ?? 200,
4091
+ http_headers: {},
4092
+ body: JSON.stringify({
4093
+ event_type: result?.event_type,
4094
+ event_id: result?.event_id,
4095
+ database_url: result?.databaseUrl,
4096
+ error: result?.error
4097
+ }),
4098
+ request_headers: message.http_headers,
4099
+ request_body: message.event_payload,
4100
+ notification_id: message.webhook_id
4101
+ };
4102
+ } catch (err) {
4103
+ const errorMessage = err instanceof Error ? err.message : String(err);
4104
+ response = {
4105
+ type: "webhook_response",
4106
+ webhook_id: message.webhook_id,
4107
+ webhook_conversation_id: message.webhook_conversation_id,
4108
+ forward_url: "stripe-sync-engine",
4109
+ status: 500,
4110
+ http_headers: {},
4111
+ body: JSON.stringify({ error: errorMessage }),
4112
+ request_headers: message.http_headers,
4113
+ request_body: message.event_payload,
4114
+ notification_id: message.webhook_id
4115
+ };
4116
+ if (onError) {
4117
+ onError(err instanceof Error ? err : new Error(errorMessage));
4118
+ }
4119
+ }
4120
+ if (ws && ws.readyState === import_ws.default.OPEN) {
4121
+ ws.send(JSON.stringify(response));
4122
+ }
3528
4123
  } catch (err) {
3529
- const errorMessage = err instanceof Error ? err.message : String(err);
3530
- response = {
3531
- type: "webhook_response",
3532
- webhook_id: message.webhook_id,
3533
- webhook_conversation_id: message.webhook_conversation_id,
3534
- forward_url: "stripe-sync-engine",
3535
- status: 500,
3536
- http_headers: {},
3537
- body: JSON.stringify({ error: errorMessage }),
3538
- request_headers: message.http_headers,
3539
- request_body: message.event_payload,
3540
- notification_id: message.webhook_id
3541
- };
3542
4124
  if (onError) {
3543
- onError(err instanceof Error ? err : new Error(errorMessage));
4125
+ onError(err instanceof Error ? err : new Error(String(err)));
3544
4126
  }
3545
4127
  }
3546
- if (ws && ws.readyState === import_ws.default.OPEN) {
3547
- ws.send(JSON.stringify(response));
3548
- }
3549
- } catch (err) {
4128
+ });
4129
+ ws.on("error", (error) => {
4130
+ clearTimeout(connectionTimeout);
3550
4131
  if (onError) {
3551
- onError(err instanceof Error ? err : new Error(String(err)));
4132
+ onError(error);
3552
4133
  }
3553
- }
3554
- });
3555
- ws.on("error", (error) => {
3556
- if (onError) {
3557
- onError(error);
3558
- }
4134
+ if (!connected) {
4135
+ reject(error);
4136
+ }
4137
+ });
4138
+ ws.on("close", (code, reason) => {
4139
+ clearTimeout(connectionTimeout);
4140
+ connected = false;
4141
+ if (pingInterval) {
4142
+ clearInterval(pingInterval);
4143
+ pingInterval = null;
4144
+ }
4145
+ if (onClose) {
4146
+ onClose(code, reason.toString());
4147
+ }
4148
+ if (notifyCloseResolve) {
4149
+ notifyCloseResolve();
4150
+ notifyCloseResolve = null;
4151
+ }
4152
+ });
3559
4153
  });
3560
- ws.on("close", (code, reason) => {
4154
+ }
4155
+ async function runLoop() {
4156
+ while (shouldRun) {
3561
4157
  connected = false;
3562
- if (pingInterval) {
3563
- clearInterval(pingInterval);
3564
- pingInterval = null;
3565
- }
3566
- if (onClose) {
3567
- onClose(code, reason.toString());
3568
- }
3569
- if (shouldReconnect) {
3570
- const delay = (session.reconnect_delay || 5) * 1e3;
3571
- setTimeout(() => {
3572
- connect();
3573
- }, delay);
4158
+ let connectError = null;
4159
+ do {
4160
+ try {
4161
+ await setupWebSocket();
4162
+ connectError = null;
4163
+ } catch (err) {
4164
+ connectError = err instanceof Error ? err : new Error(String(err));
4165
+ if (onError) {
4166
+ onError(connectError);
4167
+ }
4168
+ if (shouldRun) {
4169
+ await sleep3(CONNECT_ATTEMPT_WAIT);
4170
+ }
4171
+ }
4172
+ } while (connectError && shouldRun);
4173
+ if (!shouldRun) break;
4174
+ await new Promise((resolve) => {
4175
+ notifyCloseResolve = resolve;
4176
+ stopResolve = resolve;
4177
+ reconnectTimer = setTimeout(() => {
4178
+ cleanupConnection();
4179
+ resolve();
4180
+ }, reconnectInterval);
4181
+ });
4182
+ if (reconnectTimer) {
4183
+ clearTimeout(reconnectTimer);
4184
+ reconnectTimer = null;
3574
4185
  }
3575
- });
4186
+ notifyCloseResolve = null;
4187
+ stopResolve = null;
4188
+ }
4189
+ cleanupConnection();
3576
4190
  }
3577
- connect();
4191
+ runLoop();
3578
4192
  return {
3579
4193
  close: () => {
3580
- shouldReconnect = false;
3581
- if (pingInterval) {
3582
- clearInterval(pingInterval);
3583
- pingInterval = null;
3584
- }
3585
- if (ws) {
3586
- ws.close(1e3, "Connection Done");
3587
- ws = null;
4194
+ shouldRun = false;
4195
+ if (stopResolve) {
4196
+ stopResolve();
4197
+ stopResolve = null;
3588
4198
  }
4199
+ cleanupConnection();
3589
4200
  },
3590
4201
  isConnected: () => connected
3591
4202
  };
@@ -3631,13 +4242,13 @@ Creating ngrok tunnel for port ${port}...`));
3631
4242
  var import_supabase_management_js = require("supabase-management-js");
3632
4243
 
3633
4244
  // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
3634
- 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";
4245
+ 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";
3635
4246
 
3636
4247
  // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
3637
4248
  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";
3638
4249
 
3639
4250
  // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
3640
- var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron 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";
4251
+ 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";
3641
4252
 
3642
4253
  // src/supabase/edge-function-code.ts
3643
4254
  var setupFunctionCode = stripe_setup_default;
@@ -3735,8 +4346,8 @@ var SupabaseSetupClient = class {
3735
4346
  `Invalid interval: ${intervalSeconds}. Must be either 1-59 seconds or a multiple of 60 (e.g., 60, 120, 180).`
3736
4347
  );
3737
4348
  }
3738
- const serviceRoleKey = await this.getServiceRoleKey();
3739
- const escapedServiceRoleKey = serviceRoleKey.replace(/'/g, "''");
4349
+ const workerSecret = crypto.randomUUID();
4350
+ const escapedWorkerSecret = workerSecret.replace(/'/g, "''");
3740
4351
  const sql3 = `
3741
4352
  -- Enable extensions
3742
4353
  CREATE EXTENSION IF NOT EXISTS pg_cron;
@@ -3749,10 +4360,10 @@ var SupabaseSetupClient = class {
3749
4360
  SELECT 1 FROM pgmq.list_queues() WHERE queue_name = 'stripe_sync_work'
3750
4361
  );
3751
4362
 
3752
- -- Store service role key in vault for pg_cron to use
4363
+ -- Store unique worker secret in vault for pg_cron to use
3753
4364
  -- Delete existing secret if it exists, then create new one
3754
- DELETE FROM vault.secrets WHERE name = 'stripe_sync_service_role_key';
3755
- SELECT vault.create_secret('${escapedServiceRoleKey}', 'stripe_sync_service_role_key');
4365
+ DELETE FROM vault.secrets WHERE name = 'stripe_sync_worker_secret';
4366
+ SELECT vault.create_secret('${escapedWorkerSecret}', 'stripe_sync_worker_secret');
3756
4367
 
3757
4368
  -- Delete existing jobs if they exist
3758
4369
  SELECT cron.unschedule('stripe-sync-worker') WHERE EXISTS (
@@ -3771,7 +4382,7 @@ var SupabaseSetupClient = class {
3771
4382
  SELECT net.http_post(
3772
4383
  url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
3773
4384
  headers := jsonb_build_object(
3774
- 'Authorization', 'Bearer ' || (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'stripe_sync_service_role_key')
4385
+ 'Authorization', 'Bearer ' || (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'stripe_sync_worker_secret')
3775
4386
  )
3776
4387
  )
3777
4388
  $$
@@ -3785,17 +4396,6 @@ var SupabaseSetupClient = class {
3785
4396
  getWebhookUrl() {
3786
4397
  return `https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-webhook`;
3787
4398
  }
3788
- /**
3789
- * Get the service role key for this project (needed to invoke Edge Functions)
3790
- */
3791
- async getServiceRoleKey() {
3792
- const apiKeys = await this.api.getProjectApiKeys(this.projectRef);
3793
- const serviceRoleKey = apiKeys?.find((k) => k.name === "service_role");
3794
- if (!serviceRoleKey) {
3795
- throw new Error("Could not find service_role API key");
3796
- }
3797
- return serviceRoleKey.api_key;
3798
- }
3799
4399
  /**
3800
4400
  * Get the anon key for this project (needed for Realtime subscriptions)
3801
4401
  */
@@ -3816,12 +4416,12 @@ var SupabaseSetupClient = class {
3816
4416
  /**
3817
4417
  * Invoke an Edge Function
3818
4418
  */
3819
- async invokeFunction(name, serviceRoleKey) {
4419
+ async invokeFunction(name, bearerToken) {
3820
4420
  const url = `https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/${name}`;
3821
4421
  const response = await fetch(url, {
3822
4422
  method: "POST",
3823
4423
  headers: {
3824
- Authorization: `Bearer ${serviceRoleKey}`,
4424
+ Authorization: `Bearer ${bearerToken}`,
3825
4425
  "Content-Type": "application/json"
3826
4426
  }
3827
4427
  });
@@ -3928,18 +4528,13 @@ var SupabaseSetupClient = class {
3928
4528
  */
3929
4529
  async uninstall() {
3930
4530
  try {
3931
- const serviceRoleKey = await this.getServiceRoleKey();
3932
4531
  const url = `https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-setup`;
3933
4532
  const response = await fetch(url, {
3934
4533
  method: "DELETE",
3935
4534
  headers: {
3936
- Authorization: `Bearer ${serviceRoleKey}`,
4535
+ Authorization: `Bearer ${this.accessToken}`,
3937
4536
  "Content-Type": "application/json"
3938
- },
3939
- body: JSON.stringify({
3940
- supabase_access_token: this.accessToken,
3941
- supabase_project_ref: this.projectRef
3942
- })
4537
+ }
3943
4538
  });
3944
4539
  if (!response.ok) {
3945
4540
  const text = await response.text();
@@ -3980,12 +4575,11 @@ var SupabaseSetupClient = class {
3980
4575
  const versionedSetup = this.injectPackageVersion(setupFunctionCode, version);
3981
4576
  const versionedWebhook = this.injectPackageVersion(webhookFunctionCode, version);
3982
4577
  const versionedWorker = this.injectPackageVersion(workerFunctionCode, version);
3983
- await this.deployFunction("stripe-setup", versionedSetup, true);
4578
+ await this.deployFunction("stripe-setup", versionedSetup, false);
3984
4579
  await this.deployFunction("stripe-webhook", versionedWebhook, false);
3985
- await this.deployFunction("stripe-worker", versionedWorker, true);
4580
+ await this.deployFunction("stripe-worker", versionedWorker, false);
3986
4581
  await this.setSecrets([{ name: "STRIPE_SECRET_KEY", value: trimmedStripeKey }]);
3987
- const serviceRoleKey = await this.getServiceRoleKey();
3988
- const setupResult = await this.invokeFunction("stripe-setup", serviceRoleKey);
4582
+ const setupResult = await this.invokeFunction("stripe-setup", this.accessToken);
3989
4583
  if (!setupResult.success) {
3990
4584
  throw new Error(`Setup failed: ${setupResult.error}`);
3991
4585
  }
@@ -4048,7 +4642,9 @@ var VALID_SYNC_OBJECTS = [
4048
4642
  "credit_note",
4049
4643
  "early_fraud_warning",
4050
4644
  "refund",
4051
- "checkout_sessions"
4645
+ "checkout_sessions",
4646
+ "subscription_item_change_events_v2_beta",
4647
+ "exchange_rates_from_usd"
4052
4648
  ];
4053
4649
  async function backfillCommand(options, entityName) {
4054
4650
  let stripeSync = null;
@@ -4077,8 +4673,8 @@ async function backfillCommand(options, entityName) {
4077
4673
  if (!input || input.trim() === "") {
4078
4674
  return "Stripe API key is required";
4079
4675
  }
4080
- if (!input.startsWith("sk_")) {
4081
- return 'Stripe API key should start with "sk_"';
4676
+ if (!input.startsWith("sk_") && !input.startsWith("rk_")) {
4677
+ return 'Stripe API key should start with "sk_" or "rk_"';
4082
4678
  }
4083
4679
  return true;
4084
4680
  }
@@ -4135,6 +4731,7 @@ async function backfillCommand(options, entityName) {
4135
4731
  stripeSync = new StripeSync({
4136
4732
  databaseUrl: config.databaseUrl,
4137
4733
  stripeSecretKey: config.stripeApiKey,
4734
+ enableSigma: process.env.ENABLE_SIGMA === "true",
4138
4735
  stripeApiVersion: process.env.STRIPE_API_VERSION || "2020-08-27",
4139
4736
  autoExpandLists: process.env.AUTO_EXPAND_LISTS === "true",
4140
4737
  backfillRelatedEntities: process.env.BACKFILL_RELATED_ENTITIES !== "false",
@@ -4273,7 +4870,7 @@ Cleaning up... (signal: ${signal || "manual"})`));
4273
4870
  process.on("SIGTERM", () => cleanup("SIGTERM"));
4274
4871
  try {
4275
4872
  const config = await loadConfig(options);
4276
- const useWebSocketMode = !config.ngrokAuthToken;
4873
+ const useWebSocketMode = process.env.USE_WEBSOCKET === "true" || !config.ngrokAuthToken;
4277
4874
  const modeLabel = useWebSocketMode ? "WebSocket" : "Webhook (ngrok)";
4278
4875
  console.log(import_chalk3.default.blue(`
4279
4876
  Mode: ${modeLabel}`));
@@ -4298,6 +4895,7 @@ Mode: ${modeLabel}`));
4298
4895
  stripeSync = new StripeSync({
4299
4896
  databaseUrl: config.databaseUrl,
4300
4897
  stripeSecretKey: config.stripeApiKey,
4898
+ enableSigma: config.enableSigma,
4301
4899
  stripeApiVersion: process.env.STRIPE_API_VERSION || "2020-08-27",
4302
4900
  autoExpandLists: process.env.AUTO_EXPAND_LISTS === "true",
4303
4901
  backfillRelatedEntities: process.env.BACKFILL_RELATED_ENTITIES !== "false",
@@ -4445,7 +5043,8 @@ async function installCommand(options) {
4445
5043
  mask: "*",
4446
5044
  validate: (input) => {
4447
5045
  if (!input.trim()) return "Stripe key is required";
4448
- if (!input.startsWith("sk_")) return 'Stripe key should start with "sk_"';
5046
+ if (!input.startsWith("sk_") && !input.startsWith("rk_"))
5047
+ return 'Stripe key should start with "sk_" or "rk_"';
4449
5048
  return true;
4450
5049
  }
4451
5050
  });
@@ -4547,11 +5146,12 @@ program.command("migrate").description("Run database migrations only").option("-
4547
5146
  databaseUrl: options.databaseUrl
4548
5147
  });
4549
5148
  });
4550
- program.command("start").description("Start Stripe sync").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").option("--ngrok-token <token>", "ngrok auth token (or NGROK_AUTH_TOKEN env)").option("--database-url <url>", "Postgres DATABASE_URL (or DATABASE_URL env)").action(async (options) => {
5149
+ program.command("start").description("Start Stripe sync").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").option("--ngrok-token <token>", "ngrok auth token (or NGROK_AUTH_TOKEN env)").option("--database-url <url>", "Postgres DATABASE_URL (or DATABASE_URL env)").option("--sigma", "Sync Sigma data (requires Sigma access in Stripe API key)").action(async (options) => {
4551
5150
  await syncCommand({
4552
5151
  stripeKey: options.stripeKey,
4553
5152
  ngrokToken: options.ngrokToken,
4554
- databaseUrl: options.databaseUrl
5153
+ databaseUrl: options.databaseUrl,
5154
+ enableSigma: options.sigma
4555
5155
  });
4556
5156
  });
4557
5157
  program.command("backfill <entityName>").description("Backfill a specific entity type from Stripe (e.g., customer, invoice, product)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").option("--database-url <url>", "Postgres DATABASE_URL (or DATABASE_URL env)").action(async (entityName, options) => {