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