stripe-experiment-sync 1.0.13 → 1.0.15-beta.1766078819
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-5J4LJ44K.js +400 -0
- package/dist/{chunk-5SS5ZEQF.js → chunk-7G2ZYKBP.js} +27 -10
- package/dist/{chunk-2GSABFXH.js → chunk-ENHIM76M.js} +806 -209
- package/dist/{chunk-RCU5ZXAX.js → chunk-TPFENIK3.js} +3 -1
- package/dist/cli/index.cjs +850 -250
- package/dist/cli/index.js +7 -6
- package/dist/cli/lib.cjs +847 -248
- package/dist/cli/lib.d.cts +2 -0
- package/dist/cli/lib.d.ts +2 -0
- package/dist/cli/lib.js +4 -4
- package/dist/index.cjs +807 -208
- package/dist/index.d.cts +110 -3
- package/dist/index.d.ts +110 -3
- package/dist/index.js +2 -2
- package/dist/migrations/0059_sigma_subscription_item_change_events_v2_beta.sql +61 -0
- package/dist/migrations/0060_sigma_exchange_rates_from_usd.sql +38 -0
- package/dist/supabase/index.cjs +18 -33
- package/dist/supabase/index.d.cts +1 -5
- package/dist/supabase/index.d.ts +1 -5
- package/dist/supabase/index.js +2 -2
- package/package.json +3 -1
- package/dist/chunk-O2N4AEQS.js +0 -417
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import {
|
|
2
|
+
package_default
|
|
3
|
+
} from "./chunk-TPFENIK3.js";
|
|
4
|
+
|
|
5
|
+
// src/supabase/supabase.ts
|
|
6
|
+
import { SupabaseManagementAPI } from "supabase-management-js";
|
|
7
|
+
|
|
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\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";
|
|
10
|
+
|
|
11
|
+
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
|
|
12
|
+
var stripe_webhook_default = "import { StripeSync } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const sig = req.headers.get('stripe-signature')\n if (!sig) {\n return new Response('Missing stripe-signature header', { status: 400 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n const stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n\n try {\n const rawBody = new Uint8Array(await req.arrayBuffer())\n await stripeSync.processWebhook(rawBody, sig)\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Webhook processing error:', error)\n const isSignatureError =\n error.message?.includes('signature') || error.type === 'StripeSignatureVerificationError'\n const status = isSignatureError ? 400 : 500\n return new Response(JSON.stringify({ error: error.message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n await stripeSync.postgresClient.pool.end()\n }\n})\n";
|
|
13
|
+
|
|
14
|
+
// raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
|
|
15
|
+
var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron at a configurable interval (default: 60 seconds). Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const token = authHeader.substring(7) // Remove 'Bearer '\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Validate that the token matches the unique worker secret stored in vault\n const vaultResult = await sql`\n SELECT decrypted_secret\n FROM vault.decrypted_secrets\n WHERE name = 'stripe_sync_worker_secret'\n `\n\n if (vaultResult.length === 0) {\n await sql.end()\n return new Response('Worker secret not configured in vault', { status: 500 })\n }\n\n const storedSecret = vaultResult[0].decrypted_secret\n if (token !== storedSecret) {\n await sql.end()\n return new Response('Forbidden: Invalid worker secret', { status: 403 })\n }\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n enableSigma: (Deno.env.get('ENABLE_SIGMA') ?? 'false') === 'true',\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
|
|
16
|
+
|
|
17
|
+
// src/supabase/edge-function-code.ts
|
|
18
|
+
var setupFunctionCode = stripe_setup_default;
|
|
19
|
+
var webhookFunctionCode = stripe_webhook_default;
|
|
20
|
+
var workerFunctionCode = stripe_worker_default;
|
|
21
|
+
|
|
22
|
+
// src/supabase/supabase.ts
|
|
23
|
+
var STRIPE_SCHEMA_COMMENT_PREFIX = "stripe-sync";
|
|
24
|
+
var INSTALLATION_STARTED_SUFFIX = "installation:started";
|
|
25
|
+
var INSTALLATION_ERROR_SUFFIX = "installation:error";
|
|
26
|
+
var INSTALLATION_INSTALLED_SUFFIX = "installed";
|
|
27
|
+
var SupabaseSetupClient = class {
|
|
28
|
+
api;
|
|
29
|
+
projectRef;
|
|
30
|
+
projectBaseUrl;
|
|
31
|
+
accessToken;
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this.api = new SupabaseManagementAPI({
|
|
34
|
+
accessToken: options.accessToken,
|
|
35
|
+
baseUrl: options.managementApiBaseUrl
|
|
36
|
+
});
|
|
37
|
+
this.projectRef = options.projectRef;
|
|
38
|
+
this.projectBaseUrl = options.projectBaseUrl || process.env.SUPABASE_BASE_URL || "supabase.co";
|
|
39
|
+
this.accessToken = options.accessToken;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validate that the project exists and we have access
|
|
43
|
+
*/
|
|
44
|
+
async validateProject() {
|
|
45
|
+
const projects = await this.api.getProjects();
|
|
46
|
+
const project = projects?.find((p) => p.id === this.projectRef);
|
|
47
|
+
if (!project) {
|
|
48
|
+
throw new Error(`Project ${this.projectRef} not found or you don't have access`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
id: project.id,
|
|
52
|
+
name: project.name,
|
|
53
|
+
region: project.region
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Deploy an Edge Function
|
|
58
|
+
*/
|
|
59
|
+
async deployFunction(name, code, verifyJwt = false) {
|
|
60
|
+
const functions = await this.api.listFunctions(this.projectRef);
|
|
61
|
+
const exists = functions?.some((f) => f.slug === name);
|
|
62
|
+
if (exists) {
|
|
63
|
+
await this.api.updateFunction(this.projectRef, name, {
|
|
64
|
+
body: code,
|
|
65
|
+
verify_jwt: verifyJwt
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
await this.api.createFunction(this.projectRef, {
|
|
69
|
+
slug: name,
|
|
70
|
+
name,
|
|
71
|
+
body: code,
|
|
72
|
+
verify_jwt: verifyJwt
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Set secrets for Edge Functions
|
|
78
|
+
*/
|
|
79
|
+
async setSecrets(secrets) {
|
|
80
|
+
await this.api.createSecrets(this.projectRef, secrets);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Run SQL against the database
|
|
84
|
+
*/
|
|
85
|
+
async runSQL(sql) {
|
|
86
|
+
return await this.api.runQuery(this.projectRef, sql);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Setup pg_cron job to invoke worker function
|
|
90
|
+
* @param intervalSeconds - How often to run the worker (default: 60 seconds)
|
|
91
|
+
*/
|
|
92
|
+
async setupPgCronJob(intervalSeconds = 60) {
|
|
93
|
+
if (!Number.isInteger(intervalSeconds) || intervalSeconds < 1) {
|
|
94
|
+
throw new Error(`Invalid interval: ${intervalSeconds}. Must be a positive integer.`);
|
|
95
|
+
}
|
|
96
|
+
let schedule;
|
|
97
|
+
if (intervalSeconds < 60) {
|
|
98
|
+
schedule = `${intervalSeconds} seconds`;
|
|
99
|
+
} else if (intervalSeconds % 60 === 0) {
|
|
100
|
+
const minutes = intervalSeconds / 60;
|
|
101
|
+
if (minutes < 60) {
|
|
102
|
+
schedule = `*/${minutes} * * * *`;
|
|
103
|
+
} else {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Invalid interval: ${intervalSeconds}. Intervals >= 3600 seconds (1 hour) are not supported. Use a value between 1-3599 seconds.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Invalid interval: ${intervalSeconds}. Must be either 1-59 seconds or a multiple of 60 (e.g., 60, 120, 180).`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const workerSecret = crypto.randomUUID();
|
|
114
|
+
const escapedWorkerSecret = workerSecret.replace(/'/g, "''");
|
|
115
|
+
const sql = `
|
|
116
|
+
-- Enable extensions
|
|
117
|
+
CREATE EXTENSION IF NOT EXISTS pg_cron;
|
|
118
|
+
CREATE EXTENSION IF NOT EXISTS pg_net;
|
|
119
|
+
CREATE EXTENSION IF NOT EXISTS pgmq;
|
|
120
|
+
|
|
121
|
+
-- Create pgmq queue for sync work (idempotent)
|
|
122
|
+
SELECT pgmq.create('stripe_sync_work')
|
|
123
|
+
WHERE NOT EXISTS (
|
|
124
|
+
SELECT 1 FROM pgmq.list_queues() WHERE queue_name = 'stripe_sync_work'
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
-- Store unique worker secret in vault for pg_cron to use
|
|
128
|
+
-- Delete existing secret if it exists, then create new one
|
|
129
|
+
DELETE FROM vault.secrets WHERE name = 'stripe_sync_worker_secret';
|
|
130
|
+
SELECT vault.create_secret('${escapedWorkerSecret}', 'stripe_sync_worker_secret');
|
|
131
|
+
|
|
132
|
+
-- Delete existing jobs if they exist
|
|
133
|
+
SELECT cron.unschedule('stripe-sync-worker') WHERE EXISTS (
|
|
134
|
+
SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker'
|
|
135
|
+
);
|
|
136
|
+
SELECT cron.unschedule('stripe-sync-scheduler') WHERE EXISTS (
|
|
137
|
+
SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-scheduler'
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
-- Create job to invoke worker at configured interval
|
|
141
|
+
-- Worker reads from pgmq, enqueues objects if empty, and processes sync work
|
|
142
|
+
SELECT cron.schedule(
|
|
143
|
+
'stripe-sync-worker',
|
|
144
|
+
'${schedule}',
|
|
145
|
+
$$
|
|
146
|
+
SELECT net.http_post(
|
|
147
|
+
url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
|
|
148
|
+
headers := jsonb_build_object(
|
|
149
|
+
'Authorization', 'Bearer ' || (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'stripe_sync_worker_secret')
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
$$
|
|
153
|
+
);
|
|
154
|
+
`;
|
|
155
|
+
await this.runSQL(sql);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get the webhook URL for this project
|
|
159
|
+
*/
|
|
160
|
+
getWebhookUrl() {
|
|
161
|
+
return `https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-webhook`;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get the anon key for this project (needed for Realtime subscriptions)
|
|
165
|
+
*/
|
|
166
|
+
async getAnonKey() {
|
|
167
|
+
const apiKeys = await this.api.getProjectApiKeys(this.projectRef);
|
|
168
|
+
const anonKey = apiKeys?.find((k) => k.name === "anon");
|
|
169
|
+
if (!anonKey) {
|
|
170
|
+
throw new Error("Could not find anon API key");
|
|
171
|
+
}
|
|
172
|
+
return anonKey.api_key;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get the project URL
|
|
176
|
+
*/
|
|
177
|
+
getProjectUrl() {
|
|
178
|
+
return `https://${this.projectRef}.${this.projectBaseUrl}`;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Invoke an Edge Function
|
|
182
|
+
*/
|
|
183
|
+
async invokeFunction(name, bearerToken) {
|
|
184
|
+
const url = `https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/${name}`;
|
|
185
|
+
const response = await fetch(url, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: {
|
|
188
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
189
|
+
"Content-Type": "application/json"
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
const text = await response.text();
|
|
194
|
+
return { success: false, error: `${response.status}: ${text}` };
|
|
195
|
+
}
|
|
196
|
+
const result = await response.json();
|
|
197
|
+
if (result.success === false) {
|
|
198
|
+
return { success: false, error: result.error };
|
|
199
|
+
}
|
|
200
|
+
return { success: true };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Check if stripe-sync is installed in the database.
|
|
204
|
+
*
|
|
205
|
+
* Uses the Supabase Management API to run SQL queries.
|
|
206
|
+
* Uses duck typing (schema + migrations table) combined with comment validation.
|
|
207
|
+
* Throws error for legacy installations to prevent accidental corruption.
|
|
208
|
+
*
|
|
209
|
+
* @param schema The schema name to check (defaults to 'stripe')
|
|
210
|
+
* @returns true if properly installed with comment marker, false if not installed
|
|
211
|
+
* @throws Error if legacy installation detected (schema exists without comment)
|
|
212
|
+
*/
|
|
213
|
+
async isInstalled(schema = "stripe") {
|
|
214
|
+
try {
|
|
215
|
+
const schemaCheck = await this.runSQL(
|
|
216
|
+
`SELECT EXISTS (
|
|
217
|
+
SELECT 1 FROM information_schema.schemata
|
|
218
|
+
WHERE schema_name = '${schema}'
|
|
219
|
+
) as schema_exists`
|
|
220
|
+
);
|
|
221
|
+
const schemaExists = schemaCheck[0]?.rows?.[0]?.schema_exists === true;
|
|
222
|
+
if (!schemaExists) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
const migrationsCheck = await this.runSQL(
|
|
226
|
+
`SELECT EXISTS (
|
|
227
|
+
SELECT 1 FROM information_schema.tables
|
|
228
|
+
WHERE table_schema = '${schema}' AND table_name IN ('migrations', '_migrations')
|
|
229
|
+
) as table_exists`
|
|
230
|
+
);
|
|
231
|
+
const migrationsTableExists = migrationsCheck[0]?.rows?.[0]?.table_exists === true;
|
|
232
|
+
if (!migrationsTableExists) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
const commentCheck = await this.runSQL(
|
|
236
|
+
`SELECT obj_description(oid, 'pg_namespace') as comment
|
|
237
|
+
FROM pg_namespace
|
|
238
|
+
WHERE nspname = '${schema}'`
|
|
239
|
+
);
|
|
240
|
+
const comment = commentCheck[0]?.rows?.[0]?.comment;
|
|
241
|
+
if (!comment || !comment.includes(STRIPE_SCHEMA_COMMENT_PREFIX)) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Legacy installation detected: Schema '${schema}' and migrations table exist, but missing stripe-sync comment marker. This may be a legacy installation or manually created schema. Please contact support or manually drop the schema before proceeding.`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (comment.includes(INSTALLATION_STARTED_SUFFIX)) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
if (comment.includes(INSTALLATION_ERROR_SUFFIX)) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`Installation failed: Schema '${schema}' exists but installation encountered an error. Comment: ${comment}. Please uninstall and install again.`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return true;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (error instanceof Error && (error.message.includes("Legacy installation detected") || error.message.includes("Installation failed"))) {
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Update installation progress comment on the stripe schema
|
|
264
|
+
*/
|
|
265
|
+
async updateInstallationComment(message) {
|
|
266
|
+
const escapedMessage = message.replace(/'/g, "''");
|
|
267
|
+
await this.runSQL(`COMMENT ON SCHEMA stripe IS '${escapedMessage}'`);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Delete an Edge Function
|
|
271
|
+
*/
|
|
272
|
+
async deleteFunction(name) {
|
|
273
|
+
try {
|
|
274
|
+
await this.api.deleteFunction(this.projectRef, name);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.warn(`Could not delete function ${name}:`, err);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Delete a secret
|
|
281
|
+
*/
|
|
282
|
+
async deleteSecret(name) {
|
|
283
|
+
try {
|
|
284
|
+
await this.api.deleteSecrets(this.projectRef, [name]);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.warn(`Could not delete secret ${name}:`, err);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Uninstall stripe-sync from a Supabase project
|
|
291
|
+
* Invokes the stripe-setup edge function's DELETE endpoint which handles cleanup
|
|
292
|
+
*/
|
|
293
|
+
async uninstall() {
|
|
294
|
+
try {
|
|
295
|
+
const url = `https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-setup`;
|
|
296
|
+
const response = await fetch(url, {
|
|
297
|
+
method: "DELETE",
|
|
298
|
+
headers: {
|
|
299
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
300
|
+
"Content-Type": "application/json"
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
if (!response.ok) {
|
|
304
|
+
const text = await response.text();
|
|
305
|
+
throw new Error(`Uninstall failed: ${response.status} ${text}`);
|
|
306
|
+
}
|
|
307
|
+
const result = await response.json();
|
|
308
|
+
if (result.success === false) {
|
|
309
|
+
throw new Error(`Uninstall failed: ${result.error}`);
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
throw new Error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Inject package version into Edge Function code
|
|
317
|
+
*/
|
|
318
|
+
injectPackageVersion(code, version) {
|
|
319
|
+
if (version === "latest") {
|
|
320
|
+
return code;
|
|
321
|
+
}
|
|
322
|
+
return code.replace(
|
|
323
|
+
/from ['"]npm:stripe-experiment-sync['"]/g,
|
|
324
|
+
`from 'npm:stripe-experiment-sync@${version}'`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
async install(stripeKey, packageVersion, workerIntervalSeconds) {
|
|
328
|
+
const trimmedStripeKey = stripeKey.trim();
|
|
329
|
+
if (!trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
|
|
330
|
+
throw new Error('Stripe key should start with "sk_" or "rk_"');
|
|
331
|
+
}
|
|
332
|
+
const version = packageVersion || "latest";
|
|
333
|
+
try {
|
|
334
|
+
await this.validateProject();
|
|
335
|
+
await this.runSQL(`CREATE SCHEMA IF NOT EXISTS stripe`);
|
|
336
|
+
await this.updateInstallationComment(
|
|
337
|
+
`${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_STARTED_SUFFIX}`
|
|
338
|
+
);
|
|
339
|
+
const versionedSetup = this.injectPackageVersion(setupFunctionCode, version);
|
|
340
|
+
const versionedWebhook = this.injectPackageVersion(webhookFunctionCode, version);
|
|
341
|
+
const versionedWorker = this.injectPackageVersion(workerFunctionCode, version);
|
|
342
|
+
await this.deployFunction("stripe-setup", versionedSetup, false);
|
|
343
|
+
await this.deployFunction("stripe-webhook", versionedWebhook, false);
|
|
344
|
+
await this.deployFunction("stripe-worker", versionedWorker, false);
|
|
345
|
+
await this.setSecrets([{ name: "STRIPE_SECRET_KEY", value: trimmedStripeKey }]);
|
|
346
|
+
const setupResult = await this.invokeFunction("stripe-setup", this.accessToken);
|
|
347
|
+
if (!setupResult.success) {
|
|
348
|
+
throw new Error(`Setup failed: ${setupResult.error}`);
|
|
349
|
+
}
|
|
350
|
+
await this.setupPgCronJob(workerIntervalSeconds);
|
|
351
|
+
await this.updateInstallationComment(
|
|
352
|
+
`${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_INSTALLED_SUFFIX}`
|
|
353
|
+
);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
await this.updateInstallationComment(
|
|
356
|
+
`${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_ERROR_SUFFIX} - ${error instanceof Error ? error.message : String(error)}`
|
|
357
|
+
);
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
async function install(params) {
|
|
363
|
+
const {
|
|
364
|
+
supabaseAccessToken,
|
|
365
|
+
supabaseProjectRef,
|
|
366
|
+
stripeKey,
|
|
367
|
+
packageVersion,
|
|
368
|
+
workerIntervalSeconds
|
|
369
|
+
} = params;
|
|
370
|
+
const client = new SupabaseSetupClient({
|
|
371
|
+
accessToken: supabaseAccessToken,
|
|
372
|
+
projectRef: supabaseProjectRef,
|
|
373
|
+
projectBaseUrl: params.baseProjectUrl,
|
|
374
|
+
managementApiBaseUrl: params.baseManagementApiUrl
|
|
375
|
+
});
|
|
376
|
+
await client.install(stripeKey, packageVersion, workerIntervalSeconds);
|
|
377
|
+
}
|
|
378
|
+
async function uninstall(params) {
|
|
379
|
+
const { supabaseAccessToken, supabaseProjectRef } = params;
|
|
380
|
+
const client = new SupabaseSetupClient({
|
|
381
|
+
accessToken: supabaseAccessToken,
|
|
382
|
+
projectRef: supabaseProjectRef,
|
|
383
|
+
projectBaseUrl: params.baseProjectUrl,
|
|
384
|
+
managementApiBaseUrl: params.baseManagementApiBaseUrl
|
|
385
|
+
});
|
|
386
|
+
await client.uninstall();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export {
|
|
390
|
+
setupFunctionCode,
|
|
391
|
+
webhookFunctionCode,
|
|
392
|
+
workerFunctionCode,
|
|
393
|
+
STRIPE_SCHEMA_COMMENT_PREFIX,
|
|
394
|
+
INSTALLATION_STARTED_SUFFIX,
|
|
395
|
+
INSTALLATION_ERROR_SUFFIX,
|
|
396
|
+
INSTALLATION_INSTALLED_SUFFIX,
|
|
397
|
+
SupabaseSetupClient,
|
|
398
|
+
install,
|
|
399
|
+
uninstall
|
|
400
|
+
};
|
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
StripeSync,
|
|
3
3
|
createStripeWebSocketClient,
|
|
4
4
|
runMigrations
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-ENHIM76M.js";
|
|
6
6
|
import {
|
|
7
7
|
install,
|
|
8
8
|
uninstall
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-5J4LJ44K.js";
|
|
10
10
|
|
|
11
11
|
// src/cli/config.ts
|
|
12
12
|
import dotenv from "dotenv";
|
|
@@ -18,6 +18,7 @@ async function loadConfig(options) {
|
|
|
18
18
|
config.stripeApiKey = options.stripeKey || process.env.STRIPE_API_KEY || "";
|
|
19
19
|
config.ngrokAuthToken = options.ngrokToken || process.env.NGROK_AUTH_TOKEN || "";
|
|
20
20
|
config.databaseUrl = options.databaseUrl || process.env.DATABASE_URL || "";
|
|
21
|
+
config.enableSigma = options.enableSigma ?? (process.env.ENABLE_SIGMA !== void 0 ? process.env.ENABLE_SIGMA === "true" : void 0);
|
|
21
22
|
const questions = [];
|
|
22
23
|
if (!config.stripeApiKey) {
|
|
23
24
|
questions.push({
|
|
@@ -29,8 +30,8 @@ async function loadConfig(options) {
|
|
|
29
30
|
if (!input || input.trim() === "") {
|
|
30
31
|
return "Stripe API key is required";
|
|
31
32
|
}
|
|
32
|
-
if (!input.startsWith("sk_")) {
|
|
33
|
-
return 'Stripe API key should start with "sk_"';
|
|
33
|
+
if (!input.startsWith("sk_") && !input.startsWith("rk_")) {
|
|
34
|
+
return 'Stripe API key should start with "sk_" or "rk_"';
|
|
34
35
|
}
|
|
35
36
|
return true;
|
|
36
37
|
}
|
|
@@ -53,11 +54,22 @@ async function loadConfig(options) {
|
|
|
53
54
|
}
|
|
54
55
|
});
|
|
55
56
|
}
|
|
57
|
+
if (config.enableSigma === void 0) {
|
|
58
|
+
questions.push({
|
|
59
|
+
type: "confirm",
|
|
60
|
+
name: "enableSigma",
|
|
61
|
+
message: "Enable Sigma sync? (Requires Sigma access in Stripe API key)",
|
|
62
|
+
default: false
|
|
63
|
+
});
|
|
64
|
+
}
|
|
56
65
|
if (questions.length > 0) {
|
|
57
|
-
console.log(chalk.yellow("\nMissing
|
|
66
|
+
console.log(chalk.yellow("\nMissing configuration. Please provide:"));
|
|
58
67
|
const answers = await inquirer.prompt(questions);
|
|
59
68
|
Object.assign(config, answers);
|
|
60
69
|
}
|
|
70
|
+
if (config.enableSigma === void 0) {
|
|
71
|
+
config.enableSigma = false;
|
|
72
|
+
}
|
|
61
73
|
return config;
|
|
62
74
|
}
|
|
63
75
|
|
|
@@ -117,7 +129,9 @@ var VALID_SYNC_OBJECTS = [
|
|
|
117
129
|
"credit_note",
|
|
118
130
|
"early_fraud_warning",
|
|
119
131
|
"refund",
|
|
120
|
-
"checkout_sessions"
|
|
132
|
+
"checkout_sessions",
|
|
133
|
+
"subscription_item_change_events_v2_beta",
|
|
134
|
+
"exchange_rates_from_usd"
|
|
121
135
|
];
|
|
122
136
|
async function backfillCommand(options, entityName) {
|
|
123
137
|
let stripeSync = null;
|
|
@@ -146,8 +160,8 @@ async function backfillCommand(options, entityName) {
|
|
|
146
160
|
if (!input || input.trim() === "") {
|
|
147
161
|
return "Stripe API key is required";
|
|
148
162
|
}
|
|
149
|
-
if (!input.startsWith("sk_")) {
|
|
150
|
-
return 'Stripe API key should start with "sk_"';
|
|
163
|
+
if (!input.startsWith("sk_") && !input.startsWith("rk_")) {
|
|
164
|
+
return 'Stripe API key should start with "sk_" or "rk_"';
|
|
151
165
|
}
|
|
152
166
|
return true;
|
|
153
167
|
}
|
|
@@ -204,6 +218,7 @@ async function backfillCommand(options, entityName) {
|
|
|
204
218
|
stripeSync = new StripeSync({
|
|
205
219
|
databaseUrl: config.databaseUrl,
|
|
206
220
|
stripeSecretKey: config.stripeApiKey,
|
|
221
|
+
enableSigma: process.env.ENABLE_SIGMA === "true",
|
|
207
222
|
stripeApiVersion: process.env.STRIPE_API_VERSION || "2020-08-27",
|
|
208
223
|
autoExpandLists: process.env.AUTO_EXPAND_LISTS === "true",
|
|
209
224
|
backfillRelatedEntities: process.env.BACKFILL_RELATED_ENTITIES !== "false",
|
|
@@ -342,7 +357,7 @@ Cleaning up... (signal: ${signal || "manual"})`));
|
|
|
342
357
|
process.on("SIGTERM", () => cleanup("SIGTERM"));
|
|
343
358
|
try {
|
|
344
359
|
const config = await loadConfig(options);
|
|
345
|
-
const useWebSocketMode = !config.ngrokAuthToken;
|
|
360
|
+
const useWebSocketMode = process.env.USE_WEBSOCKET === "true" || !config.ngrokAuthToken;
|
|
346
361
|
const modeLabel = useWebSocketMode ? "WebSocket" : "Webhook (ngrok)";
|
|
347
362
|
console.log(chalk3.blue(`
|
|
348
363
|
Mode: ${modeLabel}`));
|
|
@@ -367,6 +382,7 @@ Mode: ${modeLabel}`));
|
|
|
367
382
|
stripeSync = new StripeSync({
|
|
368
383
|
databaseUrl: config.databaseUrl,
|
|
369
384
|
stripeSecretKey: config.stripeApiKey,
|
|
385
|
+
enableSigma: config.enableSigma,
|
|
370
386
|
stripeApiVersion: process.env.STRIPE_API_VERSION || "2020-08-27",
|
|
371
387
|
autoExpandLists: process.env.AUTO_EXPAND_LISTS === "true",
|
|
372
388
|
backfillRelatedEntities: process.env.BACKFILL_RELATED_ENTITIES !== "false",
|
|
@@ -514,7 +530,8 @@ async function installCommand(options) {
|
|
|
514
530
|
mask: "*",
|
|
515
531
|
validate: (input) => {
|
|
516
532
|
if (!input.trim()) return "Stripe key is required";
|
|
517
|
-
if (!input.startsWith("sk_")
|
|
533
|
+
if (!input.startsWith("sk_") && !input.startsWith("rk_"))
|
|
534
|
+
return 'Stripe key should start with "sk_" or "rk_"';
|
|
518
535
|
return true;
|
|
519
536
|
}
|
|
520
537
|
});
|