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