@paymentsdb/sync-engine 0.0.3 → 0.0.5

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.
@@ -28,6 +28,7 @@ __export(supabase_exports, {
28
28
  SupabaseSetupClient: () => SupabaseSetupClient,
29
29
  install: () => install,
30
30
  setupFunctionCode: () => setupFunctionCode,
31
+ sigmaWorkerFunctionCode: () => sigmaWorkerFunctionCode,
31
32
  uninstall: () => uninstall,
32
33
  webhookFunctionCode: () => webhookFunctionCode,
33
34
  workerFunctionCode: () => workerFunctionCode
@@ -37,24 +38,28 @@ module.exports = __toCommonJS(supabase_exports);
37
38
  // src/supabase/supabase.ts
38
39
  var import_supabase_management_js = require("supabase-management-js");
39
40
 
40
- // raw-ts:/Users/rg/startup/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
41
- var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\n// Get management API base URL from environment variable (for testing against localhost/staging)\n// Caller should provide full URL with protocol (e.g., http://localhost:54323 or https://api.supabase.com)\nconst MGMT_API_BASE_RAW = Deno.env.get('MANAGEMENT_API_URL') || 'https://api.supabase.com'\nconst MGMT_API_BASE = MGMT_API_BASE_RAW.match(/^https?:\\/\\//)\n ? MGMT_API_BASE_RAW\n : `https://${MGMT_API_BASE_RAW}`\n\n// Helper to validate accessToken against Management API\nasync function validateAccessToken(projectRef: string, accessToken: string): Promise<boolean> {\n // Try to fetch project details using the access token\n // This validates that the token is valid for the management API\n const url = `${MGMT_API_BASE}/v1/projects/${projectRef}`\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n // If we can successfully get the project, the token is valid\n return response.ok\n}\n\n// Helper to delete edge function via Management API\nasync function deleteEdgeFunction(\n projectRef: string,\n functionSlug: string,\n accessToken: string\n): Promise<void> {\n const url = `${MGMT_API_BASE}/v1/projects/${projectRef}/functions/${functionSlug}`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n throw new Error(`Failed to delete function ${functionSlug}: ${response.status} ${text}`)\n }\n}\n\n// Helper to delete secrets via Management API\nasync function deleteSecret(\n projectRef: string,\n secretName: string,\n accessToken: string\n): Promise<void> {\n const url = `${MGMT_API_BASE}/v1/projects/${projectRef}/secrets`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify([secretName]),\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n console.warn(`Failed to delete secret ${secretName}: ${response.status} ${text}`)\n }\n}\n\nDeno.serve(async (req) => {\n // Extract project ref from SUPABASE_URL (format: https://{projectRef}.{base})\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n const projectRef = new URL(supabaseUrl).hostname.split('.')[0]\n\n // Validate access token for all requests\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const accessToken = authHeader.substring(7) // Remove 'Bearer '\n const isValid = await validateAccessToken(projectRef, accessToken)\n if (!isValid) {\n return new Response('Forbidden: Invalid access token for this project', { status: 403 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle DELETE requests for uninstall\n if (req.method === 'DELETE') {\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n // Stripe key is required for uninstall to delete webhooks\n const stripeKey = Deno.env.get('STRIPE_SECRET_KEY')\n if (!stripeKey) {\n throw new Error('STRIPE_SECRET_KEY environment variable is required for uninstall')\n }\n\n // Step 1: Delete Stripe webhooks and clean up database\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 },\n stripeSecretKey: stripeKey,\n })\n\n // Delete all managed webhooks\n const webhooks = await stripeSync.listManagedWebhooks()\n for (const webhook of webhooks) {\n try {\n await stripeSync.deleteManagedWebhook(webhook.id)\n console.log(`Deleted webhook: ${webhook.id}`)\n } catch (err) {\n console.warn(`Could not delete webhook ${webhook.id}:`, err)\n }\n }\n\n // Unschedule pg_cron job\n try {\n await stripeSync.postgresClient.query(`\n DO $$\n BEGIN\n IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN\n PERFORM cron.unschedule('stripe-sync-worker');\n END IF;\n END $$;\n `)\n } catch (err) {\n console.warn('Could not unschedule pg_cron job:', err)\n }\n\n // Delete vault secret\n try {\n await stripeSync.postgresClient.query(`\n DELETE FROM vault.secrets\n WHERE name = 'stripe_sync_worker_secret'\n `)\n } catch (err) {\n console.warn('Could not delete vault secret:', err)\n }\n\n // Terminate connections holding locks on stripe schema\n try {\n await stripeSync.postgresClient.query(`\n SELECT pg_terminate_backend(pid)\n FROM pg_locks l\n JOIN pg_class c ON l.relation = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname = 'stripe'\n AND l.pid != pg_backend_pid()\n `)\n } catch (err) {\n console.warn('Could not terminate connections:', err)\n }\n\n // Drop schema with retry\n let dropAttempts = 0\n const maxAttempts = 3\n while (dropAttempts < maxAttempts) {\n try {\n await stripeSync.postgresClient.query('DROP SCHEMA IF EXISTS stripe CASCADE')\n break // Success, exit loop\n } catch (err) {\n dropAttempts++\n if (dropAttempts >= maxAttempts) {\n throw new Error(\n `Failed to drop schema after ${maxAttempts} attempts. ` +\n `There may be active connections or locks on the stripe schema. ` +\n `Error: ${err.message}`\n )\n }\n // Wait 1 second before retrying\n await new Promise((resolve) => setTimeout(resolve, 1000))\n }\n }\n\n await stripeSync.postgresClient.pool.end()\n\n // Step 2: Delete Supabase secrets\n try {\n await deleteSecret(projectRef, 'STRIPE_SECRET_KEY', accessToken)\n } catch (err) {\n console.warn('Could not delete STRIPE_SECRET_KEY secret:', err)\n }\n\n // Step 3: Delete Edge Functions\n try {\n await deleteEdgeFunction(projectRef, 'stripe-setup', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-setup function:', err)\n }\n\n try {\n await deleteEdgeFunction(projectRef, 'stripe-webhook', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-webhook function:', err)\n }\n\n try {\n await deleteEdgeFunction(projectRef, 'stripe-worker', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-worker function:', err)\n }\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Uninstall complete',\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Uninstall error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n }\n\n // Handle POST requests for install\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
41
+ // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
42
+ var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:@paymentsdb/sync-engine'\nimport postgres from 'npm:postgres'\n\n// Get management API base URL from environment variable (for testing against localhost/staging)\n// Caller should provide full URL with protocol (e.g., http://localhost:54323 or https://api.supabase.com)\nconst MGMT_API_BASE_RAW = Deno.env.get('MANAGEMENT_API_URL') || 'https://api.supabase.com'\nconst MGMT_API_BASE = MGMT_API_BASE_RAW.match(/^https?:\\/\\//)\n ? MGMT_API_BASE_RAW\n : `https://${MGMT_API_BASE_RAW}`\n\n// Helper to validate accessToken against Management API\nasync function validateAccessToken(projectRef: string, accessToken: string): Promise<boolean> {\n // Try to fetch project details using the access token\n // This validates that the token is valid for the management API\n const url = `${MGMT_API_BASE}/v1/projects/${projectRef}`\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n // If we can successfully get the project, the token is valid\n return response.ok\n}\n\n// Helper to delete edge function via Management API\nasync function deleteEdgeFunction(\n projectRef: string,\n functionSlug: string,\n accessToken: string\n): Promise<void> {\n const url = `${MGMT_API_BASE}/v1/projects/${projectRef}/functions/${functionSlug}`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n throw new Error(`Failed to delete function ${functionSlug}: ${response.status} ${text}`)\n }\n}\n\n// Helper to delete secrets via Management API\nasync function deleteSecret(\n projectRef: string,\n secretName: string,\n accessToken: string\n): Promise<void> {\n const url = `${MGMT_API_BASE}/v1/projects/${projectRef}/secrets`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify([secretName]),\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n console.warn(`Failed to delete secret ${secretName}: ${response.status} ${text}`)\n }\n}\n\nDeno.serve(async (req) => {\n // Extract project ref from SUPABASE_URL (format: https://{projectRef}.{base})\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n const projectRef = new URL(supabaseUrl).hostname.split('.')[0]\n\n // Validate access token for all requests\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const accessToken = authHeader.substring(7) // Remove 'Bearer '\n const isValid = await validateAccessToken(projectRef, accessToken)\n if (!isValid) {\n return new Response('Forbidden: Invalid access token for this project', { status: 403 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle DELETE requests for uninstall\n if (req.method === 'DELETE') {\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n // Stripe key is required for uninstall to delete webhooks\n const stripeKey = Deno.env.get('STRIPE_SECRET_KEY')\n if (!stripeKey) {\n throw new Error('STRIPE_SECRET_KEY environment variable is required for uninstall')\n }\n\n // Step 1: Delete Stripe webhooks and clean up database\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 },\n stripeSecretKey: stripeKey,\n appName: Deno.env.get('STRIPE_APP_NAME') || 'PaymentsDB',\n })\n\n // Delete all managed webhooks\n const webhooks = await stripeSync.listManagedWebhooks()\n for (const webhook of webhooks) {\n try {\n await stripeSync.deleteManagedWebhook(webhook.id)\n console.log(`Deleted webhook: ${webhook.id}`)\n } catch (err) {\n console.warn(`Could not delete webhook ${webhook.id}:`, err)\n }\n }\n\n // Unschedule pg_cron jobs\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 IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sigma-worker') THEN\n PERFORM cron.unschedule('stripe-sigma-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 secrets\n try {\n await stripeSync.postgresClient.query(`\n DELETE FROM vault.secrets\n WHERE name IN ('stripe_sync_worker_secret', 'stripe_sigma_worker_secret')\n `)\n } catch (err) {\n console.warn('Could not delete vault secret:', err)\n }\n\n // Drop Sigma self-trigger function if present\n try {\n await stripeSync.postgresClient.query(`\n DROP FUNCTION IF EXISTS stripe.trigger_sigma_worker();\n `)\n } catch (err) {\n console.warn('Could not drop sigma trigger function:', 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 const enableSigma = (Deno.env.get('ENABLE_SIGMA') ?? 'false') === 'true'\n await runMigrations({ databaseUrl: dbUrl, enableSigma })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n appName: Deno.env.get('STRIPE_APP_NAME') || 'PaymentsDB',\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
42
43
 
43
- // raw-ts:/Users/rg/startup/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
44
- 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";
44
+ // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
45
+ var stripe_webhook_default = "import { StripeSync } from 'npm:@paymentsdb/sync-engine'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const sig = req.headers.get('stripe-signature')\n if (!sig) {\n return new Response('Missing stripe-signature header', { status: 400 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n const stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n appName: Deno.env.get('STRIPE_APP_NAME') || 'PaymentsDB',\n })\n\n try {\n const rawBody = new Uint8Array(await req.arrayBuffer())\n await stripeSync.processWebhook(rawBody, sig)\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Webhook processing error:', error)\n const isSignatureError =\n error.message?.includes('signature') || error.type === 'StripeSignatureVerificationError'\n const status = isSignatureError ? 400 : 500\n return new Response(JSON.stringify({ error: error.message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n await stripeSync.postgresClient.pool.end()\n }\n})\n";
45
46
 
46
- // raw-ts:/Users/rg/startup/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
47
- 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";
47
+ // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
48
+ var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron at a configurable interval (default: 60 seconds). Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:@paymentsdb/sync-engine'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const token = authHeader.substring(7) // Remove 'Bearer '\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Validate that the token matches the unique worker secret stored in vault\n const vaultResult = await sql`\n SELECT decrypted_secret\n FROM vault.decrypted_secrets\n WHERE name = 'stripe_sync_worker_secret'\n `\n\n if (vaultResult.length === 0) {\n await sql.end()\n return new Response('Worker secret not configured in vault', { status: 500 })\n }\n\n const storedSecret = vaultResult[0].decrypted_secret\n if (token !== storedSecret) {\n await sql.end()\n return new Response('Forbidden: Invalid worker secret', { status: 403 })\n }\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n enableSigma: (Deno.env.get('ENABLE_SIGMA') ?? 'false') === 'true',\n appName: Deno.env.get('STRIPE_APP_NAME') || 'PaymentsDB',\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
49
+
50
+ // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/sigma-data-worker.ts
51
+ var sigma_data_worker_default = "/**\n * Stripe Sigma Data Worker.\n *\n * Hourly cron starts a run; self-trigger continues until all objects finish.\n * Progress persists in _sync_runs and _sync_obj_runs across invocations.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst BATCH_SIZE = 1\nconst MAX_RUN_AGE_MS = 6 * 60 * 60 * 1000\nconst jsonResponse = (body: unknown, status = 200) =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\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)\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return jsonResponse({ error: 'SUPABASE_DB_URL not set' }, 500)\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql: ReturnType<typeof postgres> | undefined\n let stripeSync: StripeSync | undefined\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return jsonResponse(\n {\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n },\n 500\n )\n }\n\n try {\n // Validate the token against vault secret\n const vaultResult = await sql`\n SELECT decrypted_secret\n FROM vault.decrypted_secrets\n WHERE name = 'stripe_sigma_worker_secret'\n `\n\n if (vaultResult.length === 0) {\n await sql.end()\n return new Response('Sigma 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 sigma 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: true,\n sigmaPageSizeOverride: 1000,\n })\n } catch (error) {\n await sql.end()\n return jsonResponse(\n {\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n },\n 500\n )\n }\n\n try {\n const accountId = await stripeSync.getAccountId()\n const sigmaObjects = stripeSync.getSupportedSigmaObjects()\n\n if (sigmaObjects.length === 0) {\n return jsonResponse({ message: 'No Sigma objects configured for sync' })\n }\n\n // Get or create sync run for sigma-worker (isolated from stripe-worker)\n const runResult = await stripeSync.postgresClient.getOrCreateSyncRun(accountId, 'sigma-worker')\n const runStartedAt =\n runResult?.runStartedAt ??\n (await stripeSync.postgresClient.getActiveSyncRun(accountId, 'sigma-worker'))?.runStartedAt\n\n if (!runStartedAt) {\n throw new Error('Failed to get or create sync run for sigma worker')\n }\n\n // Legacy cleanup: remove any prefixed sigma object runs that can block concurrency.\n // Previous versions stored objects as \"sigma.<table>\" which no longer matches processNext.\n await stripeSync.postgresClient.query(\n `UPDATE \"stripe\".\"_sync_obj_runs\"\n SET status = 'error',\n error_message = 'Legacy sigma worker prefix run (sigma.*); superseded by unprefixed runs',\n completed_at = now()\n WHERE \"_account_id\" = $1\n AND run_started_at = $2\n AND object LIKE 'sigma.%'\n AND status IN ('pending', 'running')`,\n [accountId, runStartedAt]\n )\n\n // Stop self-triggering after MAX_RUN_AGE_MS.\n const runAgeMs = Date.now() - runStartedAt.getTime()\n if (runAgeMs > MAX_RUN_AGE_MS) {\n console.warn(\n `Sigma worker: run too old (${Math.round(runAgeMs / 1000 / 60)} min), closing without self-trigger`\n )\n await stripeSync.postgresClient.closeSyncRun(accountId, runStartedAt)\n return jsonResponse({\n message: 'Sigma run exceeded max age, closed without processing',\n runAgeMinutes: Math.round(runAgeMs / 1000 / 60),\n selfTriggered: false,\n })\n }\n\n // Create object runs for all sigma objects (idempotent).\n await stripeSync.postgresClient.createObjectRuns(accountId, runStartedAt, sigmaObjects)\n await stripeSync.postgresClient.ensureSyncRunMaxConcurrent(accountId, runStartedAt, BATCH_SIZE)\n\n // Prefer running objects; otherwise claim pending ones.\n const runningObjects = await stripeSync.postgresClient.listObjectsByStatus(\n accountId,\n runStartedAt,\n 'running',\n sigmaObjects\n )\n\n const objectsToProcess = runningObjects.slice(0, BATCH_SIZE)\n let pendingObjects: string[] = []\n\n if (objectsToProcess.length === 0) {\n pendingObjects = await stripeSync.postgresClient.listObjectsByStatus(\n accountId,\n runStartedAt,\n 'pending',\n sigmaObjects\n )\n\n for (const objectKey of pendingObjects) {\n if (objectsToProcess.length >= BATCH_SIZE) break\n const started = await stripeSync.postgresClient.tryStartObjectSync(\n accountId,\n runStartedAt,\n objectKey\n )\n if (started) {\n objectsToProcess.push(objectKey)\n }\n }\n }\n\n if (objectsToProcess.length === 0) {\n if (pendingObjects.length === 0) {\n console.info('Sigma worker: all objects complete or errored - run finished')\n return jsonResponse({ message: 'Sigma sync run complete', selfTriggered: false })\n }\n\n console.info('Sigma worker: at concurrency limit, will self-trigger', {\n pendingCount: pendingObjects.length,\n })\n let selfTriggered = false\n try {\n await sql`SELECT stripe.trigger_sigma_worker()`\n selfTriggered = true\n } catch (error) {\n console.warn('Failed to self-trigger sigma worker:', error.message)\n }\n\n return jsonResponse({\n message: 'At concurrency limit',\n pendingCount: pendingObjects.length,\n selfTriggered,\n })\n }\n\n // Process objects sequentially (one lifecycle per invocation).\n const results: Array<Record<string, unknown>> = []\n\n for (const object of objectsToProcess) {\n const objectKey = object\n try {\n console.info(`Sigma worker: processing ${object}`)\n\n // Process one sigma page and upsert results.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result = await stripeSync.processNext(object as any, {\n runStartedAt,\n triggeredBy: 'sigma-worker',\n })\n\n results.push({\n object,\n processed: result.processed,\n hasMore: result.hasMore,\n status: 'success',\n })\n\n if (result.hasMore) {\n console.info(\n `Sigma worker: ${object} has more pages, processed ${result.processed} rows so far`\n )\n } else {\n console.info(`Sigma worker: ${object} complete, processed ${result.processed} rows`)\n }\n } catch (error) {\n console.error(`Sigma worker: error processing ${object}:`, error)\n\n // Mark object as failed and move on (no retries)\n await stripeSync.postgresClient.failObjectSync(\n accountId,\n runStartedAt,\n objectKey,\n error.message ?? 'Unknown error'\n )\n\n results.push({\n object,\n processed: 0,\n hasMore: false,\n status: 'error',\n error: error.message,\n })\n }\n }\n\n // Determine if self-trigger is needed\n const pendingAfter = await stripeSync.postgresClient.listObjectsByStatus(\n accountId,\n runStartedAt,\n 'pending',\n sigmaObjects\n )\n const runningAfter = await stripeSync.postgresClient.listObjectsByStatus(\n accountId,\n runStartedAt,\n 'running',\n sigmaObjects\n )\n\n // Calculate remaining run time for logging\n const remainingMs = MAX_RUN_AGE_MS - (Date.now() - runStartedAt.getTime())\n const remainingMinutes = Math.round(remainingMs / 1000 / 60)\n\n // Only self-trigger if there are pending or running objects AND run hasn't timed out\n const shouldSelfTrigger =\n (pendingAfter.length > 0 || runningAfter.length > 0) && remainingMs > 0\n\n let selfTriggered = false\n if (shouldSelfTrigger) {\n console.info('Sigma worker: more work remains, self-triggering', {\n pending: pendingAfter.length,\n running: runningAfter.length,\n remainingMinutes,\n })\n try {\n await sql`SELECT stripe.trigger_sigma_worker()`\n selfTriggered = true\n } catch (error) {\n console.warn('Failed to self-trigger sigma worker:', error.message)\n }\n } else if (pendingAfter.length > 0 || runningAfter.length > 0) {\n // Would self-trigger but run timed out\n console.warn('Sigma worker: work remains but run timed out, closing', {\n pending: pendingAfter.length,\n running: runningAfter.length,\n })\n await stripeSync.postgresClient.closeSyncRun(accountId, runStartedAt)\n } else {\n console.info('Sigma worker: no more work, run complete')\n }\n\n return jsonResponse({\n results,\n selfTriggered,\n remaining: { pending: pendingAfter.length, running: runningAfter.length },\n })\n } catch (error) {\n console.error('Sigma worker error:', error)\n return jsonResponse({ error: error.message, stack: error.stack }, 500)\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
48
52
 
49
53
  // src/supabase/edge-function-code.ts
50
54
  var setupFunctionCode = stripe_setup_default;
51
55
  var webhookFunctionCode = stripe_webhook_default;
52
56
  var workerFunctionCode = stripe_worker_default;
57
+ var sigmaWorkerFunctionCode = sigma_data_worker_default;
53
58
 
54
59
  // package.json
55
60
  var package_default = {
56
61
  name: "@paymentsdb/sync-engine",
57
- version: "0.0.2",
62
+ version: "0.0.4",
58
63
  private: false,
59
64
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
60
65
  type: "module",
@@ -84,7 +89,9 @@ var package_default = {
84
89
  prebuild: "npm run clean",
85
90
  build: "tsup src/index.ts src/supabase/index.ts src/cli/index.ts src/cli/lib.ts --format esm,cjs --dts --shims && cp -r src/database/migrations dist/migrations",
86
91
  lint: "eslint src --ext .ts",
87
- test: "vitest"
92
+ test: "vitest",
93
+ "test:integration": "TEST_POSTGRES_DB_URL=postgresql://postgres:postgres@localhost:55432/postgres vitest run src/stripeSync*integration.test.ts",
94
+ "generate:sigma-schema": "tsx src/sigma/schema/fetch-schema.ts"
88
95
  },
89
96
  files: [
90
97
  "dist"
@@ -113,9 +120,13 @@ var package_default = {
113
120
  "@types/ws": "^8.5.13",
114
121
  "@types/yesql": "^4.1.4",
115
122
  "@vitest/ui": "^4.0.9",
116
- tsx: "^4.19.2",
123
+ rimraf: "^6.0.1",
124
+ tsup: "^8.5.0",
125
+ tsx: "^4.21.0",
126
+ typescript: "^5.9.2",
117
127
  vitest: "^3.2.4"
118
128
  },
129
+ packageManager: "pnpm@10.21.0",
119
130
  repository: {
120
131
  type: "git",
121
132
  url: "https://github.com/stripe-experiments/sync-engine.git"
@@ -275,6 +286,61 @@ var SupabaseSetupClient = class {
275
286
  `;
276
287
  await this.runSQL(sql);
277
288
  }
289
+ /**
290
+ * Setup pg_cron job for Sigma data worker (every 12 hours)
291
+ * Creates secret, self-trigger function, and cron job
292
+ */
293
+ async setupSigmaPgCronJob() {
294
+ const sigmaWorkerSecret = crypto.randomUUID();
295
+ const escapedSigmaWorkerSecret = sigmaWorkerSecret.replace(/'/g, "''");
296
+ const sql = `
297
+ -- Enable extensions
298
+ CREATE EXTENSION IF NOT EXISTS pg_cron;
299
+ CREATE EXTENSION IF NOT EXISTS pg_net;
300
+
301
+ -- Store unique sigma worker secret in vault
302
+ DELETE FROM vault.secrets WHERE name = 'stripe_sigma_worker_secret';
303
+ SELECT vault.create_secret('${escapedSigmaWorkerSecret}', 'stripe_sigma_worker_secret');
304
+
305
+ -- Create self-trigger function for sigma worker continuation
306
+ -- This allows the worker to trigger itself when there's more work
307
+ CREATE OR REPLACE FUNCTION stripe.trigger_sigma_worker()
308
+ RETURNS void
309
+ LANGUAGE plpgsql
310
+ SECURITY DEFINER
311
+ AS $$
312
+ BEGIN
313
+ PERFORM net.http_post(
314
+ url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/sigma-data-worker',
315
+ headers := jsonb_build_object(
316
+ 'Authorization', 'Bearer ' || (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'stripe_sigma_worker_secret')
317
+ )
318
+ );
319
+ END;
320
+ $$;
321
+
322
+ -- Delete existing sigma job if it exists
323
+ SELECT cron.unschedule('stripe-sigma-worker') WHERE EXISTS (
324
+ SELECT 1 FROM cron.job WHERE jobname = 'stripe-sigma-worker'
325
+ );
326
+
327
+ -- Create cron job for Sigma sync
328
+ -- Runs at 00:00 and 12:00 UTC
329
+ SELECT cron.schedule(
330
+ 'stripe-sigma-worker',
331
+ '0 */12 * * *',
332
+ $$
333
+ SELECT net.http_post(
334
+ url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/sigma-data-worker',
335
+ headers := jsonb_build_object(
336
+ 'Authorization', 'Bearer ' || (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'stripe_sigma_worker_secret')
337
+ )
338
+ )
339
+ $$
340
+ );
341
+ `;
342
+ await this.runSQL(sql);
343
+ }
278
344
  /**
279
345
  * Get the webhook URL for this project
280
346
  */
@@ -441,11 +507,11 @@ var SupabaseSetupClient = class {
441
507
  return code;
442
508
  }
443
509
  return code.replace(
444
- /from ['"]npm:stripe-experiment-sync['"]/g,
445
- `from 'npm:stripe-experiment-sync@${version}'`
510
+ /from ['"]npm:@paymentsdb\/sync-engine['"]/g,
511
+ `from 'npm:@paymentsdb/sync-engine@${version}'`
446
512
  );
447
513
  }
448
- async install(stripeKey, packageVersion, workerIntervalSeconds) {
514
+ async install(stripeKey, packageVersion, workerIntervalSeconds, enableSigma) {
449
515
  const trimmedStripeKey = stripeKey.trim();
450
516
  if (!trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
451
517
  throw new Error('Stripe key should start with "sk_" or "rk_"');
@@ -463,16 +529,26 @@ var SupabaseSetupClient = class {
463
529
  await this.deployFunction("stripe-setup", versionedSetup, false);
464
530
  await this.deployFunction("stripe-webhook", versionedWebhook, false);
465
531
  await this.deployFunction("stripe-worker", versionedWorker, false);
532
+ if (enableSigma) {
533
+ const versionedSigmaWorker = this.injectPackageVersion(sigmaWorkerFunctionCode, version);
534
+ await this.deployFunction("sigma-data-worker", versionedSigmaWorker, false);
535
+ }
466
536
  const secrets = [{ name: "STRIPE_SECRET_KEY", value: trimmedStripeKey }];
467
537
  if (this.supabaseManagementUrl) {
468
538
  secrets.push({ name: "MANAGEMENT_API_URL", value: this.supabaseManagementUrl });
469
539
  }
540
+ if (enableSigma) {
541
+ secrets.push({ name: "ENABLE_SIGMA", value: "true" });
542
+ }
470
543
  await this.setSecrets(secrets);
471
544
  const setupResult = await this.invokeFunction("stripe-setup", this.accessToken);
472
545
  if (!setupResult.success) {
473
546
  throw new Error(`Setup failed: ${setupResult.error}`);
474
547
  }
475
548
  await this.setupPgCronJob(workerIntervalSeconds);
549
+ if (enableSigma) {
550
+ await this.setupSigmaPgCronJob();
551
+ }
476
552
  await this.updateInstallationComment(
477
553
  `${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_INSTALLED_SUFFIX}`
478
554
  );
@@ -490,7 +566,8 @@ async function install(params) {
490
566
  supabaseProjectRef,
491
567
  stripeKey,
492
568
  packageVersion,
493
- workerIntervalSeconds
569
+ workerIntervalSeconds,
570
+ enableSigma
494
571
  } = params;
495
572
  const client = new SupabaseSetupClient({
496
573
  accessToken: supabaseAccessToken,
@@ -498,7 +575,7 @@ async function install(params) {
498
575
  projectBaseUrl: params.baseProjectUrl,
499
576
  supabaseManagementUrl: params.supabaseManagementUrl
500
577
  });
501
- await client.install(stripeKey, packageVersion, workerIntervalSeconds);
578
+ await client.install(stripeKey, packageVersion, workerIntervalSeconds, enableSigma);
502
579
  }
503
580
  async function uninstall(params) {
504
581
  const { supabaseAccessToken, supabaseProjectRef } = params;
@@ -520,6 +597,7 @@ async function uninstall(params) {
520
597
  SupabaseSetupClient,
521
598
  install,
522
599
  setupFunctionCode,
600
+ sigmaWorkerFunctionCode,
523
601
  uninstall,
524
602
  webhookFunctionCode,
525
603
  workerFunctionCode
@@ -44,6 +44,11 @@ declare class SupabaseSetupClient {
44
44
  * @param intervalSeconds - How often to run the worker (default: 60 seconds)
45
45
  */
46
46
  setupPgCronJob(intervalSeconds?: number): Promise<void>;
47
+ /**
48
+ * Setup pg_cron job for Sigma data worker (every 12 hours)
49
+ * Creates secret, self-trigger function, and cron job
50
+ */
51
+ setupSigmaPgCronJob(): Promise<void>;
47
52
  /**
48
53
  * Get the webhook URL for this project
49
54
  */
@@ -96,7 +101,7 @@ declare class SupabaseSetupClient {
96
101
  * Inject package version into Edge Function code
97
102
  */
98
103
  private injectPackageVersion;
99
- install(stripeKey: string, packageVersion?: string, workerIntervalSeconds?: number): Promise<void>;
104
+ install(stripeKey: string, packageVersion?: string, workerIntervalSeconds?: number, enableSigma?: boolean): Promise<void>;
100
105
  }
101
106
  declare function install(params: {
102
107
  supabaseAccessToken: string;
@@ -106,6 +111,7 @@ declare function install(params: {
106
111
  workerIntervalSeconds?: number;
107
112
  baseProjectUrl?: string;
108
113
  supabaseManagementUrl?: string;
114
+ enableSigma?: boolean;
109
115
  }): Promise<void>;
110
116
  declare function uninstall(params: {
111
117
  supabaseAccessToken: string;
@@ -117,5 +123,6 @@ declare function uninstall(params: {
117
123
  declare const setupFunctionCode: string;
118
124
  declare const webhookFunctionCode: string;
119
125
  declare const workerFunctionCode: string;
126
+ declare const sigmaWorkerFunctionCode: string;
120
127
 
121
- export { type DeployClientOptions, INSTALLATION_ERROR_SUFFIX, INSTALLATION_INSTALLED_SUFFIX, INSTALLATION_STARTED_SUFFIX, type ProjectInfo, STRIPE_SCHEMA_COMMENT_PREFIX, SupabaseSetupClient as SupabaseDeployClient, SupabaseSetupClient, install, setupFunctionCode, uninstall, webhookFunctionCode, workerFunctionCode };
128
+ export { type DeployClientOptions, INSTALLATION_ERROR_SUFFIX, INSTALLATION_INSTALLED_SUFFIX, INSTALLATION_STARTED_SUFFIX, type ProjectInfo, STRIPE_SCHEMA_COMMENT_PREFIX, SupabaseSetupClient as SupabaseDeployClient, SupabaseSetupClient, install, setupFunctionCode, sigmaWorkerFunctionCode, uninstall, webhookFunctionCode, workerFunctionCode };
@@ -44,6 +44,11 @@ declare class SupabaseSetupClient {
44
44
  * @param intervalSeconds - How often to run the worker (default: 60 seconds)
45
45
  */
46
46
  setupPgCronJob(intervalSeconds?: number): Promise<void>;
47
+ /**
48
+ * Setup pg_cron job for Sigma data worker (every 12 hours)
49
+ * Creates secret, self-trigger function, and cron job
50
+ */
51
+ setupSigmaPgCronJob(): Promise<void>;
47
52
  /**
48
53
  * Get the webhook URL for this project
49
54
  */
@@ -96,7 +101,7 @@ declare class SupabaseSetupClient {
96
101
  * Inject package version into Edge Function code
97
102
  */
98
103
  private injectPackageVersion;
99
- install(stripeKey: string, packageVersion?: string, workerIntervalSeconds?: number): Promise<void>;
104
+ install(stripeKey: string, packageVersion?: string, workerIntervalSeconds?: number, enableSigma?: boolean): Promise<void>;
100
105
  }
101
106
  declare function install(params: {
102
107
  supabaseAccessToken: string;
@@ -106,6 +111,7 @@ declare function install(params: {
106
111
  workerIntervalSeconds?: number;
107
112
  baseProjectUrl?: string;
108
113
  supabaseManagementUrl?: string;
114
+ enableSigma?: boolean;
109
115
  }): Promise<void>;
110
116
  declare function uninstall(params: {
111
117
  supabaseAccessToken: string;
@@ -117,5 +123,6 @@ declare function uninstall(params: {
117
123
  declare const setupFunctionCode: string;
118
124
  declare const webhookFunctionCode: string;
119
125
  declare const workerFunctionCode: string;
126
+ declare const sigmaWorkerFunctionCode: string;
120
127
 
121
- export { type DeployClientOptions, INSTALLATION_ERROR_SUFFIX, INSTALLATION_INSTALLED_SUFFIX, INSTALLATION_STARTED_SUFFIX, type ProjectInfo, STRIPE_SCHEMA_COMMENT_PREFIX, SupabaseSetupClient as SupabaseDeployClient, SupabaseSetupClient, install, setupFunctionCode, uninstall, webhookFunctionCode, workerFunctionCode };
128
+ export { type DeployClientOptions, INSTALLATION_ERROR_SUFFIX, INSTALLATION_INSTALLED_SUFFIX, INSTALLATION_STARTED_SUFFIX, type ProjectInfo, STRIPE_SCHEMA_COMMENT_PREFIX, SupabaseSetupClient as SupabaseDeployClient, SupabaseSetupClient, install, setupFunctionCode, sigmaWorkerFunctionCode, uninstall, webhookFunctionCode, workerFunctionCode };
@@ -6,11 +6,12 @@ import {
6
6
  SupabaseSetupClient,
7
7
  install,
8
8
  setupFunctionCode,
9
+ sigmaWorkerFunctionCode,
9
10
  uninstall,
10
11
  webhookFunctionCode,
11
12
  workerFunctionCode
12
- } from "../chunk-4DX3CQZ6.js";
13
- import "../chunk-ATMQZPFA.js";
13
+ } from "../chunk-Q3AGYUHN.js";
14
+ import "../chunk-HXSDJSKR.js";
14
15
  export {
15
16
  INSTALLATION_ERROR_SUFFIX,
16
17
  INSTALLATION_INSTALLED_SUFFIX,
@@ -20,6 +21,7 @@ export {
20
21
  SupabaseSetupClient,
21
22
  install,
22
23
  setupFunctionCode,
24
+ sigmaWorkerFunctionCode,
23
25
  uninstall,
24
26
  webhookFunctionCode,
25
27
  workerFunctionCode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paymentsdb/sync-engine",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "private": false,
5
5
  "description": "Stripe Sync Engine to sync Stripe data to Postgres",
6
6
  "type": "module",
@@ -30,7 +30,9 @@
30
30
  "prebuild": "npm run clean",
31
31
  "build": "tsup src/index.ts src/supabase/index.ts src/cli/index.ts src/cli/lib.ts --format esm,cjs --dts --shims && cp -r src/database/migrations dist/migrations",
32
32
  "lint": "eslint src --ext .ts",
33
- "test": "vitest"
33
+ "test": "vitest",
34
+ "test:integration": "TEST_POSTGRES_DB_URL=postgresql://postgres:postgres@localhost:55432/postgres vitest run src/stripeSync*integration.test.ts",
35
+ "generate:sigma-schema": "tsx src/sigma/schema/fetch-schema.ts"
34
36
  },
35
37
  "files": [
36
38
  "dist"
@@ -59,9 +61,13 @@
59
61
  "@types/ws": "^8.5.13",
60
62
  "@types/yesql": "^4.1.4",
61
63
  "@vitest/ui": "^4.0.9",
62
- "tsx": "^4.19.2",
64
+ "rimraf": "^6.0.1",
65
+ "tsup": "^8.5.0",
66
+ "tsx": "^4.21.0",
67
+ "typescript": "^5.9.2",
63
68
  "vitest": "^3.2.4"
64
69
  },
70
+ "packageManager": "pnpm@10.21.0",
65
71
  "repository": {
66
72
  "type": "git",
67
73
  "url": "https://github.com/stripe-experiments/sync-engine.git"