stripe-experiment-sync 1.0.15 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,19 @@
1
1
  # Stripe Sync Engine
2
2
 
3
- ![GitHub License](https://img.shields.io/github/license/tx-stripe/stripe-sync-engine)
3
+ ![GitHub License](https://img.shields.io/github/license/stripe-experiments/sync-engine)
4
4
  ![NPM Version](https://img.shields.io/npm/v/stripe-experiment-sync)
5
5
 
6
6
  A TypeScript library to synchronize Stripe data into a PostgreSQL database, designed for use in Node.js backends and serverless environments.
7
7
 
8
8
  ## Features
9
9
 
10
- - Programmatic management of Stripe webhooks for real-time updates
11
- - Sync Stripe objects (customers, invoices, products, etc.) to your PostgreSQL database
10
+ - **Managed Webhooks:** Automatic webhook creation and lifecycle management with built-in processing
11
+ - **Real-time Sync:** Keep your database in sync with Stripe automatically
12
+ - **Backfill Support:** Sync historical data from Stripe to your database
13
+ - **Stripe Sigma:** Support for Stripe Sigma reporting data
14
+ - **Supabase Ready:** Deploy to Supabase Edge Functions with one command
15
+ - **Automatic Retries:** Built-in retry logic for rate limits and transient errors
16
+ - **Observability:** Track sync runs and monitor progress
12
17
 
13
18
  ## Installation
14
19
 
@@ -20,7 +25,7 @@ pnpm add stripe-experiment-sync stripe
20
25
  yarn add stripe-experiment-sync stripe
21
26
  ```
22
27
 
23
- ## Usage
28
+ ## Quick Start
24
29
 
25
30
  ```ts
26
31
  import { StripeSync } from 'stripe-experiment-sync'
@@ -28,103 +33,129 @@ import { StripeSync } from 'stripe-experiment-sync'
28
33
  const sync = new StripeSync({
29
34
  poolConfig: {
30
35
  connectionString: 'postgres://user:pass@host:port/db',
31
- max: 10, // Maximum number of connections
36
+ max: 10,
32
37
  },
33
38
  stripeSecretKey: 'sk_test_...',
34
- stripeWebhookSecret: 'whsec_...',
35
- // logger: <a pino logger>
36
39
  })
37
40
 
38
- // Example: process a Stripe webhook
39
- await sync.processWebhook(payload, signature)
41
+ // Create a managed webhook - no additional processing needed!
42
+ const webhook = await sync.findOrCreateManagedWebhook('https://example.com/stripe-webhooks')
43
+
44
+ // Cleanup when done (closes PostgreSQL connection pool)
45
+ await sync.close()
40
46
  ```
41
47
 
42
- ## Low-Level API (Advanced)
48
+ ## Managed Webhooks
43
49
 
44
- For more control, you can use the `StripeSync` class directly:
50
+ The Stripe Sync Engine automatically manages webhook endpoints and their processing. Once created, managed webhooks handle everything automatically - you don't need to manually process events.
45
51
 
46
- ```ts
47
- import { StripeSync } from 'stripe-experiment-sync'
52
+ ### Creating Managed Webhooks
48
53
 
49
- const sync = new StripeSync({
50
- poolConfig: {
51
- connectionString: 'postgres://user:pass@host:port/db',
52
- max: 10, // Maximum number of connections
53
- },
54
- stripeSecretKey: 'sk_test_...',
55
- stripeWebhookSecret: 'whsec_...',
56
- // logger: <a pino logger>
54
+ ```typescript
55
+ // Create or reuse an existing webhook endpoint
56
+ // This webhook will automatically sync all Stripe events to your database
57
+ const webhook = await sync.findOrCreateManagedWebhook('https://example.com/stripe-webhooks')
58
+
59
+ // Create a webhook for specific events
60
+ const webhook = await sync.createManagedWebhook('https://example.com/stripe-webhooks', {
61
+ enabled_events: ['customer.created', 'customer.updated', 'invoice.paid'],
57
62
  })
58
63
 
59
- // Example: process a Stripe webhook
60
- await sync.processWebhook(payload, signature)
64
+ console.log(webhook.id) // we_xxx
65
+ console.log(webhook.secret) // whsec_xxx
61
66
  ```
62
67
 
63
- ### Processing Webhooks
68
+ **⚠️ Important:** Managed webhooks are tracked in the database and automatically process incoming events. You don't need to call `processWebhook()` for managed webhooks - the library handles this internally.
64
69
 
65
- The `processWebhook` method validates and processes Stripe webhook events:
70
+ ### Managing Webhooks
66
71
 
67
72
  ```typescript
68
- // Process a webhook event with signature validation
69
- await sync.processWebhook(payload, signature)
73
+ // List all managed webhooks
74
+ const webhooks = await sync.listManagedWebhooks()
75
+
76
+ // Get a specific webhook
77
+ const webhook = await sync.getManagedWebhook('we_xxx')
70
78
 
71
- // Or process an event directly (no signature validation):
72
- await sync.processEvent(event)
79
+ // Delete a managed webhook
80
+ await sync.deleteManagedWebhook('we_xxx')
73
81
  ```
74
82
 
75
- ### Managed Webhook Endpoints
83
+ ### How It Works
76
84
 
77
- The library provides methods to create and manage webhook endpoints:
85
+ **Automatic Processing:** Managed webhooks are stored in the `stripe._managed_webhooks` table. When Stripe sends events to these webhooks, they are automatically processed and synced to your database.
78
86
 
79
- ```typescript
80
- // Create or reuse an existing webhook endpoint for a URL
81
- const webhook = await sync.findOrCreateManagedWebhook('https://example.com/stripe-webhooks', {
82
- enabled_events: ['*'], // or specific events like ['customer.created', 'invoice.paid']
83
- description: 'My app webhook',
84
- })
87
+ **Race Condition Protection:** PostgreSQL advisory locks prevent race conditions when multiple instances call `findOrCreateManagedWebhook()` concurrently. A unique constraint on `(url, account_id)` provides additional safety.
85
88
 
86
- // Create a new webhook endpoint (always creates new)
87
- const webhook = await sync.createManagedWebhook('https://example.com/stripe-webhooks', {
88
- enabled_events: ['customer.created', 'customer.updated'],
89
+ **Automatic Cleanup:** When you call `findOrCreateManagedWebhook()`, it will:
90
+
91
+ 1. Check if a webhook already exists for the URL in the database
92
+ 2. If found, reuse the existing webhook
93
+ 3. If not found, create a new webhook in Stripe and record it
94
+ 4. Clean up any orphaned webhooks from previous installations
95
+
96
+ ## Manual Webhook Processing
97
+
98
+ If you need to process webhooks outside of managed webhooks (e.g., for testing or custom integrations):
99
+
100
+ ```typescript
101
+ // Validate and process a webhook event
102
+ app.post('/stripe-webhooks', async (req, res) => {
103
+ const signature = req.headers['stripe-signature']
104
+ const payload = req.body
105
+
106
+ try {
107
+ await sync.processWebhook(payload, signature)
108
+ res.status(200).send({ received: true })
109
+ } catch (error) {
110
+ res.status(400).send({ error: error.message })
111
+ }
89
112
  })
90
113
 
91
- // Get a managed webhook by ID
92
- const webhook = await sync.getManagedWebhook('we_xxx')
114
+ // Or process an event directly (no signature validation)
115
+ await sync.processEvent(stripeEvent)
93
116
 
94
- // Delete a managed webhook
95
- await sync.deleteManagedWebhook('we_xxx')
117
+ // Cleanup when done
118
+ await sync.close()
96
119
  ```
97
120
 
98
- **Note:** The library automatically manages webhook endpoints for you. When you call `findOrCreateManagedWebhook()` with a URL, it will reuse an existing webhook if one is found in the database, or create a new one if needed. Old or orphaned webhooks from this package are automatically cleaned up.
99
-
100
- **Race Condition Protection:** The library uses PostgreSQL advisory locks to prevent race conditions when multiple instances call `findOrCreateManagedWebhook()` concurrently for the same URL. A unique constraint on `(url, account_id)` provides an additional safety net at the database level.
121
+ **Note:** This is only needed for custom webhook endpoints. Managed webhooks handle processing automatically.
101
122
 
102
123
  ## Configuration
103
124
 
104
- | Option | Type | Description |
105
- | ------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
106
- | `databaseUrl` | string | **Deprecated:** Use `poolConfig` with a connection string instead. |
107
- | `stripeSecretKey` | string | Stripe secret key |
108
- | `stripeWebhookSecret` | string | Stripe webhook signing secret |
109
- | `stripeApiVersion` | string | Stripe API version (default: `2020-08-27`) |
110
- | `autoExpandLists` | boolean | Fetch all list items from Stripe (not just the default 10) |
111
- | `backfillRelatedEntities` | boolean | Ensure related entities are present for foreign key integrity |
112
- | `revalidateObjectsViaStripeApi` | Array | Always fetch latest entity from Stripe instead of trusting webhook payload, possible values: charge, credit_note, customer, dispute, invoice, payment_intent, payment_method, plan, price, product, refund, review, radar.early_fraud_warning, setup_intent, subscription, subscription_schedule, tax_id |
113
- | `poolConfig` | object | Configuration for PostgreSQL connection pooling. Supports options like `connectionString`, `max`, and `keepAlive`. For more details, refer to the [Node-Postgres Pool API documentation](https://node-postgres.com/apis/pool). |
114
- | `maxPostgresConnections` | number | **Deprecated:** Use `poolConfig.max` instead to configure the maximum number of PostgreSQL connections. |
115
- | `logger` | Logger | Logger instance (pino) |
125
+ | Option | Type | Description |
126
+ | ------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
127
+ | `poolConfig` | object | **Required.** PostgreSQL connection pool configuration. Supports `connectionString`, `max`, `keepAlive`. See [Node-Postgres Pool API](https://node-postgres.com/apis/pool). |
128
+ | `stripeSecretKey` | string | **Required.** Stripe secret key |
129
+ | `stripeWebhookSecret` | string | Stripe webhook signing secret (only needed for manual webhook processing) |
130
+ | `stripeApiVersion` | string | Stripe API version (default: `2020-08-27`) |
131
+ | `enableSigma` | boolean | Enable Stripe Sigma reporting data sync. Default: false |
132
+ | `autoExpandLists` | boolean | Fetch all list items from Stripe (not just the default 10) |
133
+ | `backfillRelatedEntities` | boolean | Ensure related entities exist for foreign key integrity |
134
+ | `revalidateObjectsViaStripeApi` | Array | Always fetch latest data from Stripe instead of trusting webhook payload. Possible values: charge, credit_note, customer, dispute, invoice, payment_intent, payment_method, plan, price, product, refund, review, radar.early_fraud_warning, setup_intent, subscription, subscription_schedule, tax_id |
135
+ | `maxRetries` | number | Maximum retry attempts for 429 rate limits. Default: 5 |
136
+ | `initialRetryDelayMs` | number | Initial retry delay in milliseconds. Default: 1000 |
137
+ | `maxRetryDelayMs` | number | Maximum retry delay in milliseconds. Default: 60000 |
138
+ | `logger` | Logger | Logger instance (pino-compatible) |
116
139
 
117
140
  ## Database Schema
118
141
 
119
- The library will create and manage a `stripe` schema in your PostgreSQL database, with tables for all supported Stripe objects (products, customers, invoices, etc.).
142
+ The library creates and manages a `stripe` schema in PostgreSQL with tables for all supported Stripe objects.
143
+
144
+ > **Important:** The schema name is fixed as `stripe` and cannot be configured.
120
145
 
121
- > **Important:** The library uses a fixed schema name of `stripe`. This cannot be configured as the SQL migrations hardcode this schema name.
146
+ > **Note:** Fields and tables prefixed with `_` are reserved for internal metadata: `_account_id`, `_last_synced_at`, `_updated_at`, `_migrations`, `_managed_webhooks`, `_sync_runs`, `_sync_obj_runs`.
122
147
 
123
- > **Note:** Fields and tables prefixed with an underscore (`_`) are reserved for internal metadata managed by the sync engine and should not be modified directly. These include fields like `_account_id`, `_last_synced_at`, `_updated_at`, and tables like `_migrations`, `_managed_webhooks`, `_sync_runs`, and `_sync_obj_runs`.
148
+ ### Running Migrations
149
+
150
+ ```ts
151
+ import { runMigrations } from 'stripe-experiment-sync'
152
+
153
+ await runMigrations({ databaseUrl: 'postgres://...' })
154
+ ```
124
155
 
125
156
  ### Observability
126
157
 
127
- The sync engine tracks sync operations in the `sync_runs` view, which provides aggregated metrics for each sync session:
158
+ Track sync operations with the `sync_runs` view:
128
159
 
129
160
  ```sql
130
161
  SELECT
@@ -132,104 +163,145 @@ SELECT
132
163
  started_at,
133
164
  closed_at,
134
165
  status, -- 'running', 'complete', or 'error'
135
- total_processed, -- Total records synced across all objects
136
- complete_count, -- Number of object types completed
137
- error_count, -- Number of object types with errors
138
- running_count, -- Number of object types currently syncing
139
- pending_count -- Number of object types not yet started
166
+ total_processed, -- Total records synced
167
+ complete_count, -- Completed object types
168
+ error_count, -- Object types with errors
169
+ running_count, -- Currently syncing
170
+ pending_count -- Not yet started
140
171
  FROM stripe.sync_runs
141
172
  ORDER BY started_at DESC;
142
173
  ```
143
174
 
144
- For detailed per-object status, you can query the internal `_sync_obj_runs` table (though this is not recommended for production use).
175
+ ## Syncing Data
145
176
 
146
- ### Migrations
177
+ ### Sync a Single Entity
178
+
179
+ ```ts
180
+ // Automatically detects entity type from ID prefix
181
+ await sync.syncSingleEntity('cus_12345')
182
+ await sync.syncSingleEntity('prod_xyz')
183
+ ```
147
184
 
148
- Migrations are automatically included with the package and bundled in the `dist/migrations` directory when built. You can run them using the provided `runMigrations` function:
185
+ ### Backfill Historical Data
149
186
 
150
187
  ```ts
151
- import { runMigrations } from 'stripe-experiment-sync'
188
+ // Sync all products created after a date
189
+ await sync.processUntilDone({
190
+ object: 'product',
191
+ created: { gte: 1643872333 }, // Unix timestamp
192
+ })
152
193
 
153
- await runMigrations({ databaseUrl: 'postgres://...' })
194
+ // Sync all customers
195
+ await sync.processUntilDone({ object: 'customer' })
196
+
197
+ // Sync everything
198
+ await sync.processUntilDone({ object: 'all' })
154
199
  ```
155
200
 
156
- ## Account Management
201
+ Supported objects: `all`, `charge`, `checkout_sessions`, `credit_note`, `customer`, `customer_with_entitlements`, `dispute`, `early_fraud_warning`, `invoice`, `payment_intent`, `payment_method`, `plan`, `price`, `product`, `refund`, `setup_intent`, `subscription`, `subscription_schedules`, `tax_id`.
157
202
 
158
- ### Getting Current Account
203
+ The sync engine tracks cursors per account and resource, enabling incremental syncing that resumes after interruptions.
159
204
 
160
- Retrieve the currently authenticated Stripe account:
205
+ > **Tip:** For large Stripe accounts (>10,000 objects), loop through date ranges day-by-day to avoid timeouts.
206
+
207
+ ## Account Management
208
+
209
+ ### Get Current Account
161
210
 
162
211
  ```ts
163
212
  const account = await sync.getCurrentAccount()
164
- console.log(account.id) // e.g., "acct_xxx"
213
+ console.log(account.id) // acct_xxx
165
214
  ```
166
215
 
167
- ### Listing Synced Accounts
168
-
169
- Get all Stripe accounts that have been synced to the database:
216
+ ### List Synced Accounts
170
217
 
171
218
  ```ts
172
219
  const accounts = await sync.getAllSyncedAccounts()
173
- // Returns array of Stripe account objects from database
174
220
  ```
175
221
 
176
- ### Deleting Synced Account Data
222
+ ### Delete Account Data
177
223
 
178
- **⚠️ DANGEROUS:** Delete all synced data for a specific Stripe account from the database. This operation cannot be undone!
224
+ **⚠️ WARNING:** This permanently deletes all synced data for an account.
179
225
 
180
226
  ```ts
181
- // Preview what will be deleted (dry-run mode)
227
+ // Preview deletion
182
228
  const preview = await sync.dangerouslyDeleteSyncedAccountData('acct_xxx', {
183
229
  dryRun: true,
184
- useTransaction: true,
185
230
  })
186
- console.log(preview.deletedRecordCounts) // Shows count per table
231
+ console.log(preview.deletedRecordCounts)
187
232
 
188
- // Actually delete the data
189
- const result = await sync.dangerouslyDeleteSyncedAccountData('acct_xxx', {
190
- dryRun: false, // default
191
- useTransaction: true, // default - wraps deletion in transaction
192
- })
233
+ // Actually delete
234
+ const result = await sync.dangerouslyDeleteSyncedAccountData('acct_xxx')
193
235
  ```
194
236
 
195
- Options:
237
+ ## Supabase Deployment
196
238
 
197
- - `dryRun` (default: `false`): If true, only counts records without deleting
198
- - `useTransaction` (default: `true`): If true, wraps all deletions in a database transaction for atomicity
239
+ Deploy to Supabase Edge Functions for serverless operation with automatic webhook processing:
199
240
 
200
- The method returns:
241
+ ```bash
242
+ # Install
243
+ npx stripe-experiment-sync supabase install \
244
+ --token $SUPABASE_ACCESS_TOKEN \
245
+ --project $SUPABASE_PROJECT_REF \
246
+ --stripe-key $STRIPE_API_KEY
201
247
 
202
- - `deletedAccountId`: The account ID that was deleted
203
- - `deletedRecordCounts`: Object mapping table names to number of records deleted
204
- - `warnings`: Array of warning messages (e.g., if you're deleting your cached account)
248
+ # Install specific version
249
+ npx stripe-experiment-sync supabase install \
250
+ --token $SUPABASE_ACCESS_TOKEN \
251
+ --project $SUPABASE_PROJECT_REF \
252
+ --stripe-key $STRIPE_API_KEY \
253
+ --package-version 1.0.15
205
254
 
206
- ## Backfilling and Syncing Data
255
+ # Uninstall
256
+ npx stripe-experiment-sync supabase uninstall \
257
+ --token $SUPABASE_ACCESS_TOKEN \
258
+ --project $SUPABASE_PROJECT_REF
259
+ ```
207
260
 
208
- ### Syncing a Single Entity
261
+ ### Install Options
209
262
 
210
- You can sync or update a single Stripe entity by its ID using the `syncSingleEntity` method:
263
+ - `--token <token>` - Supabase access token (or `SUPABASE_ACCESS_TOKEN` env)
264
+ - `--project <ref>` - Supabase project ref (or `SUPABASE_PROJECT_REF` env)
265
+ - `--stripe-key <key>` - Stripe API key (or `STRIPE_API_KEY` env)
266
+ - `--package-version <version>` - npm package version (default: latest)
267
+ - `--worker-interval <seconds>` - Worker interval in seconds (default: 60)
268
+ - `--management-url <url>` - Supabase management API URL with protocol (default: https://api.supabase.com). For local testing: http://localhost:54323
211
269
 
212
- ```ts
213
- await sync.syncSingleEntity('cus_12345')
214
- ```
270
+ The install command will:
215
271
 
216
- The entity type is detected automatically based on the Stripe ID prefix (e.g., `cus_` for customer, `prod_` for product). `ent_` is not supported at the moment.
272
+ 1. Deploy Edge Functions: `stripe-setup`, `stripe-webhook`, `stripe-worker`
273
+ 2. Run database migrations to create the `stripe` schema
274
+ 3. Create a managed Stripe webhook pointing to your Supabase project
275
+ 4. Set up a pg_cron job for automatic background syncing
217
276
 
218
- ### Syncing Data
277
+ ## CLI Commands
219
278
 
220
- To sync Stripe data (e.g., all products created after a certain date), use the `processUntilDone` method:
279
+ ```bash
280
+ # Run database migrations
281
+ npx stripe-experiment-sync migrate --database-url $DATABASE_URL
221
282
 
222
- ```ts
223
- await sync.processUntilDone({
224
- object: 'product',
225
- created: { gte: 1643872333 }, // Unix timestamp
226
- })
283
+ # Start local sync with ngrok tunnel
284
+ npx stripe-experiment-sync start \
285
+ --stripe-key $STRIPE_API_KEY \
286
+ --ngrok-token $NGROK_AUTH_TOKEN \
287
+ --database-url $DATABASE_URL
288
+
289
+ # Backfill specific entity type
290
+ npx stripe-experiment-sync backfill customer \
291
+ --stripe-key $STRIPE_API_KEY \
292
+ --database-url $DATABASE_URL
293
+
294
+ # Enable Sigma data syncing
295
+ npx stripe-experiment-sync start \
296
+ --stripe-key $STRIPE_API_KEY \
297
+ --database-url $DATABASE_URL \
298
+ --sigma
227
299
  ```
228
300
 
229
- - `object` can be one of: `all`, `charge`, `checkout_sessions`, `credit_note`, `customer`, `customer_with_entitlements`, `dispute`, `early_fraud_warning`, `invoice`, `payment_intent`, `payment_method`, `plan`, `price`, `product`, `refund`, `setup_intent`, `subscription`, `subscription_schedules`, `tax_id`.
230
- - `created` is a Stripe RangeQueryParam and supports `gt`, `gte`, `lt`, `lte`.
301
+ ## License
302
+
303
+ See [LICENSE](LICENSE) file.
231
304
 
232
- The sync engine automatically tracks per-account cursors in the `_sync_runs` and `_sync_obj_runs` tables. When you call sync methods without an explicit `created` filter, they will automatically resume from the last synced position for that account and resource. This enables incremental syncing that can resume after interruptions.
305
+ ## Contributing
233
306
 
234
- > **Note:**
235
- > For large Stripe accounts (more than 10,000 objects), it is recommended to write a script that loops through each day and sets the `created` date filters to the start and end of day. This avoids timeouts and memory issues when syncing large datasets.
307
+ Issues and pull requests are welcome at [https://github.com/stripe-experiments/sync-engine](https://github.com/stripe-experiments/sync-engine).
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "stripe-experiment-sync",
4
- version: "1.0.15",
4
+ version: "1.0.17",
5
5
  private: false,
6
6
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
7
7
  type: "module",
@@ -63,11 +63,11 @@ var package_default = {
63
63
  },
64
64
  repository: {
65
65
  type: "git",
66
- url: "https://github.com/tx-stripe/stripe-sync-engine.git"
66
+ url: "https://github.com/stripe-experiments/sync-engine.git"
67
67
  },
68
- homepage: "https://github.com/tx-stripe/stripe-sync-engine#readme",
68
+ homepage: "https://github.com/stripe-experiments/sync-engine#readme",
69
69
  bugs: {
70
- url: "https://github.com/tx-stripe/stripe-sync-engine/issues"
70
+ url: "https://github.com/stripe-experiments/sync-engine/issues"
71
71
  },
72
72
  keywords: [
73
73
  "stripe",
@@ -79,7 +79,7 @@ var package_default = {
79
79
  "database",
80
80
  "typescript"
81
81
  ],
82
- author: "Supabase <https://supabase.com/>"
82
+ author: "Stripe <https://stripe.com/>"
83
83
  };
84
84
 
85
85
  export {
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  package_default
3
- } from "./chunk-3KVILTN4.js";
3
+ } from "./chunk-57SXDCMH.js";
4
4
 
5
5
  // src/supabase/supabase.ts
6
6
  import { SupabaseManagementAPI } from "supabase-management-js";
7
7
 
8
8
  // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
9
- var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\n// Helper to validate accessToken against Management API\nasync function validateAccessToken(projectRef: string, accessToken: string): Promise<boolean> {\n // Try to fetch project details using the access token\n // This validates that the token is valid for the management API\n const url = `https://api.supabase.com/v1/projects/${projectRef}`\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n // If we can successfully get the project, the token is valid\n return response.ok\n}\n\n// Helper to delete edge function via Management API\nasync function deleteEdgeFunction(\n projectRef: string,\n functionSlug: string,\n accessToken: string\n): Promise<void> {\n const url = `https://api.supabase.com/v1/projects/${projectRef}/functions/${functionSlug}`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n throw new Error(`Failed to delete function ${functionSlug}: ${response.status} ${text}`)\n }\n}\n\n// Helper to delete secrets via Management API\nasync function deleteSecret(\n projectRef: string,\n secretName: string,\n accessToken: string\n): Promise<void> {\n const url = `https://api.supabase.com/v1/projects/${projectRef}/secrets`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify([secretName]),\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n console.warn(`Failed to delete secret ${secretName}: ${response.status} ${text}`)\n }\n}\n\nDeno.serve(async (req) => {\n // Extract project ref from SUPABASE_URL (format: https://{projectRef}.{base})\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n const projectRef = new URL(supabaseUrl).hostname.split('.')[0]\n\n // Validate access token for all requests\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const accessToken = authHeader.substring(7) // Remove 'Bearer '\n const isValid = await validateAccessToken(projectRef, accessToken)\n if (!isValid) {\n return new Response('Forbidden: Invalid access token for this project', { status: 403 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle DELETE requests for uninstall\n if (req.method === 'DELETE') {\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n // Stripe key is required for uninstall to delete webhooks\n const stripeKey = Deno.env.get('STRIPE_SECRET_KEY')\n if (!stripeKey) {\n throw new Error('STRIPE_SECRET_KEY environment variable is required for uninstall')\n }\n\n // Step 1: Delete Stripe webhooks and clean up database\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 },\n stripeSecretKey: stripeKey,\n })\n\n // Delete all managed webhooks\n const webhooks = await stripeSync.listManagedWebhooks()\n for (const webhook of webhooks) {\n try {\n await stripeSync.deleteManagedWebhook(webhook.id)\n console.log(`Deleted webhook: ${webhook.id}`)\n } catch (err) {\n console.warn(`Could not delete webhook ${webhook.id}:`, err)\n }\n }\n\n // Unschedule pg_cron job\n try {\n await stripeSync.postgresClient.query(`\n DO $$\n BEGIN\n IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN\n PERFORM cron.unschedule('stripe-sync-worker');\n END IF;\n END $$;\n `)\n } catch (err) {\n console.warn('Could not unschedule pg_cron job:', err)\n }\n\n // Delete vault secret\n try {\n await stripeSync.postgresClient.query(`\n DELETE FROM vault.secrets\n WHERE name = 'stripe_sync_worker_secret'\n `)\n } catch (err) {\n console.warn('Could not delete vault secret:', err)\n }\n\n // Terminate connections holding locks on stripe schema\n try {\n await stripeSync.postgresClient.query(`\n SELECT pg_terminate_backend(pid)\n FROM pg_locks l\n JOIN pg_class c ON l.relation = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname = 'stripe'\n AND l.pid != pg_backend_pid()\n `)\n } catch (err) {\n console.warn('Could not terminate connections:', err)\n }\n\n // Drop schema with retry\n let dropAttempts = 0\n const maxAttempts = 3\n while (dropAttempts < maxAttempts) {\n try {\n await stripeSync.postgresClient.query('DROP SCHEMA IF EXISTS stripe CASCADE')\n break // Success, exit loop\n } catch (err) {\n dropAttempts++\n if (dropAttempts >= maxAttempts) {\n throw new Error(\n `Failed to drop schema after ${maxAttempts} attempts. ` +\n `There may be active connections or locks on the stripe schema. ` +\n `Error: ${err.message}`\n )\n }\n // Wait 1 second before retrying\n await new Promise((resolve) => setTimeout(resolve, 1000))\n }\n }\n\n await stripeSync.postgresClient.pool.end()\n\n // Step 2: Delete Supabase secrets\n try {\n await deleteSecret(projectRef, 'STRIPE_SECRET_KEY', accessToken)\n } catch (err) {\n console.warn('Could not delete STRIPE_SECRET_KEY secret:', err)\n }\n\n // Step 3: Delete Edge Functions\n try {\n await deleteEdgeFunction(projectRef, 'stripe-setup', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-setup function:', err)\n }\n\n try {\n await deleteEdgeFunction(projectRef, 'stripe-webhook', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-webhook function:', err)\n }\n\n try {\n await deleteEdgeFunction(projectRef, 'stripe-worker', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-worker function:', err)\n }\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Uninstall complete',\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Uninstall error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n }\n\n // Handle POST requests for install\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
9
+ var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\n// Get management API base URL from environment variable (for testing against localhost/staging)\n// Caller should provide full URL with protocol (e.g., http://localhost:54323 or https://api.supabase.com)\nconst MGMT_API_BASE_RAW = Deno.env.get('MANAGEMENT_API_URL') || 'https://api.supabase.com'\nconst MGMT_API_BASE = MGMT_API_BASE_RAW.match(/^https?:\\/\\//)\n ? MGMT_API_BASE_RAW\n : `https://${MGMT_API_BASE_RAW}`\n\n// Helper to validate accessToken against Management API\nasync function validateAccessToken(projectRef: string, accessToken: string): Promise<boolean> {\n // Try to fetch project details using the access token\n // This validates that the token is valid for the management API\n const url = `${MGMT_API_BASE}/v1/projects/${projectRef}`\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n // If we can successfully get the project, the token is valid\n return response.ok\n}\n\n// Helper to delete edge function via Management API\nasync function deleteEdgeFunction(\n projectRef: string,\n functionSlug: string,\n accessToken: string\n): Promise<void> {\n const url = `${MGMT_API_BASE}/v1/projects/${projectRef}/functions/${functionSlug}`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n throw new Error(`Failed to delete function ${functionSlug}: ${response.status} ${text}`)\n }\n}\n\n// Helper to delete secrets via Management API\nasync function deleteSecret(\n projectRef: string,\n secretName: string,\n accessToken: string\n): Promise<void> {\n const url = `${MGMT_API_BASE}/v1/projects/${projectRef}/secrets`\n const response = await fetch(url, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify([secretName]),\n })\n\n if (!response.ok && response.status !== 404) {\n const text = await response.text()\n console.warn(`Failed to delete secret ${secretName}: ${response.status} ${text}`)\n }\n}\n\nDeno.serve(async (req) => {\n // Extract project ref from SUPABASE_URL (format: https://{projectRef}.{base})\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n const projectRef = new URL(supabaseUrl).hostname.split('.')[0]\n\n // Validate access token for all requests\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const accessToken = authHeader.substring(7) // Remove 'Bearer '\n const isValid = await validateAccessToken(projectRef, accessToken)\n if (!isValid) {\n return new Response('Forbidden: Invalid access token for this project', { status: 403 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle DELETE requests for uninstall\n if (req.method === 'DELETE') {\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n // Stripe key is required for uninstall to delete webhooks\n const stripeKey = Deno.env.get('STRIPE_SECRET_KEY')\n if (!stripeKey) {\n throw new Error('STRIPE_SECRET_KEY environment variable is required for uninstall')\n }\n\n // Step 1: Delete Stripe webhooks and clean up database\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 },\n stripeSecretKey: stripeKey,\n })\n\n // Delete all managed webhooks\n const webhooks = await stripeSync.listManagedWebhooks()\n for (const webhook of webhooks) {\n try {\n await stripeSync.deleteManagedWebhook(webhook.id)\n console.log(`Deleted webhook: ${webhook.id}`)\n } catch (err) {\n console.warn(`Could not delete webhook ${webhook.id}:`, err)\n }\n }\n\n // Unschedule pg_cron job\n try {\n await stripeSync.postgresClient.query(`\n DO $$\n BEGIN\n IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN\n PERFORM cron.unschedule('stripe-sync-worker');\n END IF;\n END $$;\n `)\n } catch (err) {\n console.warn('Could not unschedule pg_cron job:', err)\n }\n\n // Delete vault secret\n try {\n await stripeSync.postgresClient.query(`\n DELETE FROM vault.secrets\n WHERE name = 'stripe_sync_worker_secret'\n `)\n } catch (err) {\n console.warn('Could not delete vault secret:', err)\n }\n\n // Terminate connections holding locks on stripe schema\n try {\n await stripeSync.postgresClient.query(`\n SELECT pg_terminate_backend(pid)\n FROM pg_locks l\n JOIN pg_class c ON l.relation = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname = 'stripe'\n AND l.pid != pg_backend_pid()\n `)\n } catch (err) {\n console.warn('Could not terminate connections:', err)\n }\n\n // Drop schema with retry\n let dropAttempts = 0\n const maxAttempts = 3\n while (dropAttempts < maxAttempts) {\n try {\n await stripeSync.postgresClient.query('DROP SCHEMA IF EXISTS stripe CASCADE')\n break // Success, exit loop\n } catch (err) {\n dropAttempts++\n if (dropAttempts >= maxAttempts) {\n throw new Error(\n `Failed to drop schema after ${maxAttempts} attempts. ` +\n `There may be active connections or locks on the stripe schema. ` +\n `Error: ${err.message}`\n )\n }\n // Wait 1 second before retrying\n await new Promise((resolve) => setTimeout(resolve, 1000))\n }\n }\n\n await stripeSync.postgresClient.pool.end()\n\n // Step 2: Delete Supabase secrets\n try {\n await deleteSecret(projectRef, 'STRIPE_SECRET_KEY', accessToken)\n } catch (err) {\n console.warn('Could not delete STRIPE_SECRET_KEY secret:', err)\n }\n\n // Step 3: Delete Edge Functions\n try {\n await deleteEdgeFunction(projectRef, 'stripe-setup', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-setup function:', err)\n }\n\n try {\n await deleteEdgeFunction(projectRef, 'stripe-webhook', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-webhook function:', err)\n }\n\n try {\n await deleteEdgeFunction(projectRef, 'stripe-worker', accessToken)\n } catch (err) {\n console.warn('Could not delete stripe-worker function:', err)\n }\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Uninstall complete',\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Uninstall error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n }\n\n // Handle POST requests for install\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
10
10
 
11
11
  // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
12
12
  var stripe_webhook_default = "import { StripeSync } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const sig = req.headers.get('stripe-signature')\n if (!sig) {\n return new Response('Missing stripe-signature header', { status: 400 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n const stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n\n try {\n const rawBody = new Uint8Array(await req.arrayBuffer())\n await stripeSync.processWebhook(rawBody, sig)\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Webhook processing error:', error)\n const isSignatureError =\n error.message?.includes('signature') || error.type === 'StripeSignatureVerificationError'\n const status = isSignatureError ? 400 : 500\n return new Response(JSON.stringify({ error: error.message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n await stripeSync.postgresClient.pool.end()\n }\n})\n";
@@ -28,14 +28,16 @@ var SupabaseSetupClient = class {
28
28
  api;
29
29
  projectRef;
30
30
  projectBaseUrl;
31
+ supabaseManagementUrl;
31
32
  accessToken;
32
33
  constructor(options) {
33
34
  this.api = new SupabaseManagementAPI({
34
35
  accessToken: options.accessToken,
35
- baseUrl: options.managementApiBaseUrl
36
+ baseUrl: options.supabaseManagementUrl
36
37
  });
37
38
  this.projectRef = options.projectRef;
38
39
  this.projectBaseUrl = options.projectBaseUrl || process.env.SUPABASE_BASE_URL || "supabase.co";
40
+ this.supabaseManagementUrl = options.supabaseManagementUrl;
39
41
  this.accessToken = options.accessToken;
40
42
  }
41
43
  /**
@@ -342,7 +344,11 @@ var SupabaseSetupClient = class {
342
344
  await this.deployFunction("stripe-setup", versionedSetup, false);
343
345
  await this.deployFunction("stripe-webhook", versionedWebhook, false);
344
346
  await this.deployFunction("stripe-worker", versionedWorker, false);
345
- await this.setSecrets([{ name: "STRIPE_SECRET_KEY", value: trimmedStripeKey }]);
347
+ const secrets = [{ name: "STRIPE_SECRET_KEY", value: trimmedStripeKey }];
348
+ if (this.supabaseManagementUrl) {
349
+ secrets.push({ name: "MANAGEMENT_API_URL", value: this.supabaseManagementUrl });
350
+ }
351
+ await this.setSecrets(secrets);
346
352
  const setupResult = await this.invokeFunction("stripe-setup", this.accessToken);
347
353
  if (!setupResult.success) {
348
354
  throw new Error(`Setup failed: ${setupResult.error}`);
@@ -371,7 +377,7 @@ async function install(params) {
371
377
  accessToken: supabaseAccessToken,
372
378
  projectRef: supabaseProjectRef,
373
379
  projectBaseUrl: params.baseProjectUrl,
374
- managementApiBaseUrl: params.baseManagementApiUrl
380
+ supabaseManagementUrl: params.supabaseManagementUrl
375
381
  });
376
382
  await client.install(stripeKey, packageVersion, workerIntervalSeconds);
377
383
  }
@@ -381,7 +387,7 @@ async function uninstall(params) {
381
387
  accessToken: supabaseAccessToken,
382
388
  projectRef: supabaseProjectRef,
383
389
  projectBaseUrl: params.baseProjectUrl,
384
- managementApiBaseUrl: params.baseManagementApiBaseUrl
390
+ supabaseManagementUrl: params.supabaseManagementUrl
385
391
  });
386
392
  await client.uninstall();
387
393
  }