@paymentsdb/sync-engine 0.0.2 → 0.0.4
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-VPHVPIUM.js → chunk-3X3TM6V4.js} +2 -2
- package/dist/{chunk-P3RP5RLG.js → chunk-FII5OTPO.js} +9 -3
- package/dist/{chunk-6ZCWQDN5.js → chunk-UD6RQUDV.js} +24 -13
- package/dist/chunk-WQOTGHLT.js +406 -0
- package/dist/cli/index.cjs +42 -25
- package/dist/cli/index.js +4 -4
- package/dist/cli/lib.cjs +42 -25
- package/dist/cli/lib.js +4 -4
- package/dist/index.cjs +34 -17
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -2
- package/dist/supabase/index.cjs +17 -11
- package/dist/supabase/index.js +2 -2
- package/package.json +17 -12
- package/dist/chunk-VFG6U6QD.js +0 -406
package/dist/cli/index.cjs
CHANGED
|
@@ -23,8 +23,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
mod
|
|
24
24
|
));
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.src
|
|
26
|
+
// node_modules/.pnpm/tsup@8.5.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
|
|
27
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
28
28
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
29
29
|
|
|
30
30
|
// src/cli/index.ts
|
|
@@ -33,12 +33,14 @@ var import_commander = require("commander");
|
|
|
33
33
|
// package.json
|
|
34
34
|
var package_default = {
|
|
35
35
|
name: "@paymentsdb/sync-engine",
|
|
36
|
-
version: "0.0.
|
|
36
|
+
version: "0.0.4",
|
|
37
37
|
private: false,
|
|
38
38
|
description: "Stripe Sync Engine to sync Stripe data to Postgres",
|
|
39
39
|
type: "module",
|
|
40
40
|
main: "./dist/index.cjs",
|
|
41
|
-
bin:
|
|
41
|
+
bin: {
|
|
42
|
+
"sync-engine": "./dist/cli/index.js"
|
|
43
|
+
},
|
|
42
44
|
exports: {
|
|
43
45
|
".": {
|
|
44
46
|
types: "./dist/index.d.ts",
|
|
@@ -90,9 +92,13 @@ var package_default = {
|
|
|
90
92
|
"@types/ws": "^8.5.13",
|
|
91
93
|
"@types/yesql": "^4.1.4",
|
|
92
94
|
"@vitest/ui": "^4.0.9",
|
|
93
|
-
|
|
95
|
+
rimraf: "^6.0.1",
|
|
96
|
+
tsup: "^8.5.0",
|
|
97
|
+
tsx: "^4.21.0",
|
|
98
|
+
typescript: "^5.9.2",
|
|
94
99
|
vitest: "^3.2.4"
|
|
95
100
|
},
|
|
101
|
+
packageManager: "pnpm@10.21.0",
|
|
96
102
|
repository: {
|
|
97
103
|
type: "git",
|
|
98
104
|
url: "https://github.com/stripe-experiments/sync-engine.git"
|
|
@@ -1175,7 +1181,7 @@ async function runSigmaQueryAndDownloadCsv(params) {
|
|
|
1175
1181
|
const pollIntervalMs = params.pollIntervalMs ?? 2e3;
|
|
1176
1182
|
const stripe = new import_stripe2.default(params.apiKey, {
|
|
1177
1183
|
appInfo: {
|
|
1178
|
-
name: "Stripe Sync Engine",
|
|
1184
|
+
name: params.appName ?? "Stripe Sync Engine",
|
|
1179
1185
|
version: package_default.version,
|
|
1180
1186
|
url: package_default.homepage
|
|
1181
1187
|
}
|
|
@@ -1408,7 +1414,7 @@ var StripeSync = class {
|
|
|
1408
1414
|
// @ts-ignore
|
|
1409
1415
|
apiVersion: config.stripeApiVersion,
|
|
1410
1416
|
appInfo: {
|
|
1411
|
-
name: "Stripe Sync Engine",
|
|
1417
|
+
name: config.appName ?? "Stripe Sync Engine",
|
|
1412
1418
|
version: package_default.version,
|
|
1413
1419
|
url: package_default.homepage
|
|
1414
1420
|
}
|
|
@@ -2459,7 +2465,8 @@ ${message}`;
|
|
|
2459
2465
|
const { queryRunId, fileId, csv } = await runSigmaQueryAndDownloadCsv({
|
|
2460
2466
|
apiKey: this.config.stripeSecretKey,
|
|
2461
2467
|
sql: sigmaSql,
|
|
2462
|
-
logger: this.config.logger
|
|
2468
|
+
logger: this.config.logger,
|
|
2469
|
+
appName: this.config.appName
|
|
2463
2470
|
});
|
|
2464
2471
|
const rows = parseCsvObjects(csv);
|
|
2465
2472
|
if (rows.length === 0) {
|
|
@@ -3655,16 +3662,26 @@ ${message}`;
|
|
|
3655
3662
|
const lineItemResponses = [];
|
|
3656
3663
|
let hasMore = true;
|
|
3657
3664
|
let startingAfter = void 0;
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3665
|
+
try {
|
|
3666
|
+
while (hasMore) {
|
|
3667
|
+
const response = await this.stripe.checkout.sessions.listLineItems(checkoutSessionId, {
|
|
3668
|
+
limit: 100,
|
|
3669
|
+
...startingAfter ? { starting_after: startingAfter } : {}
|
|
3670
|
+
});
|
|
3671
|
+
lineItemResponses.push(...response.data);
|
|
3672
|
+
hasMore = response.has_more;
|
|
3673
|
+
if (response.data.length > 0) {
|
|
3674
|
+
startingAfter = response.data[response.data.length - 1].id;
|
|
3675
|
+
}
|
|
3667
3676
|
}
|
|
3677
|
+
} catch (err) {
|
|
3678
|
+
if (err instanceof Error && "statusCode" in err && err.statusCode === 404) {
|
|
3679
|
+
this.config.logger?.warn(
|
|
3680
|
+
`Skipping line items for checkout session ${checkoutSessionId}: line items not available (old test mode session)`
|
|
3681
|
+
);
|
|
3682
|
+
continue;
|
|
3683
|
+
}
|
|
3684
|
+
throw err;
|
|
3668
3685
|
}
|
|
3669
3686
|
await this.upsertCheckoutSessionLineItems(
|
|
3670
3687
|
lineItemResponses,
|
|
@@ -4405,14 +4422,14 @@ Creating ngrok tunnel for port ${port}...`));
|
|
|
4405
4422
|
// src/supabase/supabase.ts
|
|
4406
4423
|
var import_supabase_management_js = require("supabase-management-js");
|
|
4407
4424
|
|
|
4408
|
-
// raw-ts:/Users/
|
|
4409
|
-
var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\n// Get management API base URL from environment variable (for testing against localhost/staging)\n// Caller should provide full URL with protocol (e.g., http://localhost:54323 or https://api.supabase.com)\nconst MGMT_API_BASE_RAW = Deno.env.get('MANAGEMENT_API_URL') || 'https://api.supabase.com'\nconst MGMT_API_BASE = MGMT_API_BASE_RAW.match(/^https?:\\/\\//)\n ? MGMT_API_BASE_RAW\n : `https://${MGMT_API_BASE_RAW}`\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 = `${MGMT_API_BASE}/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 = `${MGMT_API_BASE}/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 = `${MGMT_API_BASE}/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";
|
|
4425
|
+
// raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/src/supabase/edge-functions/stripe-setup.ts
|
|
4426
|
+
var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:@paymentsdb/sync-engine'\nimport postgres from 'npm:postgres'\n\n// Get management API base URL from environment variable (for testing against localhost/staging)\n// Caller should provide full URL with protocol (e.g., http://localhost:54323 or https://api.supabase.com)\nconst MGMT_API_BASE_RAW = Deno.env.get('MANAGEMENT_API_URL') || 'https://api.supabase.com'\nconst MGMT_API_BASE = MGMT_API_BASE_RAW.match(/^https?:\\/\\//)\n ? MGMT_API_BASE_RAW\n : `https://${MGMT_API_BASE_RAW}`\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 = `${MGMT_API_BASE}/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 = `${MGMT_API_BASE}/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 = `${MGMT_API_BASE}/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 appName: Deno.env.get('STRIPE_APP_NAME') || 'PaymentsDB',\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 appName: Deno.env.get('STRIPE_APP_NAME') || 'PaymentsDB',\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";
|
|
4410
4427
|
|
|
4411
|
-
// raw-ts:/Users/
|
|
4412
|
-
var stripe_webhook_default = "import { StripeSync } from 'npm
|
|
4428
|
+
// raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/src/supabase/edge-functions/stripe-webhook.ts
|
|
4429
|
+
var stripe_webhook_default = "import { StripeSync } from 'npm:@paymentsdb/sync-engine'\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 appName: Deno.env.get('STRIPE_APP_NAME') || 'PaymentsDB',\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";
|
|
4413
4430
|
|
|
4414
|
-
// raw-ts:/Users/
|
|
4415
|
-
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
|
|
4431
|
+
// raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/src/supabase/edge-functions/stripe-worker.ts
|
|
4432
|
+
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:@paymentsdb/sync-engine'\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 appName: Deno.env.get('STRIPE_APP_NAME') || 'PaymentsDB',\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";
|
|
4416
4433
|
|
|
4417
4434
|
// src/supabase/edge-function-code.ts
|
|
4418
4435
|
var setupFunctionCode = stripe_setup_default;
|
|
@@ -4722,8 +4739,8 @@ var SupabaseSetupClient = class {
|
|
|
4722
4739
|
return code;
|
|
4723
4740
|
}
|
|
4724
4741
|
return code.replace(
|
|
4725
|
-
/from ['"]npm
|
|
4726
|
-
`from 'npm
|
|
4742
|
+
/from ['"]npm:@paymentsdb\/sync-engine['"]/g,
|
|
4743
|
+
`from 'npm:@paymentsdb/sync-engine@${version}'`
|
|
4727
4744
|
);
|
|
4728
4745
|
}
|
|
4729
4746
|
async install(stripeKey, packageVersion, workerIntervalSeconds) {
|
package/dist/cli/index.js
CHANGED
|
@@ -5,12 +5,12 @@ import {
|
|
|
5
5
|
migrateCommand,
|
|
6
6
|
syncCommand,
|
|
7
7
|
uninstallCommand
|
|
8
|
-
} from "../chunk-
|
|
9
|
-
import "../chunk-
|
|
10
|
-
import "../chunk-
|
|
8
|
+
} from "../chunk-3X3TM6V4.js";
|
|
9
|
+
import "../chunk-UD6RQUDV.js";
|
|
10
|
+
import "../chunk-WQOTGHLT.js";
|
|
11
11
|
import {
|
|
12
12
|
package_default
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-FII5OTPO.js";
|
|
14
14
|
|
|
15
15
|
// src/cli/index.ts
|
|
16
16
|
import { Command } from "commander";
|
package/dist/cli/lib.cjs
CHANGED
|
@@ -40,8 +40,8 @@ __export(lib_exports, {
|
|
|
40
40
|
});
|
|
41
41
|
module.exports = __toCommonJS(lib_exports);
|
|
42
42
|
|
|
43
|
-
//
|
|
44
|
-
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.src
|
|
43
|
+
// node_modules/.pnpm/tsup@8.5.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
|
|
44
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
45
45
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
46
46
|
|
|
47
47
|
// src/cli/commands.ts
|
|
@@ -117,12 +117,14 @@ async function loadConfig(options) {
|
|
|
117
117
|
// package.json
|
|
118
118
|
var package_default = {
|
|
119
119
|
name: "@paymentsdb/sync-engine",
|
|
120
|
-
version: "0.0.
|
|
120
|
+
version: "0.0.4",
|
|
121
121
|
private: false,
|
|
122
122
|
description: "Stripe Sync Engine to sync Stripe data to Postgres",
|
|
123
123
|
type: "module",
|
|
124
124
|
main: "./dist/index.cjs",
|
|
125
|
-
bin:
|
|
125
|
+
bin: {
|
|
126
|
+
"sync-engine": "./dist/cli/index.js"
|
|
127
|
+
},
|
|
126
128
|
exports: {
|
|
127
129
|
".": {
|
|
128
130
|
types: "./dist/index.d.ts",
|
|
@@ -174,9 +176,13 @@ var package_default = {
|
|
|
174
176
|
"@types/ws": "^8.5.13",
|
|
175
177
|
"@types/yesql": "^4.1.4",
|
|
176
178
|
"@vitest/ui": "^4.0.9",
|
|
177
|
-
|
|
179
|
+
rimraf: "^6.0.1",
|
|
180
|
+
tsup: "^8.5.0",
|
|
181
|
+
tsx: "^4.21.0",
|
|
182
|
+
typescript: "^5.9.2",
|
|
178
183
|
vitest: "^3.2.4"
|
|
179
184
|
},
|
|
185
|
+
packageManager: "pnpm@10.21.0",
|
|
180
186
|
repository: {
|
|
181
187
|
type: "git",
|
|
182
188
|
url: "https://github.com/stripe-experiments/sync-engine.git"
|
|
@@ -1189,7 +1195,7 @@ async function runSigmaQueryAndDownloadCsv(params) {
|
|
|
1189
1195
|
const pollIntervalMs = params.pollIntervalMs ?? 2e3;
|
|
1190
1196
|
const stripe = new import_stripe2.default(params.apiKey, {
|
|
1191
1197
|
appInfo: {
|
|
1192
|
-
name: "Stripe Sync Engine",
|
|
1198
|
+
name: params.appName ?? "Stripe Sync Engine",
|
|
1193
1199
|
version: package_default.version,
|
|
1194
1200
|
url: package_default.homepage
|
|
1195
1201
|
}
|
|
@@ -1422,7 +1428,7 @@ var StripeSync = class {
|
|
|
1422
1428
|
// @ts-ignore
|
|
1423
1429
|
apiVersion: config.stripeApiVersion,
|
|
1424
1430
|
appInfo: {
|
|
1425
|
-
name: "Stripe Sync Engine",
|
|
1431
|
+
name: config.appName ?? "Stripe Sync Engine",
|
|
1426
1432
|
version: package_default.version,
|
|
1427
1433
|
url: package_default.homepage
|
|
1428
1434
|
}
|
|
@@ -2473,7 +2479,8 @@ ${message}`;
|
|
|
2473
2479
|
const { queryRunId, fileId, csv } = await runSigmaQueryAndDownloadCsv({
|
|
2474
2480
|
apiKey: this.config.stripeSecretKey,
|
|
2475
2481
|
sql: sigmaSql,
|
|
2476
|
-
logger: this.config.logger
|
|
2482
|
+
logger: this.config.logger,
|
|
2483
|
+
appName: this.config.appName
|
|
2477
2484
|
});
|
|
2478
2485
|
const rows = parseCsvObjects(csv);
|
|
2479
2486
|
if (rows.length === 0) {
|
|
@@ -3669,16 +3676,26 @@ ${message}`;
|
|
|
3669
3676
|
const lineItemResponses = [];
|
|
3670
3677
|
let hasMore = true;
|
|
3671
3678
|
let startingAfter = void 0;
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3679
|
+
try {
|
|
3680
|
+
while (hasMore) {
|
|
3681
|
+
const response = await this.stripe.checkout.sessions.listLineItems(checkoutSessionId, {
|
|
3682
|
+
limit: 100,
|
|
3683
|
+
...startingAfter ? { starting_after: startingAfter } : {}
|
|
3684
|
+
});
|
|
3685
|
+
lineItemResponses.push(...response.data);
|
|
3686
|
+
hasMore = response.has_more;
|
|
3687
|
+
if (response.data.length > 0) {
|
|
3688
|
+
startingAfter = response.data[response.data.length - 1].id;
|
|
3689
|
+
}
|
|
3681
3690
|
}
|
|
3691
|
+
} catch (err) {
|
|
3692
|
+
if (err instanceof Error && "statusCode" in err && err.statusCode === 404) {
|
|
3693
|
+
this.config.logger?.warn(
|
|
3694
|
+
`Skipping line items for checkout session ${checkoutSessionId}: line items not available (old test mode session)`
|
|
3695
|
+
);
|
|
3696
|
+
continue;
|
|
3697
|
+
}
|
|
3698
|
+
throw err;
|
|
3682
3699
|
}
|
|
3683
3700
|
await this.upsertCheckoutSessionLineItems(
|
|
3684
3701
|
lineItemResponses,
|
|
@@ -4419,14 +4436,14 @@ Creating ngrok tunnel for port ${port}...`));
|
|
|
4419
4436
|
// src/supabase/supabase.ts
|
|
4420
4437
|
var import_supabase_management_js = require("supabase-management-js");
|
|
4421
4438
|
|
|
4422
|
-
// raw-ts:/Users/
|
|
4423
|
-
var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\n// Get management API base URL from environment variable (for testing against localhost/staging)\n// Caller should provide full URL with protocol (e.g., http://localhost:54323 or https://api.supabase.com)\nconst MGMT_API_BASE_RAW = Deno.env.get('MANAGEMENT_API_URL') || 'https://api.supabase.com'\nconst MGMT_API_BASE = MGMT_API_BASE_RAW.match(/^https?:\\/\\//)\n ? MGMT_API_BASE_RAW\n : `https://${MGMT_API_BASE_RAW}`\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 = `${MGMT_API_BASE}/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 = `${MGMT_API_BASE}/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 = `${MGMT_API_BASE}/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";
|
|
4439
|
+
// raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/src/supabase/edge-functions/stripe-setup.ts
|
|
4440
|
+
var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:@paymentsdb/sync-engine'\nimport postgres from 'npm:postgres'\n\n// Get management API base URL from environment variable (for testing against localhost/staging)\n// Caller should provide full URL with protocol (e.g., http://localhost:54323 or https://api.supabase.com)\nconst MGMT_API_BASE_RAW = Deno.env.get('MANAGEMENT_API_URL') || 'https://api.supabase.com'\nconst MGMT_API_BASE = MGMT_API_BASE_RAW.match(/^https?:\\/\\//)\n ? MGMT_API_BASE_RAW\n : `https://${MGMT_API_BASE_RAW}`\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 = `${MGMT_API_BASE}/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 = `${MGMT_API_BASE}/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 = `${MGMT_API_BASE}/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 appName: Deno.env.get('STRIPE_APP_NAME') || 'PaymentsDB',\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 appName: Deno.env.get('STRIPE_APP_NAME') || 'PaymentsDB',\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";
|
|
4424
4441
|
|
|
4425
|
-
// raw-ts:/Users/
|
|
4426
|
-
var stripe_webhook_default = "import { StripeSync } from 'npm
|
|
4442
|
+
// raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/src/supabase/edge-functions/stripe-webhook.ts
|
|
4443
|
+
var stripe_webhook_default = "import { StripeSync } from 'npm:@paymentsdb/sync-engine'\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 appName: Deno.env.get('STRIPE_APP_NAME') || 'PaymentsDB',\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";
|
|
4427
4444
|
|
|
4428
|
-
// raw-ts:/Users/
|
|
4429
|
-
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
|
|
4445
|
+
// raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/src/supabase/edge-functions/stripe-worker.ts
|
|
4446
|
+
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:@paymentsdb/sync-engine'\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 appName: Deno.env.get('STRIPE_APP_NAME') || 'PaymentsDB',\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";
|
|
4430
4447
|
|
|
4431
4448
|
// src/supabase/edge-function-code.ts
|
|
4432
4449
|
var setupFunctionCode = stripe_setup_default;
|
|
@@ -4736,8 +4753,8 @@ var SupabaseSetupClient = class {
|
|
|
4736
4753
|
return code;
|
|
4737
4754
|
}
|
|
4738
4755
|
return code.replace(
|
|
4739
|
-
/from ['"]npm
|
|
4740
|
-
`from 'npm
|
|
4756
|
+
/from ['"]npm:@paymentsdb\/sync-engine['"]/g,
|
|
4757
|
+
`from 'npm:@paymentsdb/sync-engine@${version}'`
|
|
4741
4758
|
);
|
|
4742
4759
|
}
|
|
4743
4760
|
async install(stripeKey, packageVersion, workerIntervalSeconds) {
|
package/dist/cli/lib.js
CHANGED
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
migrateCommand,
|
|
7
7
|
syncCommand,
|
|
8
8
|
uninstallCommand
|
|
9
|
-
} from "../chunk-
|
|
10
|
-
import "../chunk-
|
|
11
|
-
import "../chunk-
|
|
12
|
-
import "../chunk-
|
|
9
|
+
} from "../chunk-3X3TM6V4.js";
|
|
10
|
+
import "../chunk-UD6RQUDV.js";
|
|
11
|
+
import "../chunk-WQOTGHLT.js";
|
|
12
|
+
import "../chunk-FII5OTPO.js";
|
|
13
13
|
export {
|
|
14
14
|
backfillCommand,
|
|
15
15
|
createTunnel,
|
package/dist/index.cjs
CHANGED
|
@@ -39,19 +39,21 @@ __export(index_exports, {
|
|
|
39
39
|
});
|
|
40
40
|
module.exports = __toCommonJS(index_exports);
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.src
|
|
42
|
+
// node_modules/.pnpm/tsup@8.5.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
|
|
43
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
44
44
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
45
45
|
|
|
46
46
|
// package.json
|
|
47
47
|
var package_default = {
|
|
48
48
|
name: "@paymentsdb/sync-engine",
|
|
49
|
-
version: "0.0.
|
|
49
|
+
version: "0.0.4",
|
|
50
50
|
private: false,
|
|
51
51
|
description: "Stripe Sync Engine to sync Stripe data to Postgres",
|
|
52
52
|
type: "module",
|
|
53
53
|
main: "./dist/index.cjs",
|
|
54
|
-
bin:
|
|
54
|
+
bin: {
|
|
55
|
+
"sync-engine": "./dist/cli/index.js"
|
|
56
|
+
},
|
|
55
57
|
exports: {
|
|
56
58
|
".": {
|
|
57
59
|
types: "./dist/index.d.ts",
|
|
@@ -103,9 +105,13 @@ var package_default = {
|
|
|
103
105
|
"@types/ws": "^8.5.13",
|
|
104
106
|
"@types/yesql": "^4.1.4",
|
|
105
107
|
"@vitest/ui": "^4.0.9",
|
|
106
|
-
|
|
108
|
+
rimraf: "^6.0.1",
|
|
109
|
+
tsup: "^8.5.0",
|
|
110
|
+
tsx: "^4.21.0",
|
|
111
|
+
typescript: "^5.9.2",
|
|
107
112
|
vitest: "^3.2.4"
|
|
108
113
|
},
|
|
114
|
+
packageManager: "pnpm@10.21.0",
|
|
109
115
|
repository: {
|
|
110
116
|
type: "git",
|
|
111
117
|
url: "https://github.com/stripe-experiments/sync-engine.git"
|
|
@@ -1118,7 +1124,7 @@ async function runSigmaQueryAndDownloadCsv(params) {
|
|
|
1118
1124
|
const pollIntervalMs = params.pollIntervalMs ?? 2e3;
|
|
1119
1125
|
const stripe = new import_stripe2.default(params.apiKey, {
|
|
1120
1126
|
appInfo: {
|
|
1121
|
-
name: "Stripe Sync Engine",
|
|
1127
|
+
name: params.appName ?? "Stripe Sync Engine",
|
|
1122
1128
|
version: package_default.version,
|
|
1123
1129
|
url: package_default.homepage
|
|
1124
1130
|
}
|
|
@@ -1351,7 +1357,7 @@ var StripeSync = class {
|
|
|
1351
1357
|
// @ts-ignore
|
|
1352
1358
|
apiVersion: config.stripeApiVersion,
|
|
1353
1359
|
appInfo: {
|
|
1354
|
-
name: "Stripe Sync Engine",
|
|
1360
|
+
name: config.appName ?? "Stripe Sync Engine",
|
|
1355
1361
|
version: package_default.version,
|
|
1356
1362
|
url: package_default.homepage
|
|
1357
1363
|
}
|
|
@@ -2402,7 +2408,8 @@ ${message}`;
|
|
|
2402
2408
|
const { queryRunId, fileId, csv } = await runSigmaQueryAndDownloadCsv({
|
|
2403
2409
|
apiKey: this.config.stripeSecretKey,
|
|
2404
2410
|
sql: sigmaSql,
|
|
2405
|
-
logger: this.config.logger
|
|
2411
|
+
logger: this.config.logger,
|
|
2412
|
+
appName: this.config.appName
|
|
2406
2413
|
});
|
|
2407
2414
|
const rows = parseCsvObjects(csv);
|
|
2408
2415
|
if (rows.length === 0) {
|
|
@@ -3598,16 +3605,26 @@ ${message}`;
|
|
|
3598
3605
|
const lineItemResponses = [];
|
|
3599
3606
|
let hasMore = true;
|
|
3600
3607
|
let startingAfter = void 0;
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3608
|
+
try {
|
|
3609
|
+
while (hasMore) {
|
|
3610
|
+
const response = await this.stripe.checkout.sessions.listLineItems(checkoutSessionId, {
|
|
3611
|
+
limit: 100,
|
|
3612
|
+
...startingAfter ? { starting_after: startingAfter } : {}
|
|
3613
|
+
});
|
|
3614
|
+
lineItemResponses.push(...response.data);
|
|
3615
|
+
hasMore = response.has_more;
|
|
3616
|
+
if (response.data.length > 0) {
|
|
3617
|
+
startingAfter = response.data[response.data.length - 1].id;
|
|
3618
|
+
}
|
|
3610
3619
|
}
|
|
3620
|
+
} catch (err) {
|
|
3621
|
+
if (err instanceof Error && "statusCode" in err && err.statusCode === 404) {
|
|
3622
|
+
this.config.logger?.warn(
|
|
3623
|
+
`Skipping line items for checkout session ${checkoutSessionId}: line items not available (old test mode session)`
|
|
3624
|
+
);
|
|
3625
|
+
continue;
|
|
3626
|
+
}
|
|
3627
|
+
throw err;
|
|
3611
3628
|
}
|
|
3612
3629
|
await this.upsertCheckoutSessionLineItems(
|
|
3613
3630
|
lineItemResponses,
|
package/dist/index.d.cts
CHANGED
|
@@ -280,6 +280,11 @@ type StripeSyncConfig = {
|
|
|
280
280
|
databaseUrl?: string;
|
|
281
281
|
/** Stripe secret key used to authenticate requests to the Stripe API. Defaults to empty string */
|
|
282
282
|
stripeSecretKey: string;
|
|
283
|
+
/**
|
|
284
|
+
* Application name sent to Stripe in the User-Agent header via appInfo.
|
|
285
|
+
* Default: 'Stripe Sync Engine'
|
|
286
|
+
*/
|
|
287
|
+
appName?: string;
|
|
283
288
|
/**
|
|
284
289
|
* Enables syncing Stripe Sigma (reporting) tables via the Sigma API.
|
|
285
290
|
*
|