stripe-experiment-sync 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-YXQZXR7S.js → chunk-CKWVW2JK.js} +1 -1
- package/dist/{chunk-7JWRDXNB.js → chunk-J7BH3XD6.js} +195 -23
- package/dist/{chunk-SX3HLE4H.js → chunk-X2OQQCC2.js} +28 -25
- package/dist/{chunk-3P5TZKWU.js → chunk-YH6KRZDQ.js} +102 -63
- package/dist/cli/index.cjs +322 -106
- package/dist/cli/index.js +10 -6
- package/dist/cli/lib.cjs +316 -104
- package/dist/cli/lib.d.cts +5 -6
- package/dist/cli/lib.d.ts +5 -6
- package/dist/cli/lib.js +4 -4
- package/dist/index.cjs +196 -23
- package/dist/index.d.cts +53 -1
- package/dist/index.d.ts +53 -1
- package/dist/index.js +4 -2
- package/dist/migrations/0058_improve_sync_runs_status.sql +36 -0
- package/dist/supabase/index.cjs +28 -25
- package/dist/supabase/index.d.cts +6 -1
- package/dist/supabase/index.d.ts +6 -1
- package/dist/supabase/index.js +2 -2
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
package_default
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-CKWVW2JK.js";
|
|
4
4
|
|
|
5
5
|
// src/stripeSync.ts
|
|
6
6
|
import Stripe2 from "stripe";
|
|
@@ -1607,19 +1607,8 @@ var StripeSync = class {
|
|
|
1607
1607
|
if (params?.runStartedAt) {
|
|
1608
1608
|
runStartedAt = params.runStartedAt;
|
|
1609
1609
|
} else {
|
|
1610
|
-
const runKey = await this.
|
|
1611
|
-
|
|
1612
|
-
params?.triggeredBy ?? "processNext"
|
|
1613
|
-
);
|
|
1614
|
-
if (!runKey) {
|
|
1615
|
-
const activeRun = await this.postgresClient.getActiveSyncRun(accountId);
|
|
1616
|
-
if (!activeRun) {
|
|
1617
|
-
throw new Error("Failed to get or create sync run");
|
|
1618
|
-
}
|
|
1619
|
-
runStartedAt = activeRun.runStartedAt;
|
|
1620
|
-
} else {
|
|
1621
|
-
runStartedAt = runKey.runStartedAt;
|
|
1622
|
-
}
|
|
1610
|
+
const { runKey } = await this.joinOrCreateSyncRun(params?.triggeredBy ?? "processNext");
|
|
1611
|
+
runStartedAt = runKey.runStartedAt;
|
|
1623
1612
|
}
|
|
1624
1613
|
await this.postgresClient.createObjectRuns(accountId, runStartedAt, [resourceName]);
|
|
1625
1614
|
const objRun = await this.postgresClient.getObjectRun(accountId, runStartedAt, resourceName);
|
|
@@ -1782,18 +1771,39 @@ var StripeSync = class {
|
|
|
1782
1771
|
}
|
|
1783
1772
|
return { synced: totalSynced };
|
|
1784
1773
|
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1774
|
+
/**
|
|
1775
|
+
* Join existing sync run or create a new one.
|
|
1776
|
+
* Returns sync run key and list of supported objects to sync.
|
|
1777
|
+
*
|
|
1778
|
+
* Cooperative behavior: If a sync run already exists, joins it instead of failing.
|
|
1779
|
+
* This is used by workers and background processes that should cooperate.
|
|
1780
|
+
*
|
|
1781
|
+
* @param triggeredBy - What triggered this sync (for observability)
|
|
1782
|
+
* @returns Run key and list of objects to sync
|
|
1783
|
+
*/
|
|
1784
|
+
async joinOrCreateSyncRun(triggeredBy = "worker") {
|
|
1787
1785
|
await this.getCurrentAccount();
|
|
1788
1786
|
const accountId = await this.getAccountId();
|
|
1789
|
-
const
|
|
1790
|
-
if (!
|
|
1787
|
+
const result = await this.postgresClient.getOrCreateSyncRun(accountId, triggeredBy);
|
|
1788
|
+
if (!result) {
|
|
1791
1789
|
const activeRun = await this.postgresClient.getActiveSyncRun(accountId);
|
|
1792
1790
|
if (!activeRun) {
|
|
1793
1791
|
throw new Error("Failed to get or create sync run");
|
|
1794
1792
|
}
|
|
1795
|
-
return
|
|
1793
|
+
return {
|
|
1794
|
+
runKey: { accountId: activeRun.accountId, runStartedAt: activeRun.runStartedAt },
|
|
1795
|
+
objects: this.getSupportedSyncObjects()
|
|
1796
|
+
};
|
|
1796
1797
|
}
|
|
1798
|
+
const { accountId: runAccountId, runStartedAt } = result;
|
|
1799
|
+
return {
|
|
1800
|
+
runKey: { accountId: runAccountId, runStartedAt },
|
|
1801
|
+
objects: this.getSupportedSyncObjects()
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
async processUntilDone(params) {
|
|
1805
|
+
const { object } = params ?? { object: "all" };
|
|
1806
|
+
const { runKey } = await this.joinOrCreateSyncRun("processUntilDone");
|
|
1797
1807
|
return this.processUntilDoneWithRun(runKey.runStartedAt, object, params);
|
|
1798
1808
|
}
|
|
1799
1809
|
/**
|
|
@@ -2408,14 +2418,14 @@ var StripeSync = class {
|
|
|
2408
2418
|
throw error;
|
|
2409
2419
|
}
|
|
2410
2420
|
}
|
|
2411
|
-
async fetchAndUpsert(
|
|
2421
|
+
async fetchAndUpsert(fetch2, upsert, accountId, resourceName, runStartedAt) {
|
|
2412
2422
|
const CHECKPOINT_SIZE = 100;
|
|
2413
2423
|
let totalSynced = 0;
|
|
2414
2424
|
let currentBatch = [];
|
|
2415
2425
|
try {
|
|
2416
2426
|
this.config.logger?.info("Fetching items to sync from Stripe");
|
|
2417
2427
|
try {
|
|
2418
|
-
for await (const item of
|
|
2428
|
+
for await (const item of fetch2()) {
|
|
2419
2429
|
currentBatch.push(item);
|
|
2420
2430
|
if (currentBatch.length >= CHECKPOINT_SIZE) {
|
|
2421
2431
|
this.config.logger?.info(`Upserting batch of ${currentBatch.length} items`);
|
|
@@ -3141,11 +3151,11 @@ var StripeSync = class {
|
|
|
3141
3151
|
}
|
|
3142
3152
|
}
|
|
3143
3153
|
}
|
|
3144
|
-
async fetchMissingEntities(ids,
|
|
3154
|
+
async fetchMissingEntities(ids, fetch2) {
|
|
3145
3155
|
if (!ids.length) return [];
|
|
3146
3156
|
const entities = [];
|
|
3147
3157
|
for (const id of ids) {
|
|
3148
|
-
const entity = await
|
|
3158
|
+
const entity = await fetch2(id);
|
|
3149
3159
|
entities.push(entity);
|
|
3150
3160
|
}
|
|
3151
3161
|
return entities;
|
|
@@ -3252,6 +3262,167 @@ async function runMigrations(config) {
|
|
|
3252
3262
|
}
|
|
3253
3263
|
}
|
|
3254
3264
|
|
|
3265
|
+
// src/websocket-client.ts
|
|
3266
|
+
import WebSocket from "ws";
|
|
3267
|
+
var CLI_VERSION = "1.33.0";
|
|
3268
|
+
var PONG_WAIT = 10 * 1e3;
|
|
3269
|
+
var PING_PERIOD = PONG_WAIT * 2 / 10;
|
|
3270
|
+
function getClientUserAgent() {
|
|
3271
|
+
return JSON.stringify({
|
|
3272
|
+
name: "stripe-cli",
|
|
3273
|
+
version: CLI_VERSION,
|
|
3274
|
+
publisher: "stripe",
|
|
3275
|
+
os: process.platform
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
async function createCliSession(stripeApiKey) {
|
|
3279
|
+
const params = new URLSearchParams();
|
|
3280
|
+
params.append("device_name", "stripe-sync-engine");
|
|
3281
|
+
params.append("websocket_features[]", "webhooks");
|
|
3282
|
+
const response = await fetch("https://api.stripe.com/v1/stripecli/sessions", {
|
|
3283
|
+
method: "POST",
|
|
3284
|
+
headers: {
|
|
3285
|
+
Authorization: `Bearer ${stripeApiKey}`,
|
|
3286
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3287
|
+
"User-Agent": `Stripe/v1 stripe-cli/${CLI_VERSION}`,
|
|
3288
|
+
"X-Stripe-Client-User-Agent": getClientUserAgent()
|
|
3289
|
+
},
|
|
3290
|
+
body: params.toString()
|
|
3291
|
+
});
|
|
3292
|
+
if (!response.ok) {
|
|
3293
|
+
const error = await response.text();
|
|
3294
|
+
throw new Error(`Failed to create CLI session: ${error}`);
|
|
3295
|
+
}
|
|
3296
|
+
return await response.json();
|
|
3297
|
+
}
|
|
3298
|
+
async function createStripeWebSocketClient(options) {
|
|
3299
|
+
const { stripeApiKey, onEvent, onReady, onError, onClose } = options;
|
|
3300
|
+
const session = await createCliSession(stripeApiKey);
|
|
3301
|
+
let ws = null;
|
|
3302
|
+
let pingInterval = null;
|
|
3303
|
+
let connected = false;
|
|
3304
|
+
let shouldReconnect = true;
|
|
3305
|
+
function connect() {
|
|
3306
|
+
const wsUrl = `${session.websocket_url}?websocket_feature=${encodeURIComponent(session.websocket_authorized_feature)}`;
|
|
3307
|
+
ws = new WebSocket(wsUrl, {
|
|
3308
|
+
headers: {
|
|
3309
|
+
"Accept-Encoding": "identity",
|
|
3310
|
+
"User-Agent": `Stripe/v1 stripe-cli/${CLI_VERSION}`,
|
|
3311
|
+
"X-Stripe-Client-User-Agent": getClientUserAgent(),
|
|
3312
|
+
"Websocket-Id": session.websocket_id
|
|
3313
|
+
}
|
|
3314
|
+
});
|
|
3315
|
+
ws.on("pong", () => {
|
|
3316
|
+
});
|
|
3317
|
+
ws.on("open", () => {
|
|
3318
|
+
connected = true;
|
|
3319
|
+
pingInterval = setInterval(() => {
|
|
3320
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
3321
|
+
ws.ping();
|
|
3322
|
+
}
|
|
3323
|
+
}, PING_PERIOD);
|
|
3324
|
+
if (onReady) {
|
|
3325
|
+
onReady(session.secret);
|
|
3326
|
+
}
|
|
3327
|
+
});
|
|
3328
|
+
ws.on("message", async (data) => {
|
|
3329
|
+
try {
|
|
3330
|
+
const message = JSON.parse(data.toString());
|
|
3331
|
+
const ack = {
|
|
3332
|
+
type: "event_ack",
|
|
3333
|
+
event_id: message.webhook_id,
|
|
3334
|
+
webhook_conversation_id: message.webhook_conversation_id,
|
|
3335
|
+
webhook_id: message.webhook_id
|
|
3336
|
+
};
|
|
3337
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
3338
|
+
ws.send(JSON.stringify(ack));
|
|
3339
|
+
}
|
|
3340
|
+
let response;
|
|
3341
|
+
try {
|
|
3342
|
+
const result = await onEvent(message);
|
|
3343
|
+
response = {
|
|
3344
|
+
type: "webhook_response",
|
|
3345
|
+
webhook_id: message.webhook_id,
|
|
3346
|
+
webhook_conversation_id: message.webhook_conversation_id,
|
|
3347
|
+
forward_url: "stripe-sync-engine",
|
|
3348
|
+
status: result?.status ?? 200,
|
|
3349
|
+
http_headers: {},
|
|
3350
|
+
body: JSON.stringify({
|
|
3351
|
+
event_type: result?.event_type,
|
|
3352
|
+
event_id: result?.event_id,
|
|
3353
|
+
database_url: result?.databaseUrl,
|
|
3354
|
+
error: result?.error
|
|
3355
|
+
}),
|
|
3356
|
+
request_headers: message.http_headers,
|
|
3357
|
+
request_body: message.event_payload,
|
|
3358
|
+
notification_id: message.webhook_id
|
|
3359
|
+
};
|
|
3360
|
+
} catch (err) {
|
|
3361
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
3362
|
+
response = {
|
|
3363
|
+
type: "webhook_response",
|
|
3364
|
+
webhook_id: message.webhook_id,
|
|
3365
|
+
webhook_conversation_id: message.webhook_conversation_id,
|
|
3366
|
+
forward_url: "stripe-sync-engine",
|
|
3367
|
+
status: 500,
|
|
3368
|
+
http_headers: {},
|
|
3369
|
+
body: JSON.stringify({ error: errorMessage }),
|
|
3370
|
+
request_headers: message.http_headers,
|
|
3371
|
+
request_body: message.event_payload,
|
|
3372
|
+
notification_id: message.webhook_id
|
|
3373
|
+
};
|
|
3374
|
+
if (onError) {
|
|
3375
|
+
onError(err instanceof Error ? err : new Error(errorMessage));
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
3379
|
+
ws.send(JSON.stringify(response));
|
|
3380
|
+
}
|
|
3381
|
+
} catch (err) {
|
|
3382
|
+
if (onError) {
|
|
3383
|
+
onError(err instanceof Error ? err : new Error(String(err)));
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
});
|
|
3387
|
+
ws.on("error", (error) => {
|
|
3388
|
+
if (onError) {
|
|
3389
|
+
onError(error);
|
|
3390
|
+
}
|
|
3391
|
+
});
|
|
3392
|
+
ws.on("close", (code, reason) => {
|
|
3393
|
+
connected = false;
|
|
3394
|
+
if (pingInterval) {
|
|
3395
|
+
clearInterval(pingInterval);
|
|
3396
|
+
pingInterval = null;
|
|
3397
|
+
}
|
|
3398
|
+
if (onClose) {
|
|
3399
|
+
onClose(code, reason.toString());
|
|
3400
|
+
}
|
|
3401
|
+
if (shouldReconnect) {
|
|
3402
|
+
const delay = (session.reconnect_delay || 5) * 1e3;
|
|
3403
|
+
setTimeout(() => {
|
|
3404
|
+
connect();
|
|
3405
|
+
}, delay);
|
|
3406
|
+
}
|
|
3407
|
+
});
|
|
3408
|
+
}
|
|
3409
|
+
connect();
|
|
3410
|
+
return {
|
|
3411
|
+
close: () => {
|
|
3412
|
+
shouldReconnect = false;
|
|
3413
|
+
if (pingInterval) {
|
|
3414
|
+
clearInterval(pingInterval);
|
|
3415
|
+
pingInterval = null;
|
|
3416
|
+
}
|
|
3417
|
+
if (ws) {
|
|
3418
|
+
ws.close(1e3, "Connection Done");
|
|
3419
|
+
ws = null;
|
|
3420
|
+
}
|
|
3421
|
+
},
|
|
3422
|
+
isConnected: () => connected
|
|
3423
|
+
};
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3255
3426
|
// src/index.ts
|
|
3256
3427
|
var VERSION = package_default.version;
|
|
3257
3428
|
|
|
@@ -3260,5 +3431,6 @@ export {
|
|
|
3260
3431
|
hashApiKey,
|
|
3261
3432
|
StripeSync,
|
|
3262
3433
|
runMigrations,
|
|
3434
|
+
createStripeWebSocketClient,
|
|
3263
3435
|
VERSION
|
|
3264
3436
|
};
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
package_default
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-CKWVW2JK.js";
|
|
4
4
|
|
|
5
5
|
// src/supabase/supabase.ts
|
|
6
6
|
import { SupabaseManagementAPI } from "supabase-management-js";
|
|
7
7
|
|
|
8
|
-
// raw-ts:/
|
|
8
|
+
// raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
|
|
9
9
|
var stripe_setup_default = "import { StripeSync, runMigrations } 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 authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\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";
|
|
10
10
|
|
|
11
|
-
// raw-ts:/
|
|
11
|
+
// raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
|
|
12
12
|
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";
|
|
13
13
|
|
|
14
|
-
// raw-ts:/
|
|
15
|
-
var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron every 10 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 const objects = stripeSync.
|
|
14
|
+
// raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
|
|
15
|
+
var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron every 10 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";
|
|
16
16
|
|
|
17
17
|
// src/supabase/edge-function-code.ts
|
|
18
18
|
var setupFunctionCode = stripe_setup_default;
|
|
@@ -324,40 +324,43 @@ var SupabaseSetupClient = class {
|
|
|
324
324
|
throw new Error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
|
-
|
|
327
|
+
/**
|
|
328
|
+
* Inject package version into Edge Function code
|
|
329
|
+
*/
|
|
330
|
+
injectPackageVersion(code, version) {
|
|
331
|
+
if (version === "latest") {
|
|
332
|
+
return code;
|
|
333
|
+
}
|
|
334
|
+
return code.replace(
|
|
335
|
+
/from ['"]npm:stripe-experiment-sync['"]/g,
|
|
336
|
+
`from 'npm:stripe-experiment-sync@${version}'`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
async install(stripeKey, packageVersion) {
|
|
328
340
|
const trimmedStripeKey = stripeKey.trim();
|
|
329
341
|
if (!trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
|
|
330
342
|
throw new Error('Stripe key should start with "sk_" or "rk_"');
|
|
331
343
|
}
|
|
344
|
+
const version = packageVersion || "latest";
|
|
332
345
|
try {
|
|
333
346
|
await this.validateProject();
|
|
334
347
|
await this.runSQL(`CREATE SCHEMA IF NOT EXISTS stripe`);
|
|
335
348
|
await this.updateInstallationComment(
|
|
336
349
|
`${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_STARTED_SUFFIX}`
|
|
337
350
|
);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
351
|
+
const versionedSetup = this.injectPackageVersion(setupFunctionCode, version);
|
|
352
|
+
const versionedWebhook = this.injectPackageVersion(webhookFunctionCode, version);
|
|
353
|
+
const versionedWorker = this.injectPackageVersion(workerFunctionCode, version);
|
|
354
|
+
await this.deployFunction("stripe-setup", versionedSetup);
|
|
355
|
+
await this.deployFunction("stripe-webhook", versionedWebhook);
|
|
356
|
+
await this.deployFunction("stripe-worker", versionedWorker);
|
|
341
357
|
await this.setSecrets([{ name: "STRIPE_SECRET_KEY", value: trimmedStripeKey }]);
|
|
342
358
|
const serviceRoleKey = await this.getServiceRoleKey();
|
|
343
359
|
const setupResult = await this.invokeFunction("stripe-setup", serviceRoleKey);
|
|
344
360
|
if (!setupResult.success) {
|
|
345
361
|
throw new Error(`Setup failed: ${setupResult.error}`);
|
|
346
362
|
}
|
|
347
|
-
|
|
348
|
-
try {
|
|
349
|
-
await this.setupPgCronJob();
|
|
350
|
-
pgCronEnabled = true;
|
|
351
|
-
} catch {
|
|
352
|
-
console.warn("pg_cron setup failed - falling back to manual worker invocation");
|
|
353
|
-
}
|
|
354
|
-
if (!pgCronEnabled) {
|
|
355
|
-
try {
|
|
356
|
-
await this.invokeFunction("stripe-worker", serviceRoleKey);
|
|
357
|
-
} catch (err) {
|
|
358
|
-
console.warn("Failed to trigger initial worker invocation:", err);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
363
|
+
await this.setupPgCronJob();
|
|
361
364
|
await this.updateInstallationComment(
|
|
362
365
|
`${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_INSTALLED_SUFFIX}`
|
|
363
366
|
);
|
|
@@ -370,14 +373,14 @@ var SupabaseSetupClient = class {
|
|
|
370
373
|
}
|
|
371
374
|
};
|
|
372
375
|
async function install(params) {
|
|
373
|
-
const { supabaseAccessToken, supabaseProjectRef, stripeKey } = params;
|
|
376
|
+
const { supabaseAccessToken, supabaseProjectRef, stripeKey, packageVersion } = params;
|
|
374
377
|
const client = new SupabaseSetupClient({
|
|
375
378
|
accessToken: supabaseAccessToken,
|
|
376
379
|
projectRef: supabaseProjectRef,
|
|
377
380
|
projectBaseUrl: params.baseProjectUrl,
|
|
378
381
|
managementApiBaseUrl: params.baseManagementApiUrl
|
|
379
382
|
});
|
|
380
|
-
await client.install(stripeKey);
|
|
383
|
+
await client.install(stripeKey, packageVersion);
|
|
381
384
|
}
|
|
382
385
|
async function uninstall(params) {
|
|
383
386
|
const { supabaseAccessToken, supabaseProjectRef, stripeKey } = params;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
StripeSync,
|
|
3
|
+
createStripeWebSocketClient,
|
|
3
4
|
runMigrations
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-J7BH3XD6.js";
|
|
5
6
|
import {
|
|
6
7
|
install,
|
|
7
8
|
uninstall
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-X2OQQCC2.js";
|
|
9
10
|
|
|
10
11
|
// src/cli/config.ts
|
|
11
12
|
import dotenv from "dotenv";
|
|
@@ -35,20 +36,6 @@ async function loadConfig(options) {
|
|
|
35
36
|
}
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
|
-
if (!config.ngrokAuthToken) {
|
|
39
|
-
questions.push({
|
|
40
|
-
type: "password",
|
|
41
|
-
name: "ngrokAuthToken",
|
|
42
|
-
message: "Enter your ngrok auth token:",
|
|
43
|
-
mask: "*",
|
|
44
|
-
validate: (input) => {
|
|
45
|
-
if (!input || input.trim() === "") {
|
|
46
|
-
return "Ngrok auth token is required";
|
|
47
|
-
}
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
39
|
if (!config.databaseUrl) {
|
|
53
40
|
questions.push({
|
|
54
41
|
type: "password",
|
|
@@ -299,10 +286,19 @@ async function syncCommand(options) {
|
|
|
299
286
|
let tunnel = null;
|
|
300
287
|
let server = null;
|
|
301
288
|
let webhookId = null;
|
|
289
|
+
let wsClient = null;
|
|
302
290
|
const cleanup = async (signal) => {
|
|
303
291
|
console.log(chalk3.blue(`
|
|
304
292
|
|
|
305
293
|
Cleaning up... (signal: ${signal || "manual"})`));
|
|
294
|
+
if (wsClient) {
|
|
295
|
+
try {
|
|
296
|
+
wsClient.close();
|
|
297
|
+
console.log(chalk3.green("\u2713 WebSocket closed"));
|
|
298
|
+
} catch {
|
|
299
|
+
console.log(chalk3.yellow("\u26A0 Could not close WebSocket"));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
306
302
|
const keepWebhooksOnShutdown = process.env.KEEP_WEBHOOKS_ON_SHUTDOWN === "true";
|
|
307
303
|
if (webhookId && stripeSync && !keepWebhooksOnShutdown) {
|
|
308
304
|
try {
|
|
@@ -346,7 +342,12 @@ Cleaning up... (signal: ${signal || "manual"})`));
|
|
|
346
342
|
process.on("SIGTERM", () => cleanup("SIGTERM"));
|
|
347
343
|
try {
|
|
348
344
|
const config = await loadConfig(options);
|
|
349
|
-
|
|
345
|
+
const useWebSocketMode = !config.ngrokAuthToken;
|
|
346
|
+
const modeLabel = useWebSocketMode ? "WebSocket" : "Webhook (ngrok)";
|
|
347
|
+
console.log(chalk3.blue(`
|
|
348
|
+
Mode: ${modeLabel}`));
|
|
349
|
+
const maskedDbUrl = config.databaseUrl.replace(/:[^:@]+@/, ":****@");
|
|
350
|
+
console.log(chalk3.gray(`Database: ${maskedDbUrl}`));
|
|
350
351
|
try {
|
|
351
352
|
await runMigrations({
|
|
352
353
|
databaseUrl: config.databaseUrl
|
|
@@ -371,53 +372,90 @@ Cleaning up... (signal: ${signal || "manual"})`));
|
|
|
371
372
|
backfillRelatedEntities: process.env.BACKFILL_RELATED_ENTITIES !== "false",
|
|
372
373
|
poolConfig
|
|
373
374
|
});
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
375
|
+
const databaseUrlWithoutPassword = config.databaseUrl.replace(/:[^:@]+@/, ":****@");
|
|
376
|
+
if (useWebSocketMode) {
|
|
377
|
+
console.log(chalk3.blue("\nConnecting to Stripe WebSocket..."));
|
|
378
|
+
wsClient = await createStripeWebSocketClient({
|
|
379
|
+
stripeApiKey: config.stripeApiKey,
|
|
380
|
+
onEvent: async (event) => {
|
|
381
|
+
try {
|
|
382
|
+
const payload = JSON.parse(event.event_payload);
|
|
383
|
+
console.log(chalk3.cyan(`\u2190 ${payload.type}`) + chalk3.gray(` (${payload.id})`));
|
|
384
|
+
if (stripeSync) {
|
|
385
|
+
await stripeSync.processEvent(payload);
|
|
386
|
+
return {
|
|
387
|
+
status: 200,
|
|
388
|
+
event_type: payload.type,
|
|
389
|
+
event_id: payload.id,
|
|
390
|
+
databaseUrl: databaseUrlWithoutPassword
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
} catch (err) {
|
|
394
|
+
console.error(chalk3.red("Error processing event:"), err);
|
|
395
|
+
return {
|
|
396
|
+
status: 500,
|
|
397
|
+
databaseUrl: databaseUrlWithoutPassword,
|
|
398
|
+
error: err instanceof Error ? err.message : String(err)
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
onReady: (secret) => {
|
|
403
|
+
console.log(chalk3.green("\u2713 Connected to Stripe WebSocket"));
|
|
404
|
+
const maskedSecret = secret.length > 14 ? `${secret.slice(0, 10)}...${secret.slice(-4)}` : "****";
|
|
405
|
+
console.log(chalk3.gray(` Webhook secret: ${maskedSecret}`));
|
|
406
|
+
},
|
|
407
|
+
onError: (error) => {
|
|
408
|
+
console.error(chalk3.red("WebSocket error:"), error.message);
|
|
409
|
+
},
|
|
410
|
+
onClose: (code, reason) => {
|
|
411
|
+
console.log(chalk3.yellow(`WebSocket closed: ${code} - ${reason}`));
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
const port = 3e3;
|
|
416
|
+
tunnel = await createTunnel(port, config.ngrokAuthToken);
|
|
417
|
+
const webhookPath = process.env.WEBHOOK_PATH || "/stripe-webhooks";
|
|
418
|
+
console.log(chalk3.blue("\nCreating Stripe webhook endpoint..."));
|
|
419
|
+
const webhook = await stripeSync.findOrCreateManagedWebhook(`${tunnel.url}${webhookPath}`);
|
|
420
|
+
webhookId = webhook.id;
|
|
421
|
+
const eventCount = webhook.enabled_events?.length || 0;
|
|
422
|
+
console.log(chalk3.green(`\u2713 Webhook created: ${webhook.id}`));
|
|
423
|
+
console.log(chalk3.cyan(` URL: ${webhook.url}`));
|
|
424
|
+
console.log(chalk3.cyan(` Events: ${eventCount} supported events`));
|
|
425
|
+
const app = express();
|
|
426
|
+
const webhookRoute = webhookPath;
|
|
427
|
+
app.use(webhookRoute, express.raw({ type: "application/json" }));
|
|
428
|
+
app.post(webhookRoute, async (req, res) => {
|
|
429
|
+
const sig = req.headers["stripe-signature"];
|
|
430
|
+
if (!sig || typeof sig !== "string") {
|
|
431
|
+
console.error("[Webhook] Missing stripe-signature header");
|
|
432
|
+
return res.status(400).send({ error: "Missing stripe-signature header" });
|
|
433
|
+
}
|
|
434
|
+
const rawBody = req.body;
|
|
435
|
+
if (!rawBody || !Buffer.isBuffer(rawBody)) {
|
|
436
|
+
console.error("[Webhook] Body is not a Buffer!");
|
|
437
|
+
return res.status(400).send({ error: "Missing raw body for signature verification" });
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
await stripeSync.processWebhook(rawBody, sig);
|
|
441
|
+
return res.status(200).send({ received: true });
|
|
442
|
+
} catch (error) {
|
|
443
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
444
|
+
console.error("[Webhook] Processing error:", errorMessage);
|
|
445
|
+
return res.status(400).send({ error: errorMessage });
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
app.use(express.json());
|
|
449
|
+
app.use(express.urlencoded({ extended: false }));
|
|
450
|
+
app.get("/health", async (req, res) => res.status(200).json({ status: "ok" }));
|
|
451
|
+
console.log(chalk3.blue(`
|
|
413
452
|
Starting server on port ${port}...`));
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
453
|
+
await new Promise((resolve, reject) => {
|
|
454
|
+
server = app.listen(port, "0.0.0.0", () => resolve());
|
|
455
|
+
server.on("error", reject);
|
|
417
456
|
});
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
console.log(chalk3.green(`\u2713 Server started on port ${port}`));
|
|
457
|
+
console.log(chalk3.green(`\u2713 Server started on port ${port}`));
|
|
458
|
+
}
|
|
421
459
|
if (process.env.SKIP_BACKFILL !== "true") {
|
|
422
460
|
console.log(chalk3.blue("\nStarting initial sync of all Stripe data..."));
|
|
423
461
|
const syncResult = await stripeSync.processUntilDone();
|
|
@@ -494,7 +532,8 @@ async function installCommand(options) {
|
|
|
494
532
|
await install({
|
|
495
533
|
supabaseAccessToken: accessToken,
|
|
496
534
|
supabaseProjectRef: projectRef,
|
|
497
|
-
stripeKey
|
|
535
|
+
stripeKey,
|
|
536
|
+
packageVersion: options.packageVersion
|
|
498
537
|
});
|
|
499
538
|
console.log(chalk3.cyan("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
|
|
500
539
|
console.log(chalk3.cyan.bold(" Installation Complete!"));
|