stripe-experiment-sync 1.0.15 → 1.0.16
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/README.md +189 -117
- package/dist/{chunk-XIFB5ALW.js → chunk-2Q3SNSKG.js} +12 -6
- package/dist/{chunk-GOBVFXU7.js → chunk-NI6UMBIL.js} +12 -4
- package/dist/{chunk-AODI5WO6.js → chunk-QHM3A4VW.js} +8 -4
- package/dist/{chunk-3KVILTN4.js → chunk-VOYYTAJF.js} +5 -5
- package/dist/cli/index.cjs +44 -18
- package/dist/cli/index.js +15 -7
- package/dist/cli/lib.cjs +33 -15
- package/dist/cli/lib.d.cts +1 -0
- package/dist/cli/lib.d.ts +1 -0
- package/dist/cli/lib.js +4 -4
- package/dist/index.cjs +16 -8
- package/dist/index.js +2 -2
- package/dist/supabase/index.cjs +16 -10
- package/dist/supabase/index.d.cts +4 -3
- package/dist/supabase/index.d.ts +4 -3
- package/dist/supabase/index.js +2 -2
- package/package.json +5 -5
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
StripeSync,
|
|
3
3
|
createStripeWebSocketClient,
|
|
4
4
|
runMigrations
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-NI6UMBIL.js";
|
|
6
6
|
import {
|
|
7
7
|
install,
|
|
8
8
|
uninstall
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-2Q3SNSKG.js";
|
|
10
10
|
|
|
11
11
|
// src/cli/config.ts
|
|
12
12
|
import dotenv from "dotenv";
|
|
@@ -545,13 +545,15 @@ async function installCommand(options) {
|
|
|
545
545
|
}
|
|
546
546
|
}
|
|
547
547
|
console.log(chalk3.blue("\n\u{1F680} Installing Stripe Sync to Supabase Edge Functions...\n"));
|
|
548
|
+
const supabaseManagementUrl = options.supabaseManagementUrl || process.env.SUPABASE_MANAGEMENT_URL;
|
|
548
549
|
console.log(chalk3.gray("Validating project access..."));
|
|
549
550
|
await install({
|
|
550
551
|
supabaseAccessToken: accessToken,
|
|
551
552
|
supabaseProjectRef: projectRef,
|
|
552
553
|
stripeKey,
|
|
553
554
|
packageVersion: options.packageVersion,
|
|
554
|
-
workerIntervalSeconds: options.workerInterval
|
|
555
|
+
workerIntervalSeconds: options.workerInterval,
|
|
556
|
+
supabaseManagementUrl
|
|
555
557
|
});
|
|
556
558
|
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"));
|
|
557
559
|
console.log(chalk3.cyan.bold(" Installation Complete!"));
|
|
@@ -602,10 +604,12 @@ async function uninstallCommand(options) {
|
|
|
602
604
|
}
|
|
603
605
|
console.log(chalk3.blue("\n\u{1F5D1}\uFE0F Uninstalling Stripe Sync from Supabase...\n"));
|
|
604
606
|
console.log(chalk3.yellow("\u26A0\uFE0F Warning: This will delete all Stripe data from your database!\n"));
|
|
607
|
+
const supabaseManagementUrl = options.supabaseManagementUrl || process.env.SUPABASE_MANAGEMENT_URL;
|
|
605
608
|
console.log(chalk3.gray("Removing all resources..."));
|
|
606
609
|
await uninstall({
|
|
607
610
|
supabaseAccessToken: accessToken,
|
|
608
|
-
supabaseProjectRef: projectRef
|
|
611
|
+
supabaseProjectRef: projectRef,
|
|
612
|
+
supabaseManagementUrl
|
|
609
613
|
});
|
|
610
614
|
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"));
|
|
611
615
|
console.log(chalk3.cyan.bold(" Uninstall Complete!"));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// package.json
|
|
2
2
|
var package_default = {
|
|
3
3
|
name: "stripe-experiment-sync",
|
|
4
|
-
version: "1.0.
|
|
4
|
+
version: "1.0.16",
|
|
5
5
|
private: false,
|
|
6
6
|
description: "Stripe Sync Engine to sync Stripe data to Postgres",
|
|
7
7
|
type: "module",
|
|
@@ -63,11 +63,11 @@ var package_default = {
|
|
|
63
63
|
},
|
|
64
64
|
repository: {
|
|
65
65
|
type: "git",
|
|
66
|
-
url: "https://github.com/
|
|
66
|
+
url: "https://github.com/stripe-experiments/sync-engine.git"
|
|
67
67
|
},
|
|
68
|
-
homepage: "https://github.com/
|
|
68
|
+
homepage: "https://github.com/stripe-experiments/sync-engine#readme",
|
|
69
69
|
bugs: {
|
|
70
|
-
url: "https://github.com/
|
|
70
|
+
url: "https://github.com/stripe-experiments/sync-engine/issues"
|
|
71
71
|
},
|
|
72
72
|
keywords: [
|
|
73
73
|
"stripe",
|
|
@@ -79,7 +79,7 @@ var package_default = {
|
|
|
79
79
|
"database",
|
|
80
80
|
"typescript"
|
|
81
81
|
],
|
|
82
|
-
author: "
|
|
82
|
+
author: "Stripe <https://stripe.com/>"
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
export {
|
package/dist/cli/index.cjs
CHANGED
|
@@ -33,7 +33,7 @@ var import_commander = require("commander");
|
|
|
33
33
|
// package.json
|
|
34
34
|
var package_default = {
|
|
35
35
|
name: "stripe-experiment-sync",
|
|
36
|
-
version: "1.0.
|
|
36
|
+
version: "1.0.16",
|
|
37
37
|
private: false,
|
|
38
38
|
description: "Stripe Sync Engine to sync Stripe data to Postgres",
|
|
39
39
|
type: "module",
|
|
@@ -95,11 +95,11 @@ var package_default = {
|
|
|
95
95
|
},
|
|
96
96
|
repository: {
|
|
97
97
|
type: "git",
|
|
98
|
-
url: "https://github.com/
|
|
98
|
+
url: "https://github.com/stripe-experiments/sync-engine.git"
|
|
99
99
|
},
|
|
100
|
-
homepage: "https://github.com/
|
|
100
|
+
homepage: "https://github.com/stripe-experiments/sync-engine#readme",
|
|
101
101
|
bugs: {
|
|
102
|
-
url: "https://github.com/
|
|
102
|
+
url: "https://github.com/stripe-experiments/sync-engine/issues"
|
|
103
103
|
},
|
|
104
104
|
keywords: [
|
|
105
105
|
"stripe",
|
|
@@ -111,7 +111,7 @@ var package_default = {
|
|
|
111
111
|
"database",
|
|
112
112
|
"typescript"
|
|
113
113
|
],
|
|
114
|
-
author: "
|
|
114
|
+
author: "Stripe <https://stripe.com/>"
|
|
115
115
|
};
|
|
116
116
|
|
|
117
117
|
// src/cli/commands.ts
|
|
@@ -1129,7 +1129,13 @@ async function fetchStripeText(url, apiKey, options) {
|
|
|
1129
1129
|
async function runSigmaQueryAndDownloadCsv(params) {
|
|
1130
1130
|
const pollTimeoutMs = params.pollTimeoutMs ?? 5 * 60 * 1e3;
|
|
1131
1131
|
const pollIntervalMs = params.pollIntervalMs ?? 2e3;
|
|
1132
|
-
const stripe = new import_stripe2.default(params.apiKey
|
|
1132
|
+
const stripe = new import_stripe2.default(params.apiKey, {
|
|
1133
|
+
appInfo: {
|
|
1134
|
+
name: "Stripe Sync Engine",
|
|
1135
|
+
version: package_default.version,
|
|
1136
|
+
url: package_default.homepage
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1133
1139
|
const created = await stripe.rawRequest("POST", "/v1/sigma/query_runs", {
|
|
1134
1140
|
sql: params.sql
|
|
1135
1141
|
});
|
|
@@ -1358,7 +1364,9 @@ var StripeSync = class {
|
|
|
1358
1364
|
// @ts-ignore
|
|
1359
1365
|
apiVersion: config.stripeApiVersion,
|
|
1360
1366
|
appInfo: {
|
|
1361
|
-
name: "Stripe
|
|
1367
|
+
name: "Stripe Sync Engine",
|
|
1368
|
+
version: package_default.version,
|
|
1369
|
+
url: package_default.homepage
|
|
1362
1370
|
}
|
|
1363
1371
|
});
|
|
1364
1372
|
this.stripe = createRetryableStripeClient(baseStripe, {}, config.logger);
|
|
@@ -2835,7 +2843,7 @@ var StripeSync = class {
|
|
|
2835
2843
|
const customerIds = await this.postgresClient.query(prepared.text, prepared.values).then(({ rows }) => rows.map((it) => it.id));
|
|
2836
2844
|
this.config.logger?.info(`Getting payment methods for ${customerIds.length} customers`);
|
|
2837
2845
|
let synced = 0;
|
|
2838
|
-
const chunkSize = this.config.maxConcurrentCustomers ??
|
|
2846
|
+
const chunkSize = this.config.maxConcurrentCustomers ?? 3;
|
|
2839
2847
|
for (const customerIdChunk of chunkArray(customerIds, chunkSize)) {
|
|
2840
2848
|
await Promise.all(
|
|
2841
2849
|
customerIdChunk.map(async (customerId) => {
|
|
@@ -4242,7 +4250,7 @@ Creating ngrok tunnel for port ${port}...`));
|
|
|
4242
4250
|
var import_supabase_management_js = require("supabase-management-js");
|
|
4243
4251
|
|
|
4244
4252
|
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
|
|
4245
|
-
var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\n// Helper to validate accessToken against Management API\nasync function validateAccessToken(projectRef: string, accessToken: string): Promise<boolean> {\n // Try to fetch project details using the access token\n // This validates that the token is valid for the management API\n const url = `https://api.supabase.com/v1/projects/${projectRef}`\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n // If we can successfully get the project, the token is valid\n return response.ok\n}\n\n// Helper to delete edge function via Management API\nasync function deleteEdgeFunction(\n projectRef: string,\n functionSlug: string,\n accessToken: string\n): Promise<void> {\n const url = `https://api.supabase.com/v1/projects/${projectRef}/functions/${functionSlug}`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n throw new Error(`Failed to delete function ${functionSlug}: ${response.status} ${text}`)\n }\n}\n\n// Helper to delete secrets via Management API\nasync function deleteSecret(\n projectRef: string,\n secretName: string,\n accessToken: string\n): Promise<void> {\n const url = `https://api.supabase.com/v1/projects/${projectRef}/secrets`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify([secretName]),\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n console.warn(`Failed to delete secret ${secretName}: ${response.status} ${text}`)\n }\n}\n\nDeno.serve(async (req) => {\n // Extract project ref from SUPABASE_URL (format: https://{projectRef}.{base})\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n const projectRef = new URL(supabaseUrl).hostname.split('.')[0]\n\n // Validate access token for all requests\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const accessToken = authHeader.substring(7) // Remove 'Bearer '\n const isValid = await validateAccessToken(projectRef, accessToken)\n if (!isValid) {\n return new Response('Forbidden: Invalid access token for this project', { status: 403 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle DELETE requests for uninstall\n if (req.method === 'DELETE') {\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n // Stripe key is required for uninstall to delete webhooks\n const stripeKey = Deno.env.get('STRIPE_SECRET_KEY')\n if (!stripeKey) {\n throw new Error('STRIPE_SECRET_KEY environment variable is required for uninstall')\n }\n\n // Step 1: Delete Stripe webhooks and clean up database\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 },\n stripeSecretKey: stripeKey,\n })\n\n // Delete all managed webhooks\n const webhooks = await stripeSync.listManagedWebhooks()\n for (const webhook of webhooks) {\n try {\n await stripeSync.deleteManagedWebhook(webhook.id)\n console.log(`Deleted webhook: ${webhook.id}`)\n } catch (err) {\n console.warn(`Could not delete webhook ${webhook.id}:`, err)\n }\n }\n\n // Unschedule pg_cron job\n try {\n await stripeSync.postgresClient.query(`\n DO $$\n BEGIN\n IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN\n PERFORM cron.unschedule('stripe-sync-worker');\n END IF;\n END $$;\n `)\n } catch (err) {\n console.warn('Could not unschedule pg_cron job:', err)\n }\n\n // Delete vault secret\n try {\n await stripeSync.postgresClient.query(`\n DELETE FROM vault.secrets\n WHERE name = 'stripe_sync_worker_secret'\n `)\n } catch (err) {\n console.warn('Could not delete vault secret:', err)\n }\n\n // Terminate connections holding locks on stripe schema\n try {\n await stripeSync.postgresClient.query(`\n SELECT pg_terminate_backend(pid)\n FROM pg_locks l\n JOIN pg_class c ON l.relation = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname = 'stripe'\n AND l.pid != pg_backend_pid()\n `)\n } catch (err) {\n console.warn('Could not terminate connections:', err)\n }\n\n // Drop schema with retry\n let dropAttempts = 0\n const maxAttempts = 3\n while (dropAttempts < maxAttempts) {\n try {\n await stripeSync.postgresClient.query('DROP SCHEMA IF EXISTS stripe CASCADE')\n break // Success, exit loop\n } catch (err) {\n dropAttempts++\n if (dropAttempts >= maxAttempts) {\n throw new Error(\n `Failed to drop schema after ${maxAttempts} attempts. ` +\n `There may be active connections or locks on the stripe schema. ` +\n `Error: ${err.message}`\n )\n }\n // Wait 1 second before retrying\n await new Promise((resolve) => setTimeout(resolve, 1000))\n }\n }\n\n await stripeSync.postgresClient.pool.end()\n\n // Step 2: Delete Supabase secrets\n try {\n await deleteSecret(projectRef, 'STRIPE_SECRET_KEY', accessToken)\n } catch (err) {\n console.warn('Could not delete STRIPE_SECRET_KEY secret:', err)\n }\n\n // Step 3: Delete Edge Functions\n try {\n await deleteEdgeFunction(projectRef, 'stripe-setup', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-setup function:', err)\n }\n\n try {\n await deleteEdgeFunction(projectRef, 'stripe-webhook', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-webhook function:', err)\n }\n\n try {\n await deleteEdgeFunction(projectRef, 'stripe-worker', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-worker function:', err)\n }\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Uninstall complete',\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Uninstall error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n }\n\n // Handle POST requests for install\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
|
|
4253
|
+
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('SUPABASE_MANAGEMENT_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";
|
|
4246
4254
|
|
|
4247
4255
|
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
|
|
4248
4256
|
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";
|
|
@@ -4264,14 +4272,16 @@ var SupabaseSetupClient = class {
|
|
|
4264
4272
|
api;
|
|
4265
4273
|
projectRef;
|
|
4266
4274
|
projectBaseUrl;
|
|
4275
|
+
supabaseManagementUrl;
|
|
4267
4276
|
accessToken;
|
|
4268
4277
|
constructor(options) {
|
|
4269
4278
|
this.api = new import_supabase_management_js.SupabaseManagementAPI({
|
|
4270
4279
|
accessToken: options.accessToken,
|
|
4271
|
-
baseUrl: options.
|
|
4280
|
+
baseUrl: options.supabaseManagementUrl
|
|
4272
4281
|
});
|
|
4273
4282
|
this.projectRef = options.projectRef;
|
|
4274
4283
|
this.projectBaseUrl = options.projectBaseUrl || process.env.SUPABASE_BASE_URL || "supabase.co";
|
|
4284
|
+
this.supabaseManagementUrl = options.supabaseManagementUrl;
|
|
4275
4285
|
this.accessToken = options.accessToken;
|
|
4276
4286
|
}
|
|
4277
4287
|
/**
|
|
@@ -4578,7 +4588,11 @@ var SupabaseSetupClient = class {
|
|
|
4578
4588
|
await this.deployFunction("stripe-setup", versionedSetup, false);
|
|
4579
4589
|
await this.deployFunction("stripe-webhook", versionedWebhook, false);
|
|
4580
4590
|
await this.deployFunction("stripe-worker", versionedWorker, false);
|
|
4581
|
-
|
|
4591
|
+
const secrets = [{ name: "STRIPE_SECRET_KEY", value: trimmedStripeKey }];
|
|
4592
|
+
if (this.supabaseManagementUrl) {
|
|
4593
|
+
secrets.push({ name: "SUPABASE_MANAGEMENT_URL", value: this.supabaseManagementUrl });
|
|
4594
|
+
}
|
|
4595
|
+
await this.setSecrets(secrets);
|
|
4582
4596
|
const setupResult = await this.invokeFunction("stripe-setup", this.accessToken);
|
|
4583
4597
|
if (!setupResult.success) {
|
|
4584
4598
|
throw new Error(`Setup failed: ${setupResult.error}`);
|
|
@@ -4607,7 +4621,7 @@ async function install(params) {
|
|
|
4607
4621
|
accessToken: supabaseAccessToken,
|
|
4608
4622
|
projectRef: supabaseProjectRef,
|
|
4609
4623
|
projectBaseUrl: params.baseProjectUrl,
|
|
4610
|
-
|
|
4624
|
+
supabaseManagementUrl: params.supabaseManagementUrl
|
|
4611
4625
|
});
|
|
4612
4626
|
await client.install(stripeKey, packageVersion, workerIntervalSeconds);
|
|
4613
4627
|
}
|
|
@@ -4617,7 +4631,7 @@ async function uninstall(params) {
|
|
|
4617
4631
|
accessToken: supabaseAccessToken,
|
|
4618
4632
|
projectRef: supabaseProjectRef,
|
|
4619
4633
|
projectBaseUrl: params.baseProjectUrl,
|
|
4620
|
-
|
|
4634
|
+
supabaseManagementUrl: params.supabaseManagementUrl
|
|
4621
4635
|
});
|
|
4622
4636
|
await client.uninstall();
|
|
4623
4637
|
}
|
|
@@ -5058,13 +5072,15 @@ async function installCommand(options) {
|
|
|
5058
5072
|
}
|
|
5059
5073
|
}
|
|
5060
5074
|
console.log(import_chalk3.default.blue("\n\u{1F680} Installing Stripe Sync to Supabase Edge Functions...\n"));
|
|
5075
|
+
const supabaseManagementUrl = options.supabaseManagementUrl || process.env.SUPABASE_MANAGEMENT_URL;
|
|
5061
5076
|
console.log(import_chalk3.default.gray("Validating project access..."));
|
|
5062
5077
|
await install({
|
|
5063
5078
|
supabaseAccessToken: accessToken,
|
|
5064
5079
|
supabaseProjectRef: projectRef,
|
|
5065
5080
|
stripeKey,
|
|
5066
5081
|
packageVersion: options.packageVersion,
|
|
5067
|
-
workerIntervalSeconds: options.workerInterval
|
|
5082
|
+
workerIntervalSeconds: options.workerInterval,
|
|
5083
|
+
supabaseManagementUrl
|
|
5068
5084
|
});
|
|
5069
5085
|
console.log(import_chalk3.default.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"));
|
|
5070
5086
|
console.log(import_chalk3.default.cyan.bold(" Installation Complete!"));
|
|
@@ -5115,10 +5131,12 @@ async function uninstallCommand(options) {
|
|
|
5115
5131
|
}
|
|
5116
5132
|
console.log(import_chalk3.default.blue("\n\u{1F5D1}\uFE0F Uninstalling Stripe Sync from Supabase...\n"));
|
|
5117
5133
|
console.log(import_chalk3.default.yellow("\u26A0\uFE0F Warning: This will delete all Stripe data from your database!\n"));
|
|
5134
|
+
const supabaseManagementUrl = options.supabaseManagementUrl || process.env.SUPABASE_MANAGEMENT_URL;
|
|
5118
5135
|
console.log(import_chalk3.default.gray("Removing all resources..."));
|
|
5119
5136
|
await uninstall({
|
|
5120
5137
|
supabaseAccessToken: accessToken,
|
|
5121
|
-
supabaseProjectRef: projectRef
|
|
5138
|
+
supabaseProjectRef: projectRef,
|
|
5139
|
+
supabaseManagementUrl
|
|
5122
5140
|
});
|
|
5123
5141
|
console.log(import_chalk3.default.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"));
|
|
5124
5142
|
console.log(import_chalk3.default.cyan.bold(" Uninstall Complete!"));
|
|
@@ -5171,19 +5189,27 @@ supabase.command("install").description("Install Stripe sync to Supabase Edge Fu
|
|
|
5171
5189
|
"--worker-interval <seconds>",
|
|
5172
5190
|
"Worker interval in seconds (defaults to 60)",
|
|
5173
5191
|
(val) => parseInt(val, 10)
|
|
5192
|
+
).option(
|
|
5193
|
+
"--management-url <url>",
|
|
5194
|
+
"Supabase management API URL with protocol (e.g., http://localhost:54323, defaults to https://api.supabase.com or SUPABASE_MANAGEMENT_URL env)"
|
|
5174
5195
|
).action(async (options) => {
|
|
5175
5196
|
await installCommand({
|
|
5176
5197
|
supabaseAccessToken: options.token,
|
|
5177
5198
|
supabaseProjectRef: options.project,
|
|
5178
5199
|
stripeKey: options.stripeKey,
|
|
5179
5200
|
packageVersion: options.packageVersion,
|
|
5180
|
-
workerInterval: options.workerInterval
|
|
5201
|
+
workerInterval: options.workerInterval,
|
|
5202
|
+
supabaseManagementUrl: options.managementUrl
|
|
5181
5203
|
});
|
|
5182
5204
|
});
|
|
5183
|
-
supabase.command("uninstall").description("Uninstall Stripe sync from Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").
|
|
5205
|
+
supabase.command("uninstall").description("Uninstall Stripe sync from Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option(
|
|
5206
|
+
"--management-url <url>",
|
|
5207
|
+
"Supabase management API URL with protocol (e.g., http://localhost:54323, defaults to https://api.supabase.com or SUPABASE_MANAGEMENT_URL env)"
|
|
5208
|
+
).action(async (options) => {
|
|
5184
5209
|
await uninstallCommand({
|
|
5185
5210
|
supabaseAccessToken: options.token,
|
|
5186
|
-
supabaseProjectRef: options.project
|
|
5211
|
+
supabaseProjectRef: options.project,
|
|
5212
|
+
supabaseManagementUrl: options.managementUrl
|
|
5187
5213
|
});
|
|
5188
5214
|
});
|
|
5189
5215
|
program.parse();
|
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-QHM3A4VW.js";
|
|
9
|
+
import "../chunk-NI6UMBIL.js";
|
|
10
|
+
import "../chunk-2Q3SNSKG.js";
|
|
11
11
|
import {
|
|
12
12
|
package_default
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-VOYYTAJF.js";
|
|
14
14
|
|
|
15
15
|
// src/cli/index.ts
|
|
16
16
|
import { Command } from "commander";
|
|
@@ -46,19 +46,27 @@ supabase.command("install").description("Install Stripe sync to Supabase Edge Fu
|
|
|
46
46
|
"--worker-interval <seconds>",
|
|
47
47
|
"Worker interval in seconds (defaults to 60)",
|
|
48
48
|
(val) => parseInt(val, 10)
|
|
49
|
+
).option(
|
|
50
|
+
"--management-url <url>",
|
|
51
|
+
"Supabase management API URL with protocol (e.g., http://localhost:54323, defaults to https://api.supabase.com or SUPABASE_MANAGEMENT_URL env)"
|
|
49
52
|
).action(async (options) => {
|
|
50
53
|
await installCommand({
|
|
51
54
|
supabaseAccessToken: options.token,
|
|
52
55
|
supabaseProjectRef: options.project,
|
|
53
56
|
stripeKey: options.stripeKey,
|
|
54
57
|
packageVersion: options.packageVersion,
|
|
55
|
-
workerInterval: options.workerInterval
|
|
58
|
+
workerInterval: options.workerInterval,
|
|
59
|
+
supabaseManagementUrl: options.managementUrl
|
|
56
60
|
});
|
|
57
61
|
});
|
|
58
|
-
supabase.command("uninstall").description("Uninstall Stripe sync from Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").
|
|
62
|
+
supabase.command("uninstall").description("Uninstall Stripe sync from Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option(
|
|
63
|
+
"--management-url <url>",
|
|
64
|
+
"Supabase management API URL with protocol (e.g., http://localhost:54323, defaults to https://api.supabase.com or SUPABASE_MANAGEMENT_URL env)"
|
|
65
|
+
).action(async (options) => {
|
|
59
66
|
await uninstallCommand({
|
|
60
67
|
supabaseAccessToken: options.token,
|
|
61
|
-
supabaseProjectRef: options.project
|
|
68
|
+
supabaseProjectRef: options.project,
|
|
69
|
+
supabaseManagementUrl: options.managementUrl
|
|
62
70
|
});
|
|
63
71
|
});
|
|
64
72
|
program.parse();
|
package/dist/cli/lib.cjs
CHANGED
|
@@ -117,7 +117,7 @@ async function loadConfig(options) {
|
|
|
117
117
|
// package.json
|
|
118
118
|
var package_default = {
|
|
119
119
|
name: "stripe-experiment-sync",
|
|
120
|
-
version: "1.0.
|
|
120
|
+
version: "1.0.16",
|
|
121
121
|
private: false,
|
|
122
122
|
description: "Stripe Sync Engine to sync Stripe data to Postgres",
|
|
123
123
|
type: "module",
|
|
@@ -179,11 +179,11 @@ var package_default = {
|
|
|
179
179
|
},
|
|
180
180
|
repository: {
|
|
181
181
|
type: "git",
|
|
182
|
-
url: "https://github.com/
|
|
182
|
+
url: "https://github.com/stripe-experiments/sync-engine.git"
|
|
183
183
|
},
|
|
184
|
-
homepage: "https://github.com/
|
|
184
|
+
homepage: "https://github.com/stripe-experiments/sync-engine#readme",
|
|
185
185
|
bugs: {
|
|
186
|
-
url: "https://github.com/
|
|
186
|
+
url: "https://github.com/stripe-experiments/sync-engine/issues"
|
|
187
187
|
},
|
|
188
188
|
keywords: [
|
|
189
189
|
"stripe",
|
|
@@ -195,7 +195,7 @@ var package_default = {
|
|
|
195
195
|
"database",
|
|
196
196
|
"typescript"
|
|
197
197
|
],
|
|
198
|
-
author: "
|
|
198
|
+
author: "Stripe <https://stripe.com/>"
|
|
199
199
|
};
|
|
200
200
|
|
|
201
201
|
// src/stripeSync.ts
|
|
@@ -1143,7 +1143,13 @@ async function fetchStripeText(url, apiKey, options) {
|
|
|
1143
1143
|
async function runSigmaQueryAndDownloadCsv(params) {
|
|
1144
1144
|
const pollTimeoutMs = params.pollTimeoutMs ?? 5 * 60 * 1e3;
|
|
1145
1145
|
const pollIntervalMs = params.pollIntervalMs ?? 2e3;
|
|
1146
|
-
const stripe = new import_stripe2.default(params.apiKey
|
|
1146
|
+
const stripe = new import_stripe2.default(params.apiKey, {
|
|
1147
|
+
appInfo: {
|
|
1148
|
+
name: "Stripe Sync Engine",
|
|
1149
|
+
version: package_default.version,
|
|
1150
|
+
url: package_default.homepage
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1147
1153
|
const created = await stripe.rawRequest("POST", "/v1/sigma/query_runs", {
|
|
1148
1154
|
sql: params.sql
|
|
1149
1155
|
});
|
|
@@ -1372,7 +1378,9 @@ var StripeSync = class {
|
|
|
1372
1378
|
// @ts-ignore
|
|
1373
1379
|
apiVersion: config.stripeApiVersion,
|
|
1374
1380
|
appInfo: {
|
|
1375
|
-
name: "Stripe
|
|
1381
|
+
name: "Stripe Sync Engine",
|
|
1382
|
+
version: package_default.version,
|
|
1383
|
+
url: package_default.homepage
|
|
1376
1384
|
}
|
|
1377
1385
|
});
|
|
1378
1386
|
this.stripe = createRetryableStripeClient(baseStripe, {}, config.logger);
|
|
@@ -2849,7 +2857,7 @@ var StripeSync = class {
|
|
|
2849
2857
|
const customerIds = await this.postgresClient.query(prepared.text, prepared.values).then(({ rows }) => rows.map((it) => it.id));
|
|
2850
2858
|
this.config.logger?.info(`Getting payment methods for ${customerIds.length} customers`);
|
|
2851
2859
|
let synced = 0;
|
|
2852
|
-
const chunkSize = this.config.maxConcurrentCustomers ??
|
|
2860
|
+
const chunkSize = this.config.maxConcurrentCustomers ?? 3;
|
|
2853
2861
|
for (const customerIdChunk of chunkArray(customerIds, chunkSize)) {
|
|
2854
2862
|
await Promise.all(
|
|
2855
2863
|
customerIdChunk.map(async (customerId) => {
|
|
@@ -4256,7 +4264,7 @@ Creating ngrok tunnel for port ${port}...`));
|
|
|
4256
4264
|
var import_supabase_management_js = require("supabase-management-js");
|
|
4257
4265
|
|
|
4258
4266
|
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
|
|
4259
|
-
var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\n// Helper to validate accessToken against Management API\nasync function validateAccessToken(projectRef: string, accessToken: string): Promise<boolean> {\n // Try to fetch project details using the access token\n // This validates that the token is valid for the management API\n const url = `https://api.supabase.com/v1/projects/${projectRef}`\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n // If we can successfully get the project, the token is valid\n return response.ok\n}\n\n// Helper to delete edge function via Management API\nasync function deleteEdgeFunction(\n projectRef: string,\n functionSlug: string,\n accessToken: string\n): Promise<void> {\n const url = `https://api.supabase.com/v1/projects/${projectRef}/functions/${functionSlug}`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n throw new Error(`Failed to delete function ${functionSlug}: ${response.status} ${text}`)\n }\n}\n\n// Helper to delete secrets via Management API\nasync function deleteSecret(\n projectRef: string,\n secretName: string,\n accessToken: string\n): Promise<void> {\n const url = `https://api.supabase.com/v1/projects/${projectRef}/secrets`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify([secretName]),\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n console.warn(`Failed to delete secret ${secretName}: ${response.status} ${text}`)\n }\n}\n\nDeno.serve(async (req) => {\n // Extract project ref from SUPABASE_URL (format: https://{projectRef}.{base})\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n const projectRef = new URL(supabaseUrl).hostname.split('.')[0]\n\n // Validate access token for all requests\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const accessToken = authHeader.substring(7) // Remove 'Bearer '\n const isValid = await validateAccessToken(projectRef, accessToken)\n if (!isValid) {\n return new Response('Forbidden: Invalid access token for this project', { status: 403 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle DELETE requests for uninstall\n if (req.method === 'DELETE') {\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n // Stripe key is required for uninstall to delete webhooks\n const stripeKey = Deno.env.get('STRIPE_SECRET_KEY')\n if (!stripeKey) {\n throw new Error('STRIPE_SECRET_KEY environment variable is required for uninstall')\n }\n\n // Step 1: Delete Stripe webhooks and clean up database\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 },\n stripeSecretKey: stripeKey,\n })\n\n // Delete all managed webhooks\n const webhooks = await stripeSync.listManagedWebhooks()\n for (const webhook of webhooks) {\n try {\n await stripeSync.deleteManagedWebhook(webhook.id)\n console.log(`Deleted webhook: ${webhook.id}`)\n } catch (err) {\n console.warn(`Could not delete webhook ${webhook.id}:`, err)\n }\n }\n\n // Unschedule pg_cron job\n try {\n await stripeSync.postgresClient.query(`\n DO $$\n BEGIN\n IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN\n PERFORM cron.unschedule('stripe-sync-worker');\n END IF;\n END $$;\n `)\n } catch (err) {\n console.warn('Could not unschedule pg_cron job:', err)\n }\n\n // Delete vault secret\n try {\n await stripeSync.postgresClient.query(`\n DELETE FROM vault.secrets\n WHERE name = 'stripe_sync_worker_secret'\n `)\n } catch (err) {\n console.warn('Could not delete vault secret:', err)\n }\n\n // Terminate connections holding locks on stripe schema\n try {\n await stripeSync.postgresClient.query(`\n SELECT pg_terminate_backend(pid)\n FROM pg_locks l\n JOIN pg_class c ON l.relation = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname = 'stripe'\n AND l.pid != pg_backend_pid()\n `)\n } catch (err) {\n console.warn('Could not terminate connections:', err)\n }\n\n // Drop schema with retry\n let dropAttempts = 0\n const maxAttempts = 3\n while (dropAttempts < maxAttempts) {\n try {\n await stripeSync.postgresClient.query('DROP SCHEMA IF EXISTS stripe CASCADE')\n break // Success, exit loop\n } catch (err) {\n dropAttempts++\n if (dropAttempts >= maxAttempts) {\n throw new Error(\n `Failed to drop schema after ${maxAttempts} attempts. ` +\n `There may be active connections or locks on the stripe schema. ` +\n `Error: ${err.message}`\n )\n }\n // Wait 1 second before retrying\n await new Promise((resolve) => setTimeout(resolve, 1000))\n }\n }\n\n await stripeSync.postgresClient.pool.end()\n\n // Step 2: Delete Supabase secrets\n try {\n await deleteSecret(projectRef, 'STRIPE_SECRET_KEY', accessToken)\n } catch (err) {\n console.warn('Could not delete STRIPE_SECRET_KEY secret:', err)\n }\n\n // Step 3: Delete Edge Functions\n try {\n await deleteEdgeFunction(projectRef, 'stripe-setup', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-setup function:', err)\n }\n\n try {\n await deleteEdgeFunction(projectRef, 'stripe-webhook', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-webhook function:', err)\n }\n\n try {\n await deleteEdgeFunction(projectRef, 'stripe-worker', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-worker function:', err)\n }\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Uninstall complete',\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Uninstall error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n }\n\n // Handle POST requests for install\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
|
|
4267
|
+
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('SUPABASE_MANAGEMENT_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";
|
|
4260
4268
|
|
|
4261
4269
|
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
|
|
4262
4270
|
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";
|
|
@@ -4278,14 +4286,16 @@ var SupabaseSetupClient = class {
|
|
|
4278
4286
|
api;
|
|
4279
4287
|
projectRef;
|
|
4280
4288
|
projectBaseUrl;
|
|
4289
|
+
supabaseManagementUrl;
|
|
4281
4290
|
accessToken;
|
|
4282
4291
|
constructor(options) {
|
|
4283
4292
|
this.api = new import_supabase_management_js.SupabaseManagementAPI({
|
|
4284
4293
|
accessToken: options.accessToken,
|
|
4285
|
-
baseUrl: options.
|
|
4294
|
+
baseUrl: options.supabaseManagementUrl
|
|
4286
4295
|
});
|
|
4287
4296
|
this.projectRef = options.projectRef;
|
|
4288
4297
|
this.projectBaseUrl = options.projectBaseUrl || process.env.SUPABASE_BASE_URL || "supabase.co";
|
|
4298
|
+
this.supabaseManagementUrl = options.supabaseManagementUrl;
|
|
4289
4299
|
this.accessToken = options.accessToken;
|
|
4290
4300
|
}
|
|
4291
4301
|
/**
|
|
@@ -4592,7 +4602,11 @@ var SupabaseSetupClient = class {
|
|
|
4592
4602
|
await this.deployFunction("stripe-setup", versionedSetup, false);
|
|
4593
4603
|
await this.deployFunction("stripe-webhook", versionedWebhook, false);
|
|
4594
4604
|
await this.deployFunction("stripe-worker", versionedWorker, false);
|
|
4595
|
-
|
|
4605
|
+
const secrets = [{ name: "STRIPE_SECRET_KEY", value: trimmedStripeKey }];
|
|
4606
|
+
if (this.supabaseManagementUrl) {
|
|
4607
|
+
secrets.push({ name: "SUPABASE_MANAGEMENT_URL", value: this.supabaseManagementUrl });
|
|
4608
|
+
}
|
|
4609
|
+
await this.setSecrets(secrets);
|
|
4596
4610
|
const setupResult = await this.invokeFunction("stripe-setup", this.accessToken);
|
|
4597
4611
|
if (!setupResult.success) {
|
|
4598
4612
|
throw new Error(`Setup failed: ${setupResult.error}`);
|
|
@@ -4621,7 +4635,7 @@ async function install(params) {
|
|
|
4621
4635
|
accessToken: supabaseAccessToken,
|
|
4622
4636
|
projectRef: supabaseProjectRef,
|
|
4623
4637
|
projectBaseUrl: params.baseProjectUrl,
|
|
4624
|
-
|
|
4638
|
+
supabaseManagementUrl: params.supabaseManagementUrl
|
|
4625
4639
|
});
|
|
4626
4640
|
await client.install(stripeKey, packageVersion, workerIntervalSeconds);
|
|
4627
4641
|
}
|
|
@@ -4631,7 +4645,7 @@ async function uninstall(params) {
|
|
|
4631
4645
|
accessToken: supabaseAccessToken,
|
|
4632
4646
|
projectRef: supabaseProjectRef,
|
|
4633
4647
|
projectBaseUrl: params.baseProjectUrl,
|
|
4634
|
-
|
|
4648
|
+
supabaseManagementUrl: params.supabaseManagementUrl
|
|
4635
4649
|
});
|
|
4636
4650
|
await client.uninstall();
|
|
4637
4651
|
}
|
|
@@ -5072,13 +5086,15 @@ async function installCommand(options) {
|
|
|
5072
5086
|
}
|
|
5073
5087
|
}
|
|
5074
5088
|
console.log(import_chalk3.default.blue("\n\u{1F680} Installing Stripe Sync to Supabase Edge Functions...\n"));
|
|
5089
|
+
const supabaseManagementUrl = options.supabaseManagementUrl || process.env.SUPABASE_MANAGEMENT_URL;
|
|
5075
5090
|
console.log(import_chalk3.default.gray("Validating project access..."));
|
|
5076
5091
|
await install({
|
|
5077
5092
|
supabaseAccessToken: accessToken,
|
|
5078
5093
|
supabaseProjectRef: projectRef,
|
|
5079
5094
|
stripeKey,
|
|
5080
5095
|
packageVersion: options.packageVersion,
|
|
5081
|
-
workerIntervalSeconds: options.workerInterval
|
|
5096
|
+
workerIntervalSeconds: options.workerInterval,
|
|
5097
|
+
supabaseManagementUrl
|
|
5082
5098
|
});
|
|
5083
5099
|
console.log(import_chalk3.default.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"));
|
|
5084
5100
|
console.log(import_chalk3.default.cyan.bold(" Installation Complete!"));
|
|
@@ -5129,10 +5145,12 @@ async function uninstallCommand(options) {
|
|
|
5129
5145
|
}
|
|
5130
5146
|
console.log(import_chalk3.default.blue("\n\u{1F5D1}\uFE0F Uninstalling Stripe Sync from Supabase...\n"));
|
|
5131
5147
|
console.log(import_chalk3.default.yellow("\u26A0\uFE0F Warning: This will delete all Stripe data from your database!\n"));
|
|
5148
|
+
const supabaseManagementUrl = options.supabaseManagementUrl || process.env.SUPABASE_MANAGEMENT_URL;
|
|
5132
5149
|
console.log(import_chalk3.default.gray("Removing all resources..."));
|
|
5133
5150
|
await uninstall({
|
|
5134
5151
|
supabaseAccessToken: accessToken,
|
|
5135
|
-
supabaseProjectRef: projectRef
|
|
5152
|
+
supabaseProjectRef: projectRef,
|
|
5153
|
+
supabaseManagementUrl
|
|
5136
5154
|
});
|
|
5137
5155
|
console.log(import_chalk3.default.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"));
|
|
5138
5156
|
console.log(import_chalk3.default.cyan.bold(" Uninstall Complete!"));
|
package/dist/cli/lib.d.cts
CHANGED
package/dist/cli/lib.d.ts
CHANGED
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-QHM3A4VW.js";
|
|
10
|
+
import "../chunk-NI6UMBIL.js";
|
|
11
|
+
import "../chunk-2Q3SNSKG.js";
|
|
12
|
+
import "../chunk-VOYYTAJF.js";
|
|
13
13
|
export {
|
|
14
14
|
backfillCommand,
|
|
15
15
|
createTunnel,
|