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 +189 -117
- package/dist/{chunk-3KVILTN4.js → chunk-57SXDCMH.js} +5 -5
- package/dist/{chunk-XIFB5ALW.js → chunk-I7IFXSAU.js} +12 -6
- package/dist/{chunk-GOBVFXU7.js → chunk-TV67ZOCK.js} +12 -4
- package/dist/{chunk-AODI5WO6.js → chunk-YXRCT3RK.js} +8 -4
- package/dist/cli/index.cjs +44 -18
- package/dist/cli/index.js +15 -7
- package/dist/cli/lib.cjs +33 -15
- package/dist/cli/lib.d.cts +1 -0
- package/dist/cli/lib.d.ts +1 -0
- package/dist/cli/lib.js +4 -4
- package/dist/index.cjs +16 -8
- package/dist/index.js +2 -2
- package/dist/supabase/index.cjs +16 -10
- package/dist/supabase/index.d.cts +4 -3
- package/dist/supabase/index.d.ts +4 -3
- package/dist/supabase/index.js +2 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
# Stripe Sync Engine
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|

|
|
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
|
-
-
|
|
11
|
-
- Sync
|
|
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
|
-
##
|
|
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,
|
|
36
|
+
max: 10,
|
|
32
37
|
},
|
|
33
38
|
stripeSecretKey: 'sk_test_...',
|
|
34
|
-
stripeWebhookSecret: 'whsec_...',
|
|
35
|
-
// logger: <a pino logger>
|
|
36
39
|
})
|
|
37
40
|
|
|
38
|
-
//
|
|
39
|
-
await sync.
|
|
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
|
-
##
|
|
48
|
+
## Managed Webhooks
|
|
43
49
|
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
import { StripeSync } from 'stripe-experiment-sync'
|
|
52
|
+
### Creating Managed Webhooks
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
//
|
|
60
|
-
|
|
64
|
+
console.log(webhook.id) // we_xxx
|
|
65
|
+
console.log(webhook.secret) // whsec_xxx
|
|
61
66
|
```
|
|
62
67
|
|
|
63
|
-
|
|
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
|
-
|
|
70
|
+
### Managing Webhooks
|
|
66
71
|
|
|
67
72
|
```typescript
|
|
68
|
-
//
|
|
69
|
-
await sync.
|
|
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
|
-
//
|
|
72
|
-
await sync.
|
|
79
|
+
// Delete a managed webhook
|
|
80
|
+
await sync.deleteManagedWebhook('we_xxx')
|
|
73
81
|
```
|
|
74
82
|
|
|
75
|
-
###
|
|
83
|
+
### How It Works
|
|
76
84
|
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
//
|
|
92
|
-
|
|
114
|
+
// Or process an event directly (no signature validation)
|
|
115
|
+
await sync.processEvent(stripeEvent)
|
|
93
116
|
|
|
94
|
-
//
|
|
95
|
-
await sync.
|
|
117
|
+
// Cleanup when done
|
|
118
|
+
await sync.close()
|
|
96
119
|
```
|
|
97
120
|
|
|
98
|
-
**Note:**
|
|
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
|
-
| `
|
|
107
|
-
| `stripeSecretKey` | string | Stripe secret key
|
|
108
|
-
| `stripeWebhookSecret` | string | Stripe webhook signing secret
|
|
109
|
-
| `stripeApiVersion` | string | Stripe API version (default: `2020-08-27`)
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
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
|
|
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
|
-
> **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
136
|
-
complete_count, --
|
|
137
|
-
error_count, --
|
|
138
|
-
running_count, --
|
|
139
|
-
pending_count --
|
|
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
|
-
|
|
175
|
+
## Syncing Data
|
|
145
176
|
|
|
146
|
-
###
|
|
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
|
-
|
|
185
|
+
### Backfill Historical Data
|
|
149
186
|
|
|
150
187
|
```ts
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
203
|
+
The sync engine tracks cursors per account and resource, enabling incremental syncing that resumes after interruptions.
|
|
159
204
|
|
|
160
|
-
|
|
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) //
|
|
213
|
+
console.log(account.id) // acct_xxx
|
|
165
214
|
```
|
|
166
215
|
|
|
167
|
-
###
|
|
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
|
-
###
|
|
222
|
+
### Delete Account Data
|
|
177
223
|
|
|
178
|
-
**⚠️
|
|
224
|
+
**⚠️ WARNING:** This permanently deletes all synced data for an account.
|
|
179
225
|
|
|
180
226
|
```ts
|
|
181
|
-
// Preview
|
|
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)
|
|
231
|
+
console.log(preview.deletedRecordCounts)
|
|
187
232
|
|
|
188
|
-
// Actually delete
|
|
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
|
-
|
|
237
|
+
## Supabase Deployment
|
|
196
238
|
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
-
|
|
204
|
-
|
|
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
|
-
|
|
255
|
+
# Uninstall
|
|
256
|
+
npx stripe-experiment-sync supabase uninstall \
|
|
257
|
+
--token $SUPABASE_ACCESS_TOKEN \
|
|
258
|
+
--project $SUPABASE_PROJECT_REF
|
|
259
|
+
```
|
|
207
260
|
|
|
208
|
-
###
|
|
261
|
+
### Install Options
|
|
209
262
|
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
await sync.syncSingleEntity('cus_12345')
|
|
214
|
-
```
|
|
270
|
+
The install command will:
|
|
215
271
|
|
|
216
|
-
|
|
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
|
-
|
|
277
|
+
## CLI Commands
|
|
219
278
|
|
|
220
|
-
|
|
279
|
+
```bash
|
|
280
|
+
# Run database migrations
|
|
281
|
+
npx stripe-experiment-sync migrate --database-url $DATABASE_URL
|
|
221
282
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
230
|
-
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
See [LICENSE](LICENSE) file.
|
|
231
304
|
|
|
232
|
-
|
|
305
|
+
## Contributing
|
|
233
306
|
|
|
234
|
-
|
|
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.
|
|
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/
|
|
66
|
+
url: "https://github.com/stripe-experiments/sync-engine.git"
|
|
67
67
|
},
|
|
68
|
-
homepage: "https://github.com/
|
|
68
|
+
homepage: "https://github.com/stripe-experiments/sync-engine#readme",
|
|
69
69
|
bugs: {
|
|
70
|
-
url: "https://github.com/
|
|
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: "
|
|
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-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
390
|
+
supabaseManagementUrl: params.supabaseManagementUrl
|
|
385
391
|
});
|
|
386
392
|
await client.uninstall();
|
|
387
393
|
}
|