@paymentsdb/sync-engine 0.0.7 → 0.0.9

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  package_default
3
- } from "./chunk-7DYBM7H3.js";
3
+ } from "./chunk-CVZHIBXG.js";
4
4
 
5
5
  // src/stripeSync.ts
6
6
  import Stripe3 from "stripe";
@@ -45213,7 +45213,7 @@ var StripeSync = class {
45213
45213
  subscription: {
45214
45214
  order: 5,
45215
45215
  // Depends on customer, price
45216
- listFn: (p) => this.stripe.subscriptions.list(p),
45216
+ listFn: (p) => this.stripe.subscriptions.list({ ...p, status: "all" }),
45217
45217
  upsertFn: (items, id, bf) => this.upsertSubscriptions(items, id, bf),
45218
45218
  supportsCreatedFilter: true
45219
45219
  },
@@ -46303,6 +46303,26 @@ ${message}`;
46303
46303
  batchSize
46304
46304
  );
46305
46305
  if (customerBatch.length === 0) {
46306
+ const customerRun = await this.postgresClient.getObjectRun(
46307
+ accountId,
46308
+ runStartedAt,
46309
+ "customers"
46310
+ );
46311
+ if (!customerRun || customerRun.status === "pending" || customerRun.status === "running") {
46312
+ this.config.logger?.info(
46313
+ "payment_methods: waiting for customers to finish syncing, will retry"
46314
+ );
46315
+ return { processed: 0, hasMore: true, runStartedAt };
46316
+ }
46317
+ if (customerRun.status === "error") {
46318
+ await this.postgresClient.failObjectSync(
46319
+ accountId,
46320
+ runStartedAt,
46321
+ resourceName,
46322
+ "Dependency failed: customers sync errored"
46323
+ );
46324
+ return { processed: 0, hasMore: false, runStartedAt };
46325
+ }
46306
46326
  await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
46307
46327
  return { processed: 0, hasMore: false, runStartedAt };
46308
46328
  }
@@ -3,11 +3,11 @@ import {
3
3
  StripeSync,
4
4
  createStripeWebSocketClient,
5
5
  runMigrations
6
- } from "./chunk-IQ64IUIL.js";
6
+ } from "./chunk-2N7737DI.js";
7
7
  import {
8
8
  install,
9
9
  uninstall
10
- } from "./chunk-OLHHINPH.js";
10
+ } from "./chunk-5OQVNBPL.js";
11
11
 
12
12
  // src/cli/config.ts
13
13
  import dotenv from "dotenv";
@@ -1,18 +1,18 @@
1
1
  import {
2
2
  package_default
3
- } from "./chunk-7DYBM7H3.js";
3
+ } from "./chunk-CVZHIBXG.js";
4
4
 
5
5
  // src/supabase/supabase.ts
6
6
  import { SupabaseManagementAPI } from "supabase-management-js";
7
7
 
8
8
  // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
9
- 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";
9
+ 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 // Drop worker self-trigger function if present\n try {\n await stripeSync.postgresClient.query(`\n DROP FUNCTION IF EXISTS stripe.trigger_worker();\n `)\n } catch (err) {\n console.warn('Could not drop worker 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";
10
10
 
11
11
  // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
12
12
  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";
13
13
 
14
14
  // raw-ts:/Users/prasoon/work/paymentsdb-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:@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";
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), then self-trigger\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Self-trigger if work was re-enqueued (with progress) and run isn't too old\n * 5. 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 * Self-triggering:\n * - Accelerates initial sync by not waiting for next cron tick.\n * - Only triggers when work was re-enqueued AND progress was made.\n * - Stops after MAX_RUN_AGE_MS to prevent runaway loops.\n * - Cron remains as fallback for errors/timeouts.\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\nconst MAX_RUN_AGE_MS = 6 * 60 * 60 * 1000 // 6 hours - stop self-triggering after this\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 { runKey, 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 // Self-trigger to start processing immediately (don't wait for cron)\n // But only if run isn't too old (prevents runaway on stale runs)\n let selfTriggered = false\n if (objects.length > 0) {\n const runAgeMs = Date.now() - runKey.runStartedAt.getTime()\n if (runAgeMs <= MAX_RUN_AGE_MS) {\n try {\n await sql`SELECT stripe.trigger_worker()`\n selfTriggered = true\n } catch (err) {\n console.warn('Failed to self-trigger after enqueue:', err)\n }\n } else {\n console.warn(\n `Worker: run too old (${Math.round(runAgeMs / 1000 / 60)} min), not self-triggering after enqueue`\n )\n }\n }\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects, selfTriggered }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n // Pass triggeredBy: 'worker' so all processing uses the same run as joinOrCreateSyncRun('worker')\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, { triggeredBy: 'worker' })\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, reenqueued: result.hasMore }\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 reenqueued: false,\n runStartedAt: null,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n // Self-trigger if work was re-enqueued with progress, and run isn't too old\n let selfTriggered = false\n const successfulResults = results.filter((r) => r.reenqueued && r.processed > 0 && !r.error)\n\n if (successfulResults.length > 0) {\n // Use runStartedAt from the first successful result (all use same run)\n const runStartedAt = successfulResults[0].runStartedAt\n if (runStartedAt) {\n const runAgeMs = Date.now() - runStartedAt.getTime()\n if (runAgeMs <= MAX_RUN_AGE_MS) {\n try {\n await sql`SELECT stripe.trigger_worker()`\n selfTriggered = true\n } catch (err) {\n console.warn('Failed to self-trigger after processing:', err)\n }\n } else {\n console.warn(\n `Worker: run too old (${Math.round(runAgeMs / 1000 / 60)} min), not self-triggering`\n )\n }\n }\n }\n\n return new Response(JSON.stringify({ results, selfTriggered }), {\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
16
 
17
17
  // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/sigma-data-worker.ts
18
18
  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";
@@ -143,6 +143,23 @@ var SupabaseSetupClient = class {
143
143
  SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-scheduler'
144
144
  );
145
145
 
146
+ -- Create self-trigger function for worker continuation
147
+ -- This allows the worker to trigger itself when there's more work
148
+ CREATE OR REPLACE FUNCTION stripe.trigger_worker()
149
+ RETURNS void
150
+ LANGUAGE plpgsql
151
+ SECURITY DEFINER
152
+ AS $$
153
+ BEGIN
154
+ PERFORM net.http_post(
155
+ url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
156
+ headers := jsonb_build_object(
157
+ 'Authorization', 'Bearer ' || (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'stripe_sync_worker_secret')
158
+ )
159
+ );
160
+ END;
161
+ $$;
162
+
146
163
  -- Create job to invoke worker at configured interval
147
164
  -- Worker reads from pgmq, enqueues objects if empty, and processes sync work
148
165
  SELECT cron.schedule(
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "@paymentsdb/sync-engine",
4
- version: "0.0.7",
4
+ version: "0.0.9",
5
5
  private: false,
6
6
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
7
7
  type: "module",
@@ -33,7 +33,7 @@ var import_commander = require("commander");
33
33
  // package.json
34
34
  var package_default = {
35
35
  name: "@paymentsdb/sync-engine",
36
- version: "0.0.7",
36
+ version: "0.0.9",
37
37
  private: false,
38
38
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
39
39
  type: "module",
@@ -45404,7 +45404,7 @@ var StripeSync = class {
45404
45404
  subscription: {
45405
45405
  order: 5,
45406
45406
  // Depends on customer, price
45407
- listFn: (p) => this.stripe.subscriptions.list(p),
45407
+ listFn: (p) => this.stripe.subscriptions.list({ ...p, status: "all" }),
45408
45408
  upsertFn: (items, id, bf) => this.upsertSubscriptions(items, id, bf),
45409
45409
  supportsCreatedFilter: true
45410
45410
  },
@@ -46494,6 +46494,26 @@ ${message}`;
46494
46494
  batchSize
46495
46495
  );
46496
46496
  if (customerBatch.length === 0) {
46497
+ const customerRun = await this.postgresClient.getObjectRun(
46498
+ accountId,
46499
+ runStartedAt,
46500
+ "customers"
46501
+ );
46502
+ if (!customerRun || customerRun.status === "pending" || customerRun.status === "running") {
46503
+ this.config.logger?.info(
46504
+ "payment_methods: waiting for customers to finish syncing, will retry"
46505
+ );
46506
+ return { processed: 0, hasMore: true, runStartedAt };
46507
+ }
46508
+ if (customerRun.status === "error") {
46509
+ await this.postgresClient.failObjectSync(
46510
+ accountId,
46511
+ runStartedAt,
46512
+ resourceName,
46513
+ "Dependency failed: customers sync errored"
46514
+ );
46515
+ return { processed: 0, hasMore: false, runStartedAt };
46516
+ }
46497
46517
  await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
46498
46518
  return { processed: 0, hasMore: false, runStartedAt };
46499
46519
  }
@@ -49101,13 +49121,13 @@ Creating ngrok tunnel for port ${port}...`));
49101
49121
  var import_supabase_management_js = require("supabase-management-js");
49102
49122
 
49103
49123
  // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
49104
- 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";
49124
+ 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 // Drop worker self-trigger function if present\n try {\n await stripeSync.postgresClient.query(`\n DROP FUNCTION IF EXISTS stripe.trigger_worker();\n `)\n } catch (err) {\n console.warn('Could not drop worker 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";
49105
49125
 
49106
49126
  // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
49107
49127
  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";
49108
49128
 
49109
49129
  // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
49110
- 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";
49130
+ 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), then self-trigger\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Self-trigger if work was re-enqueued (with progress) and run isn't too old\n * 5. 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 * Self-triggering:\n * - Accelerates initial sync by not waiting for next cron tick.\n * - Only triggers when work was re-enqueued AND progress was made.\n * - Stops after MAX_RUN_AGE_MS to prevent runaway loops.\n * - Cron remains as fallback for errors/timeouts.\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\nconst MAX_RUN_AGE_MS = 6 * 60 * 60 * 1000 // 6 hours - stop self-triggering after this\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 { runKey, 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 // Self-trigger to start processing immediately (don't wait for cron)\n // But only if run isn't too old (prevents runaway on stale runs)\n let selfTriggered = false\n if (objects.length > 0) {\n const runAgeMs = Date.now() - runKey.runStartedAt.getTime()\n if (runAgeMs <= MAX_RUN_AGE_MS) {\n try {\n await sql`SELECT stripe.trigger_worker()`\n selfTriggered = true\n } catch (err) {\n console.warn('Failed to self-trigger after enqueue:', err)\n }\n } else {\n console.warn(\n `Worker: run too old (${Math.round(runAgeMs / 1000 / 60)} min), not self-triggering after enqueue`\n )\n }\n }\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects, selfTriggered }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n // Pass triggeredBy: 'worker' so all processing uses the same run as joinOrCreateSyncRun('worker')\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, { triggeredBy: 'worker' })\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, reenqueued: result.hasMore }\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 reenqueued: false,\n runStartedAt: null,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n // Self-trigger if work was re-enqueued with progress, and run isn't too old\n let selfTriggered = false\n const successfulResults = results.filter((r) => r.reenqueued && r.processed > 0 && !r.error)\n\n if (successfulResults.length > 0) {\n // Use runStartedAt from the first successful result (all use same run)\n const runStartedAt = successfulResults[0].runStartedAt\n if (runStartedAt) {\n const runAgeMs = Date.now() - runStartedAt.getTime()\n if (runAgeMs <= MAX_RUN_AGE_MS) {\n try {\n await sql`SELECT stripe.trigger_worker()`\n selfTriggered = true\n } catch (err) {\n console.warn('Failed to self-trigger after processing:', err)\n }\n } else {\n console.warn(\n `Worker: run too old (${Math.round(runAgeMs / 1000 / 60)} min), not self-triggering`\n )\n }\n }\n }\n\n return new Response(JSON.stringify({ results, selfTriggered }), {\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";
49111
49131
 
49112
49132
  // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/sigma-data-worker.ts
49113
49133
  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";
@@ -49238,6 +49258,23 @@ var SupabaseSetupClient = class {
49238
49258
  SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-scheduler'
49239
49259
  );
49240
49260
 
49261
+ -- Create self-trigger function for worker continuation
49262
+ -- This allows the worker to trigger itself when there's more work
49263
+ CREATE OR REPLACE FUNCTION stripe.trigger_worker()
49264
+ RETURNS void
49265
+ LANGUAGE plpgsql
49266
+ SECURITY DEFINER
49267
+ AS $$
49268
+ BEGIN
49269
+ PERFORM net.http_post(
49270
+ url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
49271
+ headers := jsonb_build_object(
49272
+ 'Authorization', 'Bearer ' || (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'stripe_sync_worker_secret')
49273
+ )
49274
+ );
49275
+ END;
49276
+ $$;
49277
+
49241
49278
  -- Create job to invoke worker at configured interval
49242
49279
  -- Worker reads from pgmq, enqueues objects if empty, and processes sync work
49243
49280
  SELECT cron.schedule(
package/dist/cli/index.js CHANGED
@@ -5,12 +5,12 @@ import {
5
5
  migrateCommand,
6
6
  syncCommand,
7
7
  uninstallCommand
8
- } from "../chunk-LN6KHV6O.js";
9
- import "../chunk-IQ64IUIL.js";
10
- import "../chunk-OLHHINPH.js";
8
+ } from "../chunk-52SZMUB6.js";
9
+ import "../chunk-2N7737DI.js";
10
+ import "../chunk-5OQVNBPL.js";
11
11
  import {
12
12
  package_default
13
- } from "../chunk-7DYBM7H3.js";
13
+ } from "../chunk-CVZHIBXG.js";
14
14
 
15
15
  // src/cli/index.ts
16
16
  import { Command } from "commander";
package/dist/cli/lib.cjs CHANGED
@@ -117,7 +117,7 @@ async function loadConfig(options) {
117
117
  // package.json
118
118
  var package_default = {
119
119
  name: "@paymentsdb/sync-engine",
120
- version: "0.0.7",
120
+ version: "0.0.9",
121
121
  private: false,
122
122
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
123
123
  type: "module",
@@ -45418,7 +45418,7 @@ var StripeSync = class {
45418
45418
  subscription: {
45419
45419
  order: 5,
45420
45420
  // Depends on customer, price
45421
- listFn: (p) => this.stripe.subscriptions.list(p),
45421
+ listFn: (p) => this.stripe.subscriptions.list({ ...p, status: "all" }),
45422
45422
  upsertFn: (items, id, bf) => this.upsertSubscriptions(items, id, bf),
45423
45423
  supportsCreatedFilter: true
45424
45424
  },
@@ -46508,6 +46508,26 @@ ${message}`;
46508
46508
  batchSize
46509
46509
  );
46510
46510
  if (customerBatch.length === 0) {
46511
+ const customerRun = await this.postgresClient.getObjectRun(
46512
+ accountId,
46513
+ runStartedAt,
46514
+ "customers"
46515
+ );
46516
+ if (!customerRun || customerRun.status === "pending" || customerRun.status === "running") {
46517
+ this.config.logger?.info(
46518
+ "payment_methods: waiting for customers to finish syncing, will retry"
46519
+ );
46520
+ return { processed: 0, hasMore: true, runStartedAt };
46521
+ }
46522
+ if (customerRun.status === "error") {
46523
+ await this.postgresClient.failObjectSync(
46524
+ accountId,
46525
+ runStartedAt,
46526
+ resourceName,
46527
+ "Dependency failed: customers sync errored"
46528
+ );
46529
+ return { processed: 0, hasMore: false, runStartedAt };
46530
+ }
46511
46531
  await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
46512
46532
  return { processed: 0, hasMore: false, runStartedAt };
46513
46533
  }
@@ -49115,13 +49135,13 @@ Creating ngrok tunnel for port ${port}...`));
49115
49135
  var import_supabase_management_js = require("supabase-management-js");
49116
49136
 
49117
49137
  // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
49118
- 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";
49138
+ 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 // Drop worker self-trigger function if present\n try {\n await stripeSync.postgresClient.query(`\n DROP FUNCTION IF EXISTS stripe.trigger_worker();\n `)\n } catch (err) {\n console.warn('Could not drop worker 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";
49119
49139
 
49120
49140
  // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
49121
49141
  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";
49122
49142
 
49123
49143
  // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
49124
- 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";
49144
+ 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), then self-trigger\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Self-trigger if work was re-enqueued (with progress) and run isn't too old\n * 5. 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 * Self-triggering:\n * - Accelerates initial sync by not waiting for next cron tick.\n * - Only triggers when work was re-enqueued AND progress was made.\n * - Stops after MAX_RUN_AGE_MS to prevent runaway loops.\n * - Cron remains as fallback for errors/timeouts.\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\nconst MAX_RUN_AGE_MS = 6 * 60 * 60 * 1000 // 6 hours - stop self-triggering after this\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 { runKey, 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 // Self-trigger to start processing immediately (don't wait for cron)\n // But only if run isn't too old (prevents runaway on stale runs)\n let selfTriggered = false\n if (objects.length > 0) {\n const runAgeMs = Date.now() - runKey.runStartedAt.getTime()\n if (runAgeMs <= MAX_RUN_AGE_MS) {\n try {\n await sql`SELECT stripe.trigger_worker()`\n selfTriggered = true\n } catch (err) {\n console.warn('Failed to self-trigger after enqueue:', err)\n }\n } else {\n console.warn(\n `Worker: run too old (${Math.round(runAgeMs / 1000 / 60)} min), not self-triggering after enqueue`\n )\n }\n }\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects, selfTriggered }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n // Pass triggeredBy: 'worker' so all processing uses the same run as joinOrCreateSyncRun('worker')\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, { triggeredBy: 'worker' })\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, reenqueued: result.hasMore }\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 reenqueued: false,\n runStartedAt: null,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n // Self-trigger if work was re-enqueued with progress, and run isn't too old\n let selfTriggered = false\n const successfulResults = results.filter((r) => r.reenqueued && r.processed > 0 && !r.error)\n\n if (successfulResults.length > 0) {\n // Use runStartedAt from the first successful result (all use same run)\n const runStartedAt = successfulResults[0].runStartedAt\n if (runStartedAt) {\n const runAgeMs = Date.now() - runStartedAt.getTime()\n if (runAgeMs <= MAX_RUN_AGE_MS) {\n try {\n await sql`SELECT stripe.trigger_worker()`\n selfTriggered = true\n } catch (err) {\n console.warn('Failed to self-trigger after processing:', err)\n }\n } else {\n console.warn(\n `Worker: run too old (${Math.round(runAgeMs / 1000 / 60)} min), not self-triggering`\n )\n }\n }\n }\n\n return new Response(JSON.stringify({ results, selfTriggered }), {\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";
49125
49145
 
49126
49146
  // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/sigma-data-worker.ts
49127
49147
  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";
@@ -49252,6 +49272,23 @@ var SupabaseSetupClient = class {
49252
49272
  SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-scheduler'
49253
49273
  );
49254
49274
 
49275
+ -- Create self-trigger function for worker continuation
49276
+ -- This allows the worker to trigger itself when there's more work
49277
+ CREATE OR REPLACE FUNCTION stripe.trigger_worker()
49278
+ RETURNS void
49279
+ LANGUAGE plpgsql
49280
+ SECURITY DEFINER
49281
+ AS $$
49282
+ BEGIN
49283
+ PERFORM net.http_post(
49284
+ url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
49285
+ headers := jsonb_build_object(
49286
+ 'Authorization', 'Bearer ' || (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'stripe_sync_worker_secret')
49287
+ )
49288
+ );
49289
+ END;
49290
+ $$;
49291
+
49255
49292
  -- Create job to invoke worker at configured interval
49256
49293
  -- Worker reads from pgmq, enqueues objects if empty, and processes sync work
49257
49294
  SELECT cron.schedule(
package/dist/cli/lib.js CHANGED
@@ -6,10 +6,10 @@ import {
6
6
  migrateCommand,
7
7
  syncCommand,
8
8
  uninstallCommand
9
- } from "../chunk-LN6KHV6O.js";
10
- import "../chunk-IQ64IUIL.js";
11
- import "../chunk-OLHHINPH.js";
12
- import "../chunk-7DYBM7H3.js";
9
+ } from "../chunk-52SZMUB6.js";
10
+ import "../chunk-2N7737DI.js";
11
+ import "../chunk-5OQVNBPL.js";
12
+ import "../chunk-CVZHIBXG.js";
13
13
  export {
14
14
  backfillCommand,
15
15
  createTunnel,
package/dist/index.cjs CHANGED
@@ -46,7 +46,7 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
46
46
  // package.json
47
47
  var package_default = {
48
48
  name: "@paymentsdb/sync-engine",
49
- version: "0.0.7",
49
+ version: "0.0.9",
50
50
  private: false,
51
51
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
52
52
  type: "module",
@@ -45347,7 +45347,7 @@ var StripeSync = class {
45347
45347
  subscription: {
45348
45348
  order: 5,
45349
45349
  // Depends on customer, price
45350
- listFn: (p) => this.stripe.subscriptions.list(p),
45350
+ listFn: (p) => this.stripe.subscriptions.list({ ...p, status: "all" }),
45351
45351
  upsertFn: (items, id, bf) => this.upsertSubscriptions(items, id, bf),
45352
45352
  supportsCreatedFilter: true
45353
45353
  },
@@ -46437,6 +46437,26 @@ ${message}`;
46437
46437
  batchSize
46438
46438
  );
46439
46439
  if (customerBatch.length === 0) {
46440
+ const customerRun = await this.postgresClient.getObjectRun(
46441
+ accountId,
46442
+ runStartedAt,
46443
+ "customers"
46444
+ );
46445
+ if (!customerRun || customerRun.status === "pending" || customerRun.status === "running") {
46446
+ this.config.logger?.info(
46447
+ "payment_methods: waiting for customers to finish syncing, will retry"
46448
+ );
46449
+ return { processed: 0, hasMore: true, runStartedAt };
46450
+ }
46451
+ if (customerRun.status === "error") {
46452
+ await this.postgresClient.failObjectSync(
46453
+ accountId,
46454
+ runStartedAt,
46455
+ resourceName,
46456
+ "Dependency failed: customers sync errored"
46457
+ );
46458
+ return { processed: 0, hasMore: false, runStartedAt };
46459
+ }
46440
46460
  await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
46441
46461
  return { processed: 0, hasMore: false, runStartedAt };
46442
46462
  }
package/dist/index.js CHANGED
@@ -5,8 +5,8 @@ import {
5
5
  createStripeWebSocketClient,
6
6
  hashApiKey,
7
7
  runMigrations
8
- } from "./chunk-IQ64IUIL.js";
9
- import "./chunk-7DYBM7H3.js";
8
+ } from "./chunk-2N7737DI.js";
9
+ import "./chunk-CVZHIBXG.js";
10
10
  export {
11
11
  PostgresClient,
12
12
  StripeSync,
@@ -39,13 +39,13 @@ module.exports = __toCommonJS(supabase_exports);
39
39
  var import_supabase_management_js = require("supabase-management-js");
40
40
 
41
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
+ 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 // Drop worker self-trigger function if present\n try {\n await stripeSync.postgresClient.query(`\n DROP FUNCTION IF EXISTS stripe.trigger_worker();\n `)\n } catch (err) {\n console.warn('Could not drop worker 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";
43
43
 
44
44
  // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
45
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";
46
46
 
47
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";
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), then self-trigger\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Self-trigger if work was re-enqueued (with progress) and run isn't too old\n * 5. 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 * Self-triggering:\n * - Accelerates initial sync by not waiting for next cron tick.\n * - Only triggers when work was re-enqueued AND progress was made.\n * - Stops after MAX_RUN_AGE_MS to prevent runaway loops.\n * - Cron remains as fallback for errors/timeouts.\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\nconst MAX_RUN_AGE_MS = 6 * 60 * 60 * 1000 // 6 hours - stop self-triggering after this\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 { runKey, 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 // Self-trigger to start processing immediately (don't wait for cron)\n // But only if run isn't too old (prevents runaway on stale runs)\n let selfTriggered = false\n if (objects.length > 0) {\n const runAgeMs = Date.now() - runKey.runStartedAt.getTime()\n if (runAgeMs <= MAX_RUN_AGE_MS) {\n try {\n await sql`SELECT stripe.trigger_worker()`\n selfTriggered = true\n } catch (err) {\n console.warn('Failed to self-trigger after enqueue:', err)\n }\n } else {\n console.warn(\n `Worker: run too old (${Math.round(runAgeMs / 1000 / 60)} min), not self-triggering after enqueue`\n )\n }\n }\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects, selfTriggered }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n // Pass triggeredBy: 'worker' so all processing uses the same run as joinOrCreateSyncRun('worker')\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, { triggeredBy: 'worker' })\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, reenqueued: result.hasMore }\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 reenqueued: false,\n runStartedAt: null,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n // Self-trigger if work was re-enqueued with progress, and run isn't too old\n let selfTriggered = false\n const successfulResults = results.filter((r) => r.reenqueued && r.processed > 0 && !r.error)\n\n if (successfulResults.length > 0) {\n // Use runStartedAt from the first successful result (all use same run)\n const runStartedAt = successfulResults[0].runStartedAt\n if (runStartedAt) {\n const runAgeMs = Date.now() - runStartedAt.getTime()\n if (runAgeMs <= MAX_RUN_AGE_MS) {\n try {\n await sql`SELECT stripe.trigger_worker()`\n selfTriggered = true\n } catch (err) {\n console.warn('Failed to self-trigger after processing:', err)\n }\n } else {\n console.warn(\n `Worker: run too old (${Math.round(runAgeMs / 1000 / 60)} min), not self-triggering`\n )\n }\n }\n }\n\n return new Response(JSON.stringify({ results, selfTriggered }), {\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
49
 
50
50
  // raw-ts:/Users/prasoon/work/paymentsdb-sync-engine/packages/sync-engine/src/supabase/edge-functions/sigma-data-worker.ts
51
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";
@@ -59,7 +59,7 @@ var sigmaWorkerFunctionCode = sigma_data_worker_default;
59
59
  // package.json
60
60
  var package_default = {
61
61
  name: "@paymentsdb/sync-engine",
62
- version: "0.0.7",
62
+ version: "0.0.9",
63
63
  private: false,
64
64
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
65
65
  type: "module",
@@ -269,6 +269,23 @@ var SupabaseSetupClient = class {
269
269
  SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-scheduler'
270
270
  );
271
271
 
272
+ -- Create self-trigger function for worker continuation
273
+ -- This allows the worker to trigger itself when there's more work
274
+ CREATE OR REPLACE FUNCTION stripe.trigger_worker()
275
+ RETURNS void
276
+ LANGUAGE plpgsql
277
+ SECURITY DEFINER
278
+ AS $$
279
+ BEGIN
280
+ PERFORM net.http_post(
281
+ url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
282
+ headers := jsonb_build_object(
283
+ 'Authorization', 'Bearer ' || (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'stripe_sync_worker_secret')
284
+ )
285
+ );
286
+ END;
287
+ $$;
288
+
272
289
  -- Create job to invoke worker at configured interval
273
290
  -- Worker reads from pgmq, enqueues objects if empty, and processes sync work
274
291
  SELECT cron.schedule(
@@ -10,8 +10,8 @@ import {
10
10
  uninstall,
11
11
  webhookFunctionCode,
12
12
  workerFunctionCode
13
- } from "../chunk-OLHHINPH.js";
14
- import "../chunk-7DYBM7H3.js";
13
+ } from "../chunk-5OQVNBPL.js";
14
+ import "../chunk-CVZHIBXG.js";
15
15
  export {
16
16
  INSTALLATION_ERROR_SUFFIX,
17
17
  INSTALLATION_INSTALLED_SUFFIX,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paymentsdb/sync-engine",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "private": false,
5
5
  "description": "Stripe Sync Engine to sync Stripe data to Postgres",
6
6
  "type": "module",
@@ -88,4 +88,4 @@
88
88
  ],
89
89
  "author": "PaymentsDB <https://paymentsdb.com/>",
90
90
  "license": "Apache-2.0"
91
- }
91
+ }