stripe-experiment-sync 1.0.12 → 1.0.13
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-2TIRMOJR.js → chunk-2GSABFXH.js} +1 -1
- package/dist/{chunk-6KTLIYUL.js → chunk-5SS5ZEQF.js} +4 -20
- package/dist/{chunk-RAM7OG7J.js → chunk-O2N4AEQS.js} +34 -84
- package/dist/{chunk-IO2EEPFD.js → chunk-RCU5ZXAX.js} +1 -1
- package/dist/cli/index.cjs +38 -105
- package/dist/cli/index.js +6 -7
- package/dist/cli/lib.cjs +36 -102
- package/dist/cli/lib.js +4 -4
- package/dist/index.cjs +1 -1
- package/dist/index.js +2 -2
- package/dist/supabase/index.cjs +34 -94
- package/dist/supabase/index.d.cts +5 -5
- package/dist/supabase/index.d.ts +5 -5
- package/dist/supabase/index.js +2 -2
- package/package.json +1 -1
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
StripeSync,
|
|
3
3
|
createStripeWebSocketClient,
|
|
4
4
|
runMigrations
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-2GSABFXH.js";
|
|
6
6
|
import {
|
|
7
7
|
install,
|
|
8
8
|
uninstall
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-O2N4AEQS.js";
|
|
10
10
|
|
|
11
11
|
// src/cli/config.ts
|
|
12
12
|
import dotenv from "dotenv";
|
|
@@ -556,8 +556,7 @@ async function uninstallCommand(options) {
|
|
|
556
556
|
dotenv2.config();
|
|
557
557
|
let accessToken = options.supabaseAccessToken || process.env.SUPABASE_ACCESS_TOKEN || "";
|
|
558
558
|
let projectRef = options.supabaseProjectRef || process.env.SUPABASE_PROJECT_REF || "";
|
|
559
|
-
|
|
560
|
-
if (!accessToken || !projectRef || !stripeKey) {
|
|
559
|
+
if (!accessToken || !projectRef) {
|
|
561
560
|
const inquirer2 = (await import("inquirer")).default;
|
|
562
561
|
const questions = [];
|
|
563
562
|
if (!accessToken) {
|
|
@@ -577,25 +576,11 @@ async function uninstallCommand(options) {
|
|
|
577
576
|
validate: (input) => input.trim() !== "" || "Project ref is required"
|
|
578
577
|
});
|
|
579
578
|
}
|
|
580
|
-
if (!stripeKey) {
|
|
581
|
-
questions.push({
|
|
582
|
-
type: "password",
|
|
583
|
-
name: "stripeKey",
|
|
584
|
-
message: "Enter your Stripe secret key:",
|
|
585
|
-
mask: "*",
|
|
586
|
-
validate: (input) => {
|
|
587
|
-
if (!input.trim()) return "Stripe key is required";
|
|
588
|
-
if (!input.startsWith("sk_")) return 'Stripe key should start with "sk_"';
|
|
589
|
-
return true;
|
|
590
|
-
}
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
579
|
if (questions.length > 0) {
|
|
594
580
|
console.log(chalk3.yellow("\nMissing required configuration. Please provide:"));
|
|
595
581
|
const answers = await inquirer2.prompt(questions);
|
|
596
582
|
if (answers.accessToken) accessToken = answers.accessToken;
|
|
597
583
|
if (answers.projectRef) projectRef = answers.projectRef;
|
|
598
|
-
if (answers.stripeKey) stripeKey = answers.stripeKey;
|
|
599
584
|
}
|
|
600
585
|
}
|
|
601
586
|
console.log(chalk3.blue("\n\u{1F5D1}\uFE0F Uninstalling Stripe Sync from Supabase...\n"));
|
|
@@ -603,8 +588,7 @@ async function uninstallCommand(options) {
|
|
|
603
588
|
console.log(chalk3.gray("Removing all resources..."));
|
|
604
589
|
await uninstall({
|
|
605
590
|
supabaseAccessToken: accessToken,
|
|
606
|
-
supabaseProjectRef: projectRef
|
|
607
|
-
stripeKey
|
|
591
|
+
supabaseProjectRef: projectRef
|
|
608
592
|
});
|
|
609
593
|
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"));
|
|
610
594
|
console.log(chalk3.cyan.bold(" Uninstall Complete!"));
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
package_default
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-RCU5ZXAX.js";
|
|
4
4
|
|
|
5
5
|
// src/supabase/supabase.ts
|
|
6
6
|
import { SupabaseManagementAPI } from "supabase-management-js";
|
|
7
7
|
|
|
8
8
|
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
|
|
9
|
-
var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nDeno.serve(async (req) => {\n // Require authentication for both GET and POST\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\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 POST requests for setup (existing logic)\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";
|
|
9
|
+
var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\n// Helper to delete edge function via Management API\nasync function deleteEdgeFunction(\n projectRef: string,\n functionSlug: string,\n accessToken: string\n): Promise<void> {\n const url = `https://api.supabase.com/v1/projects/${projectRef}/functions/${functionSlug}`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n throw new Error(`Failed to delete function ${functionSlug}: ${response.status} ${text}`)\n }\n}\n\n// Helper to delete secrets via Management API\nasync function deleteSecret(\n projectRef: string,\n secretName: string,\n accessToken: string\n): Promise<void> {\n const url = `https://api.supabase.com/v1/projects/${projectRef}/secrets`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify([secretName]),\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n console.warn(`Failed to delete secret ${secretName}: ${response.status} ${text}`)\n }\n}\n\nDeno.serve(async (req) => {\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle DELETE requests for uninstall\n if (req.method === 'DELETE') {\n let stripeSync = null\n try {\n // Parse request body\n const body = await req.json()\n const { supabase_access_token, supabase_project_ref } = body\n\n if (!supabase_access_token || !supabase_project_ref) {\n throw new Error(\n 'supabase_access_token and supabase_project_ref are required in request body'\n )\n }\n\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n // Stripe key is required for uninstall to delete webhooks\n const stripeKey = Deno.env.get('STRIPE_SECRET_KEY')\n if (!stripeKey) {\n throw new Error('STRIPE_SECRET_KEY environment variable is required for uninstall')\n }\n\n // Step 1: Delete Stripe webhooks and clean up database\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 },\n stripeSecretKey: stripeKey,\n })\n\n // Delete all managed webhooks\n const webhooks = await stripeSync.listManagedWebhooks()\n for (const webhook of webhooks) {\n try {\n await stripeSync.deleteManagedWebhook(webhook.id)\n console.log(`Deleted webhook: ${webhook.id}`)\n } catch (err) {\n console.warn(`Could not delete webhook ${webhook.id}:`, err)\n }\n }\n\n // Unschedule pg_cron job\n try {\n await stripeSync.postgresClient.query(`\n DO $$\n BEGIN\n IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN\n PERFORM cron.unschedule('stripe-sync-worker');\n END IF;\n END $$;\n `)\n } catch (err) {\n console.warn('Could not unschedule pg_cron job:', err)\n }\n\n // Delete vault secret\n try {\n await stripeSync.postgresClient.query(`\n DELETE FROM vault.secrets\n WHERE name = 'stripe_sync_service_role_key'\n `)\n } catch (err) {\n console.warn('Could not delete vault secret:', err)\n }\n\n // Terminate connections holding locks on stripe schema\n try {\n await stripeSync.postgresClient.query(`\n SELECT pg_terminate_backend(pid)\n FROM pg_locks l\n JOIN pg_class c ON l.relation = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname = 'stripe'\n AND l.pid != pg_backend_pid()\n `)\n } catch (err) {\n console.warn('Could not terminate connections:', err)\n }\n\n // Drop schema with retry\n let dropAttempts = 0\n const maxAttempts = 3\n while (dropAttempts < maxAttempts) {\n try {\n await stripeSync.postgresClient.query('DROP SCHEMA IF EXISTS stripe CASCADE')\n break // Success, exit loop\n } catch (err) {\n dropAttempts++\n if (dropAttempts >= maxAttempts) {\n throw new Error(\n `Failed to drop schema after ${maxAttempts} attempts. ` +\n `There may be active connections or locks on the stripe schema. ` +\n `Error: ${err.message}`\n )\n }\n // Wait 1 second before retrying\n await new Promise((resolve) => setTimeout(resolve, 1000))\n }\n }\n\n await stripeSync.postgresClient.pool.end()\n\n // Step 2: Delete Supabase secrets\n try {\n await deleteSecret(supabase_project_ref, 'STRIPE_SECRET_KEY', supabase_access_token)\n } catch (err) {\n console.warn('Could not delete STRIPE_SECRET_KEY secret:', err)\n }\n\n // Step 3: Delete Edge Functions\n try {\n await deleteEdgeFunction(supabase_project_ref, 'stripe-setup', supabase_access_token)\n } catch (err) {\n console.warn('Could not delete stripe-setup function:', err)\n }\n\n try {\n await deleteEdgeFunction(supabase_project_ref, 'stripe-webhook', supabase_access_token)\n } catch (err) {\n console.warn('Could not delete stripe-webhook function:', err)\n }\n\n try {\n await deleteEdgeFunction(supabase_project_ref, 'stripe-worker', supabase_access_token)\n } catch (err) {\n console.warn('Could not delete stripe-worker function:', err)\n }\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Uninstall complete',\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Uninstall error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n }\n\n // Handle POST requests for install\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
|
|
10
10
|
|
|
11
11
|
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
|
|
12
12
|
var stripe_webhook_default = "import { StripeSync } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const sig = req.headers.get('stripe-signature')\n if (!sig) {\n return new Response('Missing stripe-signature header', { status: 400 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n const stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n\n try {\n const rawBody = new Uint8Array(await req.arrayBuffer())\n await stripeSync.processWebhook(rawBody, sig)\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Webhook processing error:', error)\n const isSignatureError =\n error.message?.includes('signature') || error.type === 'StripeSignatureVerificationError'\n const status = isSignatureError ? 400 : 500\n return new Response(JSON.stringify({ error: error.message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n await stripeSync.postgresClient.pool.end()\n }\n})\n";
|
|
@@ -20,7 +20,6 @@ var webhookFunctionCode = stripe_webhook_default;
|
|
|
20
20
|
var workerFunctionCode = stripe_worker_default;
|
|
21
21
|
|
|
22
22
|
// src/supabase/supabase.ts
|
|
23
|
-
import Stripe from "stripe";
|
|
24
23
|
var STRIPE_SCHEMA_COMMENT_PREFIX = "stripe-sync";
|
|
25
24
|
var INSTALLATION_STARTED_SUFFIX = "installation:started";
|
|
26
25
|
var INSTALLATION_ERROR_SUFFIX = "installation:error";
|
|
@@ -29,6 +28,7 @@ var SupabaseSetupClient = class {
|
|
|
29
28
|
api;
|
|
30
29
|
projectRef;
|
|
31
30
|
projectBaseUrl;
|
|
31
|
+
accessToken;
|
|
32
32
|
constructor(options) {
|
|
33
33
|
this.api = new SupabaseManagementAPI({
|
|
34
34
|
accessToken: options.accessToken,
|
|
@@ -36,6 +36,7 @@ var SupabaseSetupClient = class {
|
|
|
36
36
|
});
|
|
37
37
|
this.projectRef = options.projectRef;
|
|
38
38
|
this.projectBaseUrl = options.projectBaseUrl || process.env.SUPABASE_BASE_URL || "supabase.co";
|
|
39
|
+
this.accessToken = options.accessToken;
|
|
39
40
|
}
|
|
40
41
|
/**
|
|
41
42
|
* Validate that the project exists and we have access
|
|
@@ -55,20 +56,20 @@ var SupabaseSetupClient = class {
|
|
|
55
56
|
/**
|
|
56
57
|
* Deploy an Edge Function
|
|
57
58
|
*/
|
|
58
|
-
async deployFunction(name, code) {
|
|
59
|
+
async deployFunction(name, code, verifyJwt = false) {
|
|
59
60
|
const functions = await this.api.listFunctions(this.projectRef);
|
|
60
61
|
const exists = functions?.some((f) => f.slug === name);
|
|
61
62
|
if (exists) {
|
|
62
63
|
await this.api.updateFunction(this.projectRef, name, {
|
|
63
64
|
body: code,
|
|
64
|
-
verify_jwt:
|
|
65
|
+
verify_jwt: verifyJwt
|
|
65
66
|
});
|
|
66
67
|
} else {
|
|
67
68
|
await this.api.createFunction(this.projectRef, {
|
|
68
69
|
slug: name,
|
|
69
70
|
name,
|
|
70
71
|
body: code,
|
|
71
|
-
verify_jwt:
|
|
72
|
+
verify_jwt: verifyJwt
|
|
72
73
|
});
|
|
73
74
|
}
|
|
74
75
|
}
|
|
@@ -298,77 +299,30 @@ var SupabaseSetupClient = class {
|
|
|
298
299
|
}
|
|
299
300
|
/**
|
|
300
301
|
* Uninstall stripe-sync from a Supabase project
|
|
301
|
-
*
|
|
302
|
+
* Invokes the stripe-setup edge function's DELETE endpoint which handles cleanup
|
|
302
303
|
*/
|
|
303
|
-
async uninstall(
|
|
304
|
-
const stripe = stripeSecretKey ? new Stripe(stripeSecretKey, { apiVersion: "2025-02-24.acacia" }) : null;
|
|
304
|
+
async uninstall() {
|
|
305
305
|
try {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
await this.deleteFunction("stripe-webhook");
|
|
323
|
-
await this.deleteFunction("stripe-worker");
|
|
324
|
-
await this.deleteSecret("STRIPE_SECRET_KEY");
|
|
325
|
-
try {
|
|
326
|
-
await this.runSQL(`
|
|
327
|
-
DO $$
|
|
328
|
-
BEGIN
|
|
329
|
-
IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN
|
|
330
|
-
PERFORM cron.unschedule('stripe-sync-worker');
|
|
331
|
-
END IF;
|
|
332
|
-
END $$;
|
|
333
|
-
`);
|
|
334
|
-
} catch (err) {
|
|
335
|
-
console.warn("Could not unschedule pg_cron job:", err);
|
|
336
|
-
}
|
|
337
|
-
try {
|
|
338
|
-
await this.runSQL(`
|
|
339
|
-
DELETE FROM vault.secrets
|
|
340
|
-
WHERE name = 'stripe_sync_service_role_key'
|
|
341
|
-
`);
|
|
342
|
-
} catch (err) {
|
|
343
|
-
console.warn("Could not delete vault secret:", err);
|
|
344
|
-
}
|
|
345
|
-
try {
|
|
346
|
-
await this.runSQL(`
|
|
347
|
-
SELECT pg_terminate_backend(pid)
|
|
348
|
-
FROM pg_locks l
|
|
349
|
-
JOIN pg_class c ON l.relation = c.oid
|
|
350
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
351
|
-
WHERE n.nspname = 'stripe'
|
|
352
|
-
AND l.pid != pg_backend_pid()
|
|
353
|
-
`);
|
|
354
|
-
} catch (err) {
|
|
355
|
-
console.warn("Could not terminate connections:", err);
|
|
306
|
+
const serviceRoleKey = await this.getServiceRoleKey();
|
|
307
|
+
const url = `https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-setup`;
|
|
308
|
+
const response = await fetch(url, {
|
|
309
|
+
method: "DELETE",
|
|
310
|
+
headers: {
|
|
311
|
+
Authorization: `Bearer ${serviceRoleKey}`,
|
|
312
|
+
"Content-Type": "application/json"
|
|
313
|
+
},
|
|
314
|
+
body: JSON.stringify({
|
|
315
|
+
supabase_access_token: this.accessToken,
|
|
316
|
+
supabase_project_ref: this.projectRef
|
|
317
|
+
})
|
|
318
|
+
});
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
const text = await response.text();
|
|
321
|
+
throw new Error(`Uninstall failed: ${response.status} ${text}`);
|
|
356
322
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
|
|
362
|
-
break;
|
|
363
|
-
} catch (err) {
|
|
364
|
-
dropAttempts++;
|
|
365
|
-
if (dropAttempts >= maxAttempts) {
|
|
366
|
-
throw new Error(
|
|
367
|
-
`Failed to drop schema after ${maxAttempts} attempts. There may be active connections or locks on the stripe schema. Error: ${err instanceof Error ? err.message : String(err)}`
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
371
|
-
}
|
|
323
|
+
const result = await response.json();
|
|
324
|
+
if (result.success === false) {
|
|
325
|
+
throw new Error(`Uninstall failed: ${result.error}`);
|
|
372
326
|
}
|
|
373
327
|
} catch (error) {
|
|
374
328
|
throw new Error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -401,9 +355,9 @@ var SupabaseSetupClient = class {
|
|
|
401
355
|
const versionedSetup = this.injectPackageVersion(setupFunctionCode, version);
|
|
402
356
|
const versionedWebhook = this.injectPackageVersion(webhookFunctionCode, version);
|
|
403
357
|
const versionedWorker = this.injectPackageVersion(workerFunctionCode, version);
|
|
404
|
-
await this.deployFunction("stripe-setup", versionedSetup);
|
|
405
|
-
await this.deployFunction("stripe-webhook", versionedWebhook);
|
|
406
|
-
await this.deployFunction("stripe-worker", versionedWorker);
|
|
358
|
+
await this.deployFunction("stripe-setup", versionedSetup, true);
|
|
359
|
+
await this.deployFunction("stripe-webhook", versionedWebhook, false);
|
|
360
|
+
await this.deployFunction("stripe-worker", versionedWorker, true);
|
|
407
361
|
await this.setSecrets([{ name: "STRIPE_SECRET_KEY", value: trimmedStripeKey }]);
|
|
408
362
|
const serviceRoleKey = await this.getServiceRoleKey();
|
|
409
363
|
const setupResult = await this.invokeFunction("stripe-setup", serviceRoleKey);
|
|
@@ -439,18 +393,14 @@ async function install(params) {
|
|
|
439
393
|
await client.install(stripeKey, packageVersion, workerIntervalSeconds);
|
|
440
394
|
}
|
|
441
395
|
async function uninstall(params) {
|
|
442
|
-
const { supabaseAccessToken, supabaseProjectRef
|
|
443
|
-
const trimmedStripeKey = stripeKey && stripeKey.trim();
|
|
444
|
-
if (trimmedStripeKey && !trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
|
|
445
|
-
throw new Error('Stripe key should start with "sk_" or "rk_"');
|
|
446
|
-
}
|
|
396
|
+
const { supabaseAccessToken, supabaseProjectRef } = params;
|
|
447
397
|
const client = new SupabaseSetupClient({
|
|
448
398
|
accessToken: supabaseAccessToken,
|
|
449
399
|
projectRef: supabaseProjectRef,
|
|
450
400
|
projectBaseUrl: params.baseProjectUrl,
|
|
451
|
-
managementApiBaseUrl: params.
|
|
401
|
+
managementApiBaseUrl: params.baseManagementApiBaseUrl
|
|
452
402
|
});
|
|
453
|
-
await client.uninstall(
|
|
403
|
+
await client.uninstall();
|
|
454
404
|
}
|
|
455
405
|
|
|
456
406
|
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.13",
|
|
37
37
|
private: false,
|
|
38
38
|
description: "Stripe Sync Engine to sync Stripe data to Postgres",
|
|
39
39
|
type: "module",
|
|
@@ -3631,7 +3631,7 @@ Creating ngrok tunnel for port ${port}...`));
|
|
|
3631
3631
|
var import_supabase_management_js = require("supabase-management-js");
|
|
3632
3632
|
|
|
3633
3633
|
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
|
|
3634
|
-
var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nDeno.serve(async (req) => {\n // Require authentication for both GET and POST\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\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 POST requests for setup (existing logic)\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";
|
|
3634
|
+
var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\n// Helper to delete edge function via Management API\nasync function deleteEdgeFunction(\n projectRef: string,\n functionSlug: string,\n accessToken: string\n): Promise<void> {\n const url = `https://api.supabase.com/v1/projects/${projectRef}/functions/${functionSlug}`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n throw new Error(`Failed to delete function ${functionSlug}: ${response.status} ${text}`)\n }\n}\n\n// Helper to delete secrets via Management API\nasync function deleteSecret(\n projectRef: string,\n secretName: string,\n accessToken: string\n): Promise<void> {\n const url = `https://api.supabase.com/v1/projects/${projectRef}/secrets`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify([secretName]),\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n console.warn(`Failed to delete secret ${secretName}: ${response.status} ${text}`)\n }\n}\n\nDeno.serve(async (req) => {\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle DELETE requests for uninstall\n if (req.method === 'DELETE') {\n let stripeSync = null\n try {\n // Parse request body\n const body = await req.json()\n const { supabase_access_token, supabase_project_ref } = body\n\n if (!supabase_access_token || !supabase_project_ref) {\n throw new Error(\n 'supabase_access_token and supabase_project_ref are required in request body'\n )\n }\n\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n // Stripe key is required for uninstall to delete webhooks\n const stripeKey = Deno.env.get('STRIPE_SECRET_KEY')\n if (!stripeKey) {\n throw new Error('STRIPE_SECRET_KEY environment variable is required for uninstall')\n }\n\n // Step 1: Delete Stripe webhooks and clean up database\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 },\n stripeSecretKey: stripeKey,\n })\n\n // Delete all managed webhooks\n const webhooks = await stripeSync.listManagedWebhooks()\n for (const webhook of webhooks) {\n try {\n await stripeSync.deleteManagedWebhook(webhook.id)\n console.log(`Deleted webhook: ${webhook.id}`)\n } catch (err) {\n console.warn(`Could not delete webhook ${webhook.id}:`, err)\n }\n }\n\n // Unschedule pg_cron job\n try {\n await stripeSync.postgresClient.query(`\n DO $$\n BEGIN\n IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN\n PERFORM cron.unschedule('stripe-sync-worker');\n END IF;\n END $$;\n `)\n } catch (err) {\n console.warn('Could not unschedule pg_cron job:', err)\n }\n\n // Delete vault secret\n try {\n await stripeSync.postgresClient.query(`\n DELETE FROM vault.secrets\n WHERE name = 'stripe_sync_service_role_key'\n `)\n } catch (err) {\n console.warn('Could not delete vault secret:', err)\n }\n\n // Terminate connections holding locks on stripe schema\n try {\n await stripeSync.postgresClient.query(`\n SELECT pg_terminate_backend(pid)\n FROM pg_locks l\n JOIN pg_class c ON l.relation = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname = 'stripe'\n AND l.pid != pg_backend_pid()\n `)\n } catch (err) {\n console.warn('Could not terminate connections:', err)\n }\n\n // Drop schema with retry\n let dropAttempts = 0\n const maxAttempts = 3\n while (dropAttempts < maxAttempts) {\n try {\n await stripeSync.postgresClient.query('DROP SCHEMA IF EXISTS stripe CASCADE')\n break // Success, exit loop\n } catch (err) {\n dropAttempts++\n if (dropAttempts >= maxAttempts) {\n throw new Error(\n `Failed to drop schema after ${maxAttempts} attempts. ` +\n `There may be active connections or locks on the stripe schema. ` +\n `Error: ${err.message}`\n )\n }\n // Wait 1 second before retrying\n await new Promise((resolve) => setTimeout(resolve, 1000))\n }\n }\n\n await stripeSync.postgresClient.pool.end()\n\n // Step 2: Delete Supabase secrets\n try {\n await deleteSecret(supabase_project_ref, 'STRIPE_SECRET_KEY', supabase_access_token)\n } catch (err) {\n console.warn('Could not delete STRIPE_SECRET_KEY secret:', err)\n }\n\n // Step 3: Delete Edge Functions\n try {\n await deleteEdgeFunction(supabase_project_ref, 'stripe-setup', supabase_access_token)\n } catch (err) {\n console.warn('Could not delete stripe-setup function:', err)\n }\n\n try {\n await deleteEdgeFunction(supabase_project_ref, 'stripe-webhook', supabase_access_token)\n } catch (err) {\n console.warn('Could not delete stripe-webhook function:', err)\n }\n\n try {\n await deleteEdgeFunction(supabase_project_ref, 'stripe-worker', supabase_access_token)\n } catch (err) {\n console.warn('Could not delete stripe-worker function:', err)\n }\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Uninstall complete',\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Uninstall error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n }\n\n // Handle POST requests for install\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
|
|
3635
3635
|
|
|
3636
3636
|
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
|
|
3637
3637
|
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";
|
|
@@ -3645,7 +3645,6 @@ var webhookFunctionCode = stripe_webhook_default;
|
|
|
3645
3645
|
var workerFunctionCode = stripe_worker_default;
|
|
3646
3646
|
|
|
3647
3647
|
// src/supabase/supabase.ts
|
|
3648
|
-
var import_stripe3 = __toESM(require("stripe"), 1);
|
|
3649
3648
|
var STRIPE_SCHEMA_COMMENT_PREFIX = "stripe-sync";
|
|
3650
3649
|
var INSTALLATION_STARTED_SUFFIX = "installation:started";
|
|
3651
3650
|
var INSTALLATION_ERROR_SUFFIX = "installation:error";
|
|
@@ -3654,6 +3653,7 @@ var SupabaseSetupClient = class {
|
|
|
3654
3653
|
api;
|
|
3655
3654
|
projectRef;
|
|
3656
3655
|
projectBaseUrl;
|
|
3656
|
+
accessToken;
|
|
3657
3657
|
constructor(options) {
|
|
3658
3658
|
this.api = new import_supabase_management_js.SupabaseManagementAPI({
|
|
3659
3659
|
accessToken: options.accessToken,
|
|
@@ -3661,6 +3661,7 @@ var SupabaseSetupClient = class {
|
|
|
3661
3661
|
});
|
|
3662
3662
|
this.projectRef = options.projectRef;
|
|
3663
3663
|
this.projectBaseUrl = options.projectBaseUrl || process.env.SUPABASE_BASE_URL || "supabase.co";
|
|
3664
|
+
this.accessToken = options.accessToken;
|
|
3664
3665
|
}
|
|
3665
3666
|
/**
|
|
3666
3667
|
* Validate that the project exists and we have access
|
|
@@ -3680,20 +3681,20 @@ var SupabaseSetupClient = class {
|
|
|
3680
3681
|
/**
|
|
3681
3682
|
* Deploy an Edge Function
|
|
3682
3683
|
*/
|
|
3683
|
-
async deployFunction(name, code) {
|
|
3684
|
+
async deployFunction(name, code, verifyJwt = false) {
|
|
3684
3685
|
const functions = await this.api.listFunctions(this.projectRef);
|
|
3685
3686
|
const exists = functions?.some((f) => f.slug === name);
|
|
3686
3687
|
if (exists) {
|
|
3687
3688
|
await this.api.updateFunction(this.projectRef, name, {
|
|
3688
3689
|
body: code,
|
|
3689
|
-
verify_jwt:
|
|
3690
|
+
verify_jwt: verifyJwt
|
|
3690
3691
|
});
|
|
3691
3692
|
} else {
|
|
3692
3693
|
await this.api.createFunction(this.projectRef, {
|
|
3693
3694
|
slug: name,
|
|
3694
3695
|
name,
|
|
3695
3696
|
body: code,
|
|
3696
|
-
verify_jwt:
|
|
3697
|
+
verify_jwt: verifyJwt
|
|
3697
3698
|
});
|
|
3698
3699
|
}
|
|
3699
3700
|
}
|
|
@@ -3923,77 +3924,30 @@ var SupabaseSetupClient = class {
|
|
|
3923
3924
|
}
|
|
3924
3925
|
/**
|
|
3925
3926
|
* Uninstall stripe-sync from a Supabase project
|
|
3926
|
-
*
|
|
3927
|
+
* Invokes the stripe-setup edge function's DELETE endpoint which handles cleanup
|
|
3927
3928
|
*/
|
|
3928
|
-
async uninstall(
|
|
3929
|
-
const stripe = stripeSecretKey ? new import_stripe3.default(stripeSecretKey, { apiVersion: "2025-02-24.acacia" }) : null;
|
|
3929
|
+
async uninstall() {
|
|
3930
3930
|
try {
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
}
|
|
3943
|
-
}
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
await this.deleteFunction("stripe-webhook");
|
|
3948
|
-
await this.deleteFunction("stripe-worker");
|
|
3949
|
-
await this.deleteSecret("STRIPE_SECRET_KEY");
|
|
3950
|
-
try {
|
|
3951
|
-
await this.runSQL(`
|
|
3952
|
-
DO $$
|
|
3953
|
-
BEGIN
|
|
3954
|
-
IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN
|
|
3955
|
-
PERFORM cron.unschedule('stripe-sync-worker');
|
|
3956
|
-
END IF;
|
|
3957
|
-
END $$;
|
|
3958
|
-
`);
|
|
3959
|
-
} catch (err) {
|
|
3960
|
-
console.warn("Could not unschedule pg_cron job:", err);
|
|
3961
|
-
}
|
|
3962
|
-
try {
|
|
3963
|
-
await this.runSQL(`
|
|
3964
|
-
DELETE FROM vault.secrets
|
|
3965
|
-
WHERE name = 'stripe_sync_service_role_key'
|
|
3966
|
-
`);
|
|
3967
|
-
} catch (err) {
|
|
3968
|
-
console.warn("Could not delete vault secret:", err);
|
|
3931
|
+
const serviceRoleKey = await this.getServiceRoleKey();
|
|
3932
|
+
const url = `https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-setup`;
|
|
3933
|
+
const response = await fetch(url, {
|
|
3934
|
+
method: "DELETE",
|
|
3935
|
+
headers: {
|
|
3936
|
+
Authorization: `Bearer ${serviceRoleKey}`,
|
|
3937
|
+
"Content-Type": "application/json"
|
|
3938
|
+
},
|
|
3939
|
+
body: JSON.stringify({
|
|
3940
|
+
supabase_access_token: this.accessToken,
|
|
3941
|
+
supabase_project_ref: this.projectRef
|
|
3942
|
+
})
|
|
3943
|
+
});
|
|
3944
|
+
if (!response.ok) {
|
|
3945
|
+
const text = await response.text();
|
|
3946
|
+
throw new Error(`Uninstall failed: ${response.status} ${text}`);
|
|
3969
3947
|
}
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
FROM pg_locks l
|
|
3974
|
-
JOIN pg_class c ON l.relation = c.oid
|
|
3975
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
3976
|
-
WHERE n.nspname = 'stripe'
|
|
3977
|
-
AND l.pid != pg_backend_pid()
|
|
3978
|
-
`);
|
|
3979
|
-
} catch (err) {
|
|
3980
|
-
console.warn("Could not terminate connections:", err);
|
|
3981
|
-
}
|
|
3982
|
-
let dropAttempts = 0;
|
|
3983
|
-
const maxAttempts = 3;
|
|
3984
|
-
while (dropAttempts < maxAttempts) {
|
|
3985
|
-
try {
|
|
3986
|
-
await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
|
|
3987
|
-
break;
|
|
3988
|
-
} catch (err) {
|
|
3989
|
-
dropAttempts++;
|
|
3990
|
-
if (dropAttempts >= maxAttempts) {
|
|
3991
|
-
throw new Error(
|
|
3992
|
-
`Failed to drop schema after ${maxAttempts} attempts. There may be active connections or locks on the stripe schema. Error: ${err instanceof Error ? err.message : String(err)}`
|
|
3993
|
-
);
|
|
3994
|
-
}
|
|
3995
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3996
|
-
}
|
|
3948
|
+
const result = await response.json();
|
|
3949
|
+
if (result.success === false) {
|
|
3950
|
+
throw new Error(`Uninstall failed: ${result.error}`);
|
|
3997
3951
|
}
|
|
3998
3952
|
} catch (error) {
|
|
3999
3953
|
throw new Error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -4026,9 +3980,9 @@ var SupabaseSetupClient = class {
|
|
|
4026
3980
|
const versionedSetup = this.injectPackageVersion(setupFunctionCode, version);
|
|
4027
3981
|
const versionedWebhook = this.injectPackageVersion(webhookFunctionCode, version);
|
|
4028
3982
|
const versionedWorker = this.injectPackageVersion(workerFunctionCode, version);
|
|
4029
|
-
await this.deployFunction("stripe-setup", versionedSetup);
|
|
4030
|
-
await this.deployFunction("stripe-webhook", versionedWebhook);
|
|
4031
|
-
await this.deployFunction("stripe-worker", versionedWorker);
|
|
3983
|
+
await this.deployFunction("stripe-setup", versionedSetup, true);
|
|
3984
|
+
await this.deployFunction("stripe-webhook", versionedWebhook, false);
|
|
3985
|
+
await this.deployFunction("stripe-worker", versionedWorker, true);
|
|
4032
3986
|
await this.setSecrets([{ name: "STRIPE_SECRET_KEY", value: trimmedStripeKey }]);
|
|
4033
3987
|
const serviceRoleKey = await this.getServiceRoleKey();
|
|
4034
3988
|
const setupResult = await this.invokeFunction("stripe-setup", serviceRoleKey);
|
|
@@ -4064,18 +4018,14 @@ async function install(params) {
|
|
|
4064
4018
|
await client.install(stripeKey, packageVersion, workerIntervalSeconds);
|
|
4065
4019
|
}
|
|
4066
4020
|
async function uninstall(params) {
|
|
4067
|
-
const { supabaseAccessToken, supabaseProjectRef
|
|
4068
|
-
const trimmedStripeKey = stripeKey && stripeKey.trim();
|
|
4069
|
-
if (trimmedStripeKey && !trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
|
|
4070
|
-
throw new Error('Stripe key should start with "sk_" or "rk_"');
|
|
4071
|
-
}
|
|
4021
|
+
const { supabaseAccessToken, supabaseProjectRef } = params;
|
|
4072
4022
|
const client = new SupabaseSetupClient({
|
|
4073
4023
|
accessToken: supabaseAccessToken,
|
|
4074
4024
|
projectRef: supabaseProjectRef,
|
|
4075
4025
|
projectBaseUrl: params.baseProjectUrl,
|
|
4076
|
-
managementApiBaseUrl: params.
|
|
4026
|
+
managementApiBaseUrl: params.baseManagementApiBaseUrl
|
|
4077
4027
|
});
|
|
4078
|
-
await client.uninstall(
|
|
4028
|
+
await client.uninstall();
|
|
4079
4029
|
}
|
|
4080
4030
|
|
|
4081
4031
|
// src/cli/commands.ts
|
|
@@ -4537,8 +4487,7 @@ async function uninstallCommand(options) {
|
|
|
4537
4487
|
import_dotenv2.default.config();
|
|
4538
4488
|
let accessToken = options.supabaseAccessToken || process.env.SUPABASE_ACCESS_TOKEN || "";
|
|
4539
4489
|
let projectRef = options.supabaseProjectRef || process.env.SUPABASE_PROJECT_REF || "";
|
|
4540
|
-
|
|
4541
|
-
if (!accessToken || !projectRef || !stripeKey) {
|
|
4490
|
+
if (!accessToken || !projectRef) {
|
|
4542
4491
|
const inquirer2 = (await import("inquirer")).default;
|
|
4543
4492
|
const questions = [];
|
|
4544
4493
|
if (!accessToken) {
|
|
@@ -4558,25 +4507,11 @@ async function uninstallCommand(options) {
|
|
|
4558
4507
|
validate: (input) => input.trim() !== "" || "Project ref is required"
|
|
4559
4508
|
});
|
|
4560
4509
|
}
|
|
4561
|
-
if (!stripeKey) {
|
|
4562
|
-
questions.push({
|
|
4563
|
-
type: "password",
|
|
4564
|
-
name: "stripeKey",
|
|
4565
|
-
message: "Enter your Stripe secret key:",
|
|
4566
|
-
mask: "*",
|
|
4567
|
-
validate: (input) => {
|
|
4568
|
-
if (!input.trim()) return "Stripe key is required";
|
|
4569
|
-
if (!input.startsWith("sk_")) return 'Stripe key should start with "sk_"';
|
|
4570
|
-
return true;
|
|
4571
|
-
}
|
|
4572
|
-
});
|
|
4573
|
-
}
|
|
4574
4510
|
if (questions.length > 0) {
|
|
4575
4511
|
console.log(import_chalk3.default.yellow("\nMissing required configuration. Please provide:"));
|
|
4576
4512
|
const answers = await inquirer2.prompt(questions);
|
|
4577
4513
|
if (answers.accessToken) accessToken = answers.accessToken;
|
|
4578
4514
|
if (answers.projectRef) projectRef = answers.projectRef;
|
|
4579
|
-
if (answers.stripeKey) stripeKey = answers.stripeKey;
|
|
4580
4515
|
}
|
|
4581
4516
|
}
|
|
4582
4517
|
console.log(import_chalk3.default.blue("\n\u{1F5D1}\uFE0F Uninstalling Stripe Sync from Supabase...\n"));
|
|
@@ -4584,8 +4519,7 @@ async function uninstallCommand(options) {
|
|
|
4584
4519
|
console.log(import_chalk3.default.gray("Removing all resources..."));
|
|
4585
4520
|
await uninstall({
|
|
4586
4521
|
supabaseAccessToken: accessToken,
|
|
4587
|
-
supabaseProjectRef: projectRef
|
|
4588
|
-
stripeKey
|
|
4522
|
+
supabaseProjectRef: projectRef
|
|
4589
4523
|
});
|
|
4590
4524
|
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"));
|
|
4591
4525
|
console.log(import_chalk3.default.cyan.bold(" Uninstall Complete!"));
|
|
@@ -4646,11 +4580,10 @@ supabase.command("install").description("Install Stripe sync to Supabase Edge Fu
|
|
|
4646
4580
|
workerInterval: options.workerInterval
|
|
4647
4581
|
});
|
|
4648
4582
|
});
|
|
4649
|
-
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)").
|
|
4583
|
+
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)").action(async (options) => {
|
|
4650
4584
|
await uninstallCommand({
|
|
4651
4585
|
supabaseAccessToken: options.token,
|
|
4652
|
-
supabaseProjectRef: options.project
|
|
4653
|
-
stripeKey: options.stripeKey
|
|
4586
|
+
supabaseProjectRef: options.project
|
|
4654
4587
|
});
|
|
4655
4588
|
});
|
|
4656
4589
|
program.parse();
|