@xenterprises/fastify-xstripe 1.1.1 → 1.2.1
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/LICENSE +97 -12
- package/README.md +108 -253
- package/index.d.ts +78 -184
- package/package.json +6 -4
- package/src/webhooks/webhooks.js +10 -10
- package/src/xStripe.js +28 -15
- package/.dockerignore +0 -62
- package/.env.example +0 -116
- package/.gitlab-ci.yml +0 -45
package/LICENSE
CHANGED
|
@@ -1,15 +1,100 @@
|
|
|
1
|
-
|
|
1
|
+
PROPRIETARY SOFTWARE LICENSE
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2024
|
|
3
|
+
Copyright (c) 2024-2026 X Enterprises LLC. All Rights Reserved.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
This software and associated documentation files (the "Software") are the
|
|
6
|
+
exclusive property of X Enterprises LLC, a Washington limited liability
|
|
7
|
+
company ("X Enterprises"). The Software is distributed through public
|
|
8
|
+
package registries (including npm) for operational convenience only; such
|
|
9
|
+
distribution does not grant any rights beyond those expressly stated below.
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
TERMS AND CONDITIONS
|
|
12
|
+
|
|
13
|
+
1. OWNERSHIP
|
|
14
|
+
All rights, title, and interest in and to the Software, including all
|
|
15
|
+
intellectual property rights, are and shall remain the exclusive property
|
|
16
|
+
of X Enterprises. No rights are granted except as expressly set forth in
|
|
17
|
+
this License.
|
|
18
|
+
|
|
19
|
+
2. PERMITTED USE
|
|
20
|
+
Subject to the restrictions in Section 3, you are permitted to download,
|
|
21
|
+
install, and execute the Software solely as a dependency of:
|
|
22
|
+
|
|
23
|
+
(a) software developed, owned, or operated by X Enterprises;
|
|
24
|
+
|
|
25
|
+
(b) software that X Enterprises has developed, delivered, or licensed to
|
|
26
|
+
a third party ("Client") under a written engagement agreement with
|
|
27
|
+
X Enterprises, when such use is performed by or on behalf of that
|
|
28
|
+
Client; or
|
|
29
|
+
|
|
30
|
+
(c) end-user access to, or consumption of, a product or service described
|
|
31
|
+
in (a) or (b), provided that such access does not involve
|
|
32
|
+
redistribution, modification, or separate use of the Software.
|
|
33
|
+
|
|
34
|
+
Permitted Use includes automated installation and execution by continuous
|
|
35
|
+
integration systems, container builds, hosting platforms, and similar
|
|
36
|
+
infrastructure, to the extent necessary to support (a), (b), or (c).
|
|
37
|
+
|
|
38
|
+
3. RESTRICTIONS
|
|
39
|
+
Except as expressly permitted in Section 2, and without the prior written
|
|
40
|
+
consent of X Enterprises, you may not:
|
|
41
|
+
|
|
42
|
+
(a) copy, modify, adapt, translate, or create derivative works of the
|
|
43
|
+
Software for any purpose outside the scope of Section 2;
|
|
44
|
+
|
|
45
|
+
(b) redistribute, republish, sublicense, sell, lease, rent, or otherwise
|
|
46
|
+
transfer the Software, in whole or in part, whether standalone or
|
|
47
|
+
bundled with other software;
|
|
48
|
+
|
|
49
|
+
(c) reverse engineer, decompile, disassemble, or attempt to derive the
|
|
50
|
+
source code or underlying ideas, algorithms, structure, or
|
|
51
|
+
organization of the Software, except to the extent such activity is
|
|
52
|
+
expressly permitted by applicable law notwithstanding this
|
|
53
|
+
restriction;
|
|
54
|
+
|
|
55
|
+
(d) use the Software, in whole or in part, to develop, operate, or
|
|
56
|
+
provide any product or service that competes with or substitutes for
|
|
57
|
+
any X Enterprises product or service;
|
|
58
|
+
|
|
59
|
+
(e) remove, obscure, or alter any copyright, trademark, license, or other
|
|
60
|
+
proprietary notice contained in or on the Software; or
|
|
61
|
+
|
|
62
|
+
(f) use the Software in violation of any applicable law or regulation.
|
|
63
|
+
|
|
64
|
+
4. NO WARRANTY
|
|
65
|
+
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
66
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
67
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
68
|
+
X ENTERPRISES BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
|
|
69
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT
|
|
70
|
+
OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
71
|
+
THE SOFTWARE.
|
|
72
|
+
|
|
73
|
+
5. LIMITATION OF LIABILITY
|
|
74
|
+
IN NO EVENT SHALL X ENTERPRISES BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
|
|
75
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
|
76
|
+
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
77
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
78
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
79
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
80
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
81
|
+
|
|
82
|
+
6. GOVERNING LAW
|
|
83
|
+
This License shall be governed by and construed in accordance with the
|
|
84
|
+
laws of the State of Washington, United States, without regard to its
|
|
85
|
+
conflict of law provisions. Exclusive jurisdiction for any dispute
|
|
86
|
+
arising out of this License shall lie in the state or federal courts
|
|
87
|
+
located in King County, Washington.
|
|
88
|
+
|
|
89
|
+
7. TERMINATION
|
|
90
|
+
This License is effective until terminated. Your rights under this
|
|
91
|
+
License will terminate automatically and without notice if you fail to
|
|
92
|
+
comply with any term herein. Upon termination, you must cease all use of
|
|
93
|
+
the Software and destroy all copies in your possession or control.
|
|
94
|
+
Sections 1, 3, 4, 5, 6, and 7 survive termination.
|
|
95
|
+
|
|
96
|
+
For licensing inquiries, contact: legal@x.enterprises
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
X Enterprises LLC
|
|
100
|
+
Bothell, Washington, United States
|
package/README.md
CHANGED
|
@@ -1,26 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @xenterprises/fastify-xstripe
|
|
2
2
|
|
|
3
|
-
Fastify v5 plugin for
|
|
3
|
+
Fastify v5 plugin for Stripe webhook handling with built-in signature verification, 23 default event handlers, and the Stripe client decorated on the Fastify instance.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
- **Fastify v5.0.0+**
|
|
8
|
-
- **Node.js v20+**
|
|
9
|
-
|
|
10
|
-
## Features
|
|
11
|
-
|
|
12
|
-
- **Simple webhook handling** - Clean event-based architecture
|
|
13
|
-
- **Built-in handlers** - 20+ default handlers for common events
|
|
14
|
-
- **Easy to test** - Handlers are pure functions
|
|
15
|
-
- **Type-safe** - Full TypeScript support
|
|
16
|
-
- **Readable** - Clear, self-documenting code
|
|
17
|
-
- **Flexible** - Override any default handler
|
|
18
|
-
- **Production-ready** - Signature verification, error handling, logging
|
|
19
|
-
|
|
20
|
-
## Installation
|
|
5
|
+
## Install
|
|
21
6
|
|
|
22
7
|
```bash
|
|
23
|
-
npm install @xenterprises/fastify-xstripe
|
|
8
|
+
npm install @xenterprises/fastify-xstripe stripe
|
|
24
9
|
```
|
|
25
10
|
|
|
26
11
|
## Quick Start
|
|
@@ -34,298 +19,168 @@ const fastify = Fastify({ logger: true });
|
|
|
34
19
|
await fastify.register(xStripe, {
|
|
35
20
|
apiKey: process.env.STRIPE_API_KEY,
|
|
36
21
|
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
|
37
|
-
webhookPath: '/stripe/webhook', // Optional, defaults to /stripe/webhook
|
|
38
22
|
});
|
|
39
23
|
|
|
24
|
+
// Use the Stripe client directly
|
|
25
|
+
const customer = await fastify.stripe.customers.create({ email: 'user@example.com' });
|
|
26
|
+
|
|
40
27
|
await fastify.listen({ port: 3000 });
|
|
41
28
|
```
|
|
42
29
|
|
|
30
|
+
## Options
|
|
31
|
+
|
|
32
|
+
| Name | Type | Default | Required | Description |
|
|
33
|
+
|------|------|---------|----------|-------------|
|
|
34
|
+
| `apiKey` | `string` | — | Yes | Stripe secret API key (`sk_test_...` or `sk_live_...`) |
|
|
35
|
+
| `webhookSecret` | `string` | — | Yes | Stripe webhook signing secret (`whsec_...`) |
|
|
36
|
+
| `webhookPath` | `string` | `"/stripe/webhook"` | No | Path where the webhook POST route is registered |
|
|
37
|
+
| `handlers` | `object` | `{}` | No | Custom event handlers that override the defaults |
|
|
38
|
+
| `apiVersion` | `string` | `"2024-11-20.acacia"` | No | Stripe API version |
|
|
39
|
+
|
|
40
|
+
All options are validated at startup. Invalid or missing required options throw with an `[xStripe]` prefix.
|
|
41
|
+
|
|
42
|
+
## Decorated Properties
|
|
43
|
+
|
|
44
|
+
| Property | Type | Description |
|
|
45
|
+
|----------|------|-------------|
|
|
46
|
+
| `fastify.stripe` | `Stripe` | The initialized Stripe SDK client — use it to call any Stripe API |
|
|
47
|
+
|
|
43
48
|
## Custom Handlers
|
|
44
49
|
|
|
45
|
-
Override default
|
|
50
|
+
Override any default handler with your business logic. Handlers receive `(event, fastify, stripe)`:
|
|
46
51
|
|
|
47
52
|
```javascript
|
|
48
53
|
await fastify.register(xStripe, {
|
|
49
54
|
apiKey: process.env.STRIPE_API_KEY,
|
|
50
55
|
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
|
51
56
|
handlers: {
|
|
52
|
-
// Handle new subscription
|
|
53
57
|
'customer.subscription.created': async (event, fastify, stripe) => {
|
|
54
58
|
const subscription = event.data.object;
|
|
55
|
-
|
|
56
|
-
// Update your database
|
|
57
|
-
await fastify.prisma.user.update({
|
|
59
|
+
await db.users.update({
|
|
58
60
|
where: { stripeCustomerId: subscription.customer },
|
|
59
|
-
data: {
|
|
60
|
-
subscriptionId: subscription.id,
|
|
61
|
-
subscriptionStatus: subscription.status,
|
|
62
|
-
planId: subscription.items.data[0]?.price.id,
|
|
63
|
-
},
|
|
61
|
+
data: { subscriptionId: subscription.id, status: subscription.status },
|
|
64
62
|
});
|
|
65
|
-
|
|
66
|
-
// Send welcome email
|
|
67
|
-
await fastify.email.send(
|
|
68
|
-
subscription.customer.email,
|
|
69
|
-
'Welcome to Premium!',
|
|
70
|
-
'<h1>Thanks for subscribing!</h1>'
|
|
71
|
-
);
|
|
72
63
|
},
|
|
73
|
-
|
|
74
|
-
// Handle subscription cancellation
|
|
75
|
-
'customer.subscription.deleted': async (event, fastify, stripe) => {
|
|
76
|
-
const subscription = event.data.object;
|
|
77
|
-
|
|
78
|
-
// Revoke access
|
|
79
|
-
await fastify.prisma.user.update({
|
|
80
|
-
where: { stripeSubscriptionId: subscription.id },
|
|
81
|
-
data: {
|
|
82
|
-
subscriptionStatus: 'canceled',
|
|
83
|
-
hasAccess: false,
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
// Handle failed payment
|
|
89
64
|
'invoice.payment_failed': async (event, fastify, stripe) => {
|
|
90
65
|
const invoice = event.data.object;
|
|
91
|
-
|
|
92
|
-
// Send payment failure email
|
|
93
66
|
const customer = await stripe.customers.retrieve(invoice.customer);
|
|
94
|
-
await
|
|
95
|
-
customer.email,
|
|
96
|
-
'Payment Failed',
|
|
97
|
-
'<p>Please update your payment method.</p>'
|
|
98
|
-
);
|
|
67
|
+
await sendEmail(customer.email, 'Payment Failed', 'Please update your card.');
|
|
99
68
|
},
|
|
100
69
|
},
|
|
101
70
|
});
|
|
102
71
|
```
|
|
103
72
|
|
|
104
|
-
##
|
|
105
|
-
|
|
106
|
-
All handlers receive three parameters:
|
|
107
|
-
|
|
108
|
-
```javascript
|
|
109
|
-
async function handler(event, fastify, stripe) {
|
|
110
|
-
// event - The Stripe webhook event object
|
|
111
|
-
// fastify - The Fastify instance (access to decorators)
|
|
112
|
-
// stripe - The Stripe client instance
|
|
113
|
-
}
|
|
114
|
-
```
|
|
73
|
+
## Default Event Handlers
|
|
115
74
|
|
|
116
|
-
|
|
75
|
+
All 23 built-in handlers log structured data via `fastify.log`. Override any of them via the `handlers` option.
|
|
117
76
|
|
|
118
77
|
### Subscription Events
|
|
119
|
-
- `customer.subscription.created`
|
|
120
|
-
- `customer.subscription.updated`
|
|
121
|
-
- `customer.subscription.deleted`
|
|
122
|
-
- `customer.subscription.trial_will_end`
|
|
123
|
-
- `customer.subscription.paused`
|
|
124
|
-
- `customer.subscription.resumed`
|
|
78
|
+
- `customer.subscription.created` — logs subscriptionId, customerId, status, planId
|
|
79
|
+
- `customer.subscription.updated` — logs subscriptionId, customerId, status, previous changes
|
|
80
|
+
- `customer.subscription.deleted` — logs subscriptionId, customerId, canceledAt
|
|
81
|
+
- `customer.subscription.trial_will_end` — logs subscriptionId, customerId, trialEnd
|
|
82
|
+
- `customer.subscription.paused` — logs subscriptionId, customerId
|
|
83
|
+
- `customer.subscription.resumed` — logs subscriptionId, customerId
|
|
125
84
|
|
|
126
85
|
### Invoice Events
|
|
127
|
-
- `invoice.created`
|
|
128
|
-
- `invoice.finalized`
|
|
129
|
-
- `invoice.paid`
|
|
130
|
-
- `invoice.payment_failed`
|
|
131
|
-
- `invoice.upcoming`
|
|
86
|
+
- `invoice.created` — logs invoiceId, customerId, amount, status
|
|
87
|
+
- `invoice.finalized` — logs invoiceId, customerId, amount
|
|
88
|
+
- `invoice.paid` — logs invoiceId, customerId, subscriptionId, amount
|
|
89
|
+
- `invoice.payment_failed` — logs (warn) invoiceId, customerId, amount, attemptCount
|
|
90
|
+
- `invoice.upcoming` — logs customerId, subscriptionId, amount, periodEnd
|
|
132
91
|
|
|
133
92
|
### Payment Events
|
|
134
|
-
- `payment_intent.succeeded`
|
|
135
|
-
- `payment_intent.payment_failed`
|
|
93
|
+
- `payment_intent.succeeded` — logs paymentIntentId, customerId, amount, currency
|
|
94
|
+
- `payment_intent.payment_failed` — logs (warn) paymentIntentId, customerId, amount, lastPaymentError
|
|
136
95
|
|
|
137
96
|
### Customer Events
|
|
138
|
-
- `customer.created`
|
|
139
|
-
- `customer.updated`
|
|
140
|
-
- `customer.deleted`
|
|
97
|
+
- `customer.created` — logs customerId, email
|
|
98
|
+
- `customer.updated` — logs customerId, previous changes
|
|
99
|
+
- `customer.deleted` — logs customerId
|
|
141
100
|
|
|
142
101
|
### Payment Method Events
|
|
143
|
-
- `payment_method.attached`
|
|
144
|
-
- `payment_method.detached`
|
|
102
|
+
- `payment_method.attached` — logs paymentMethodId, customerId, type
|
|
103
|
+
- `payment_method.detached` — logs paymentMethodId, type
|
|
145
104
|
|
|
146
105
|
### Checkout Events
|
|
147
|
-
- `checkout.session.completed`
|
|
148
|
-
- `checkout.session.expired`
|
|
106
|
+
- `checkout.session.completed` — logs sessionId, customerId, subscriptionId, mode, paymentStatus
|
|
107
|
+
- `checkout.session.expired` — logs sessionId
|
|
149
108
|
|
|
150
|
-
|
|
109
|
+
### Charge Events
|
|
110
|
+
- `charge.succeeded` — logs chargeId, customerId, amount, currency, paymentMethod
|
|
111
|
+
- `charge.failed` — logs (error) chargeId, customerId, amount, failureCode, failureMessage
|
|
112
|
+
- `charge.refunded` — logs chargeId, customerId, amountRefunded, refundCount
|
|
151
113
|
|
|
152
|
-
|
|
114
|
+
## Helper Utilities
|
|
153
115
|
|
|
154
|
-
|
|
155
|
-
# Install Stripe CLI
|
|
156
|
-
brew install stripe/stripe-cli/stripe
|
|
157
|
-
|
|
158
|
-
# Login to Stripe
|
|
159
|
-
stripe login
|
|
160
|
-
|
|
161
|
-
# Forward webhooks to your local server
|
|
162
|
-
stripe listen --forward-to localhost:3000/stripe/webhook
|
|
163
|
-
|
|
164
|
-
# Trigger test events
|
|
165
|
-
stripe trigger customer.subscription.created
|
|
166
|
-
stripe trigger invoice.payment_failed
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### 2. Test Handlers Directly
|
|
170
|
-
|
|
171
|
-
Since handlers are pure functions, they're easy to test:
|
|
172
|
-
|
|
173
|
-
```javascript
|
|
174
|
-
import { test } from 'node:test';
|
|
175
|
-
import assert from 'node:assert';
|
|
176
|
-
|
|
177
|
-
test('subscription.created handler', async () => {
|
|
178
|
-
const mockEvent = {
|
|
179
|
-
type: 'customer.subscription.created',
|
|
180
|
-
data: {
|
|
181
|
-
object: {
|
|
182
|
-
id: 'sub_123',
|
|
183
|
-
customer: 'cus_123',
|
|
184
|
-
status: 'active',
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const mockFastify = {
|
|
190
|
-
log: { info: () => {} },
|
|
191
|
-
prisma: {
|
|
192
|
-
user: {
|
|
193
|
-
update: async (data) => {
|
|
194
|
-
assert.equal(data.where.stripeCustomerId, 'cus_123');
|
|
195
|
-
return {};
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
const mockStripe = {};
|
|
202
|
-
|
|
203
|
-
await handlers['customer.subscription.created'](
|
|
204
|
-
mockEvent,
|
|
205
|
-
mockFastify,
|
|
206
|
-
mockStripe
|
|
207
|
-
);
|
|
208
|
-
});
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
## Common Patterns
|
|
212
|
-
|
|
213
|
-
### Update Database on Subscription Change
|
|
214
|
-
|
|
215
|
-
```javascript
|
|
216
|
-
'customer.subscription.updated': async (event, fastify, stripe) => {
|
|
217
|
-
const subscription = event.data.object;
|
|
218
|
-
const previous = event.data.previous_attributes || {};
|
|
219
|
-
|
|
220
|
-
// Check what changed
|
|
221
|
-
if ('status' in previous) {
|
|
222
|
-
await fastify.prisma.user.update({
|
|
223
|
-
where: { stripeSubscriptionId: subscription.id },
|
|
224
|
-
data: { subscriptionStatus: subscription.status },
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// Handle specific status changes
|
|
228
|
-
if (subscription.status === 'past_due') {
|
|
229
|
-
// Send payment reminder
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Send Notification Emails
|
|
116
|
+
Import from `@xenterprises/fastify-xstripe/helpers`:
|
|
236
117
|
|
|
237
118
|
```javascript
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
119
|
+
import { helpers } from '@xenterprises/fastify-xstripe';
|
|
120
|
+
|
|
121
|
+
helpers.formatAmount(2000, 'USD'); // "$20.00"
|
|
122
|
+
helpers.getPlanName(subscription); // "Pro Plan"
|
|
123
|
+
helpers.isActiveSubscription(subscription); // true
|
|
124
|
+
helpers.isInTrial(subscription); // true/false
|
|
125
|
+
helpers.getDaysUntilTrialEnd(subscription); // 3
|
|
126
|
+
helpers.isRenewal(event); // true/false
|
|
127
|
+
helpers.calculateMRR(subscription); // 2000 (cents)
|
|
128
|
+
helpers.getSubscriptionStatusText('active'); // "Active"
|
|
129
|
+
helpers.getEventDescription(event); // "Payment received"
|
|
130
|
+
helpers.getCustomerEmail(event, stripe); // "user@example.com"
|
|
131
|
+
helpers.isTestEvent(event); // true/false
|
|
132
|
+
helpers.getMetadata(event); // { key: "value" }
|
|
133
|
+
helpers.getPaymentMethodType(pm); // "Card"
|
|
134
|
+
helpers.getInvoiceLineItems(invoice); // [{ description, amount, ... }]
|
|
135
|
+
helpers.isSubscriptionInvoice(invoice); // true/false
|
|
136
|
+
helpers.getNextBillingDate(subscription); // Date
|
|
137
|
+
helpers.formatDate(1700000000); // "November 14, 2023"
|
|
248
138
|
```
|
|
249
139
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
```javascript
|
|
253
|
-
'invoice.payment_failed': async (event, fastify, stripe) => {
|
|
254
|
-
const invoice = event.data.object;
|
|
255
|
-
|
|
256
|
-
await fastify.prisma.user.update({
|
|
257
|
-
where: { stripeSubscriptionId: invoice.subscription },
|
|
258
|
-
data: {
|
|
259
|
-
failedPaymentCount: { increment: 1 },
|
|
260
|
-
lastFailedPayment: new Date(),
|
|
261
|
-
},
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// After 3 failed payments, suspend account
|
|
265
|
-
const user = await fastify.prisma.user.findUnique({
|
|
266
|
-
where: { stripeSubscriptionId: invoice.subscription },
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
if (user.failedPaymentCount >= 3) {
|
|
270
|
-
await fastify.prisma.user.update({
|
|
271
|
-
where: { id: user.id },
|
|
272
|
-
data: { accountSuspended: true },
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
```
|
|
140
|
+
## Environment Variables
|
|
277
141
|
|
|
278
|
-
|
|
142
|
+
| Name | Required | Description |
|
|
143
|
+
|------|----------|-------------|
|
|
144
|
+
| `STRIPE_API_KEY` | Yes | Stripe secret key (`sk_test_...` or `sk_live_...`) |
|
|
145
|
+
| `STRIPE_WEBHOOK_SECRET` | Yes | Webhook signing secret from Stripe Dashboard or CLI (`whsec_...`) |
|
|
279
146
|
|
|
280
|
-
|
|
281
|
-
|--------|------|----------|---------|-------------|
|
|
282
|
-
| `apiKey` | string | Yes | - | Stripe API key |
|
|
283
|
-
| `webhookSecret` | string | No | - | Stripe webhook signing secret |
|
|
284
|
-
| `webhookPath` | string | No | `/stripe/webhook` | Webhook endpoint path |
|
|
285
|
-
| `handlers` | object | No | `{}` | Custom event handlers |
|
|
286
|
-
| `apiVersion` | string | No | `2024-11-20.acacia` | Stripe API version |
|
|
147
|
+
## Error Reference
|
|
287
148
|
|
|
288
|
-
|
|
149
|
+
All errors use the `[xStripe]` prefix for easy identification in logs.
|
|
289
150
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
151
|
+
| Error | When |
|
|
152
|
+
|-------|------|
|
|
153
|
+
| `[xStripe] apiKey is required and must be a string` | Missing or non-string `apiKey` option |
|
|
154
|
+
| `[xStripe] webhookSecret is required and must be a string` | Missing or non-string `webhookSecret` option |
|
|
155
|
+
| `[xStripe] webhookPath must be a string` | Non-string `webhookPath` option |
|
|
156
|
+
| `[xStripe] handlers must be a plain object` | `handlers` is not an object or is an array |
|
|
157
|
+
| `[xStripe] apiVersion must be a string` | Non-string `apiVersion` option |
|
|
158
|
+
| `[xStripe] Missing stripe-signature header` | Webhook request without signature header (HTTP 400) |
|
|
159
|
+
| `[xStripe] Webhook signature verification failed: ...` | Invalid webhook signature (HTTP 400) |
|
|
294
160
|
|
|
295
|
-
##
|
|
161
|
+
## How It Works
|
|
296
162
|
|
|
297
|
-
1. **
|
|
298
|
-
2. **
|
|
299
|
-
3. **
|
|
300
|
-
4. **
|
|
301
|
-
5. **Monitor failed handlers** - Set up alerts for handler errors
|
|
163
|
+
1. **Registration** — Validates all options, initializes the Stripe SDK client, and decorates it as `fastify.stripe`.
|
|
164
|
+
2. **Webhook Route** — Registers a POST route at `webhookPath` that reads the raw body, verifies the Stripe signature using `stripe.webhooks.constructEvent()`, and dispatches to the matching handler.
|
|
165
|
+
3. **Handler Dispatch** — User-provided handlers override defaults via object spread (`{ ...defaultHandlers, ...userHandlers }`). If a handler throws, the error is logged but the webhook still returns HTTP 200 to prevent Stripe retries.
|
|
166
|
+
4. **Stripe Client** — The `fastify.stripe` decorator gives full access to the Stripe SDK for any API call (customers, subscriptions, invoices, etc.).
|
|
302
167
|
|
|
303
|
-
##
|
|
168
|
+
## Testing Webhooks Locally
|
|
304
169
|
|
|
305
170
|
```bash
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
## Integration with Other xPlugins
|
|
171
|
+
# Install Stripe CLI
|
|
172
|
+
brew install stripe/stripe-cli/stripe
|
|
311
173
|
|
|
312
|
-
|
|
174
|
+
# Login and forward webhooks
|
|
175
|
+
stripe login
|
|
176
|
+
stripe listen --forward-to localhost:3000/stripe/webhook
|
|
313
177
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
// In your handlers:
|
|
320
|
-
'invoice.payment_failed': async (event, fastify, stripe) => {
|
|
321
|
-
// Send email via xConfig/SendGrid
|
|
322
|
-
await fastify.email.send(/* ... */);
|
|
323
|
-
|
|
324
|
-
// Send SMS via xTwilio
|
|
325
|
-
await fastify.sms.send(/* ... */);
|
|
326
|
-
}
|
|
178
|
+
# Trigger test events
|
|
179
|
+
stripe trigger customer.subscription.created
|
|
180
|
+
stripe trigger invoice.payment_failed
|
|
181
|
+
stripe trigger checkout.session.completed
|
|
327
182
|
```
|
|
328
183
|
|
|
329
184
|
## License
|
|
330
185
|
|
|
331
|
-
|
|
186
|
+
UNLICENSED
|
package/index.d.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* TypeScript Type Definitions
|
|
4
4
|
*
|
|
5
5
|
* @module @xenterprises/fastify-xstripe
|
|
6
|
-
* @version 1.
|
|
6
|
+
* @version 1.2.0
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { FastifyPluginAsync, FastifyInstance
|
|
9
|
+
import { FastifyPluginAsync, FastifyInstance } from 'fastify';
|
|
10
10
|
import Stripe from 'stripe';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -16,6 +16,10 @@ export type StripeWebhookEvent = Stripe.Event;
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Webhook Event Handler Function
|
|
19
|
+
*
|
|
20
|
+
* @param event - The Stripe webhook event object
|
|
21
|
+
* @param fastify - The Fastify instance (access to decorators)
|
|
22
|
+
* @param stripe - The Stripe client instance
|
|
19
23
|
*/
|
|
20
24
|
export type WebhookEventHandler = (
|
|
21
25
|
event: StripeWebhookEvent,
|
|
@@ -72,94 +76,20 @@ export interface DefaultHandlers {
|
|
|
72
76
|
* Plugin Configuration Options
|
|
73
77
|
*/
|
|
74
78
|
export interface XStripePluginOptions {
|
|
75
|
-
/** Stripe API Key (Secret Key) */
|
|
76
|
-
apiKey
|
|
79
|
+
/** Stripe API Key (Secret Key). Required. */
|
|
80
|
+
apiKey: string;
|
|
77
81
|
|
|
78
|
-
/** Stripe Webhook Signing Secret */
|
|
79
|
-
webhookSecret
|
|
82
|
+
/** Stripe Webhook Signing Secret. Required. */
|
|
83
|
+
webhookSecret: string;
|
|
80
84
|
|
|
81
|
-
/** Webhook endpoint path */
|
|
85
|
+
/** Webhook endpoint path. Defaults to "/stripe/webhook". */
|
|
82
86
|
webhookPath?: string;
|
|
83
87
|
|
|
84
|
-
/** Custom event handlers (override defaults) */
|
|
88
|
+
/** Custom event handlers (override defaults). */
|
|
85
89
|
handlers?: Partial<DefaultHandlers>;
|
|
86
90
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
/** Request timeout in milliseconds */
|
|
91
|
-
requestTimeout?: number;
|
|
92
|
-
|
|
93
|
-
/** Enable event logging */
|
|
94
|
-
logEvents?: boolean;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Helper Functions
|
|
99
|
-
*/
|
|
100
|
-
export namespace helpers {
|
|
101
|
-
/**
|
|
102
|
-
* Format amount as currency string
|
|
103
|
-
* @param amount Amount in cents
|
|
104
|
-
* @param currency Currency code (USD, EUR, etc.)
|
|
105
|
-
* @returns Formatted currency string (e.g., "$20.00")
|
|
106
|
-
*/
|
|
107
|
-
function formatAmount(amount: number, currency: string): string;
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Get plan name from subscription
|
|
111
|
-
* @param subscription Subscription object
|
|
112
|
-
* @returns Plan name or product name
|
|
113
|
-
*/
|
|
114
|
-
function getPlanName(subscription: Stripe.Subscription): string;
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Check if subscription is active
|
|
118
|
-
* @param subscription Subscription object
|
|
119
|
-
* @returns True if subscription is active
|
|
120
|
-
*/
|
|
121
|
-
function isActiveSubscription(subscription: Stripe.Subscription): boolean;
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Get customer email from various objects
|
|
125
|
-
* @param obj Stripe object with customer reference
|
|
126
|
-
* @param stripe Stripe client instance
|
|
127
|
-
* @returns Customer email
|
|
128
|
-
*/
|
|
129
|
-
function getCustomerEmail(obj: any, stripe: Stripe): Promise<string>;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Create idempotent request ID from event
|
|
133
|
-
* @param event Stripe webhook event
|
|
134
|
-
* @returns Idempotency key
|
|
135
|
-
*/
|
|
136
|
-
function createIdempotencyKey(event: StripeWebhookEvent): string;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* xStripe Service Methods (available on fastify.xStripe)
|
|
141
|
-
*/
|
|
142
|
-
export interface XStripeService {
|
|
143
|
-
/**
|
|
144
|
-
* Register custom event handler
|
|
145
|
-
* @param eventType Stripe event type
|
|
146
|
-
* @param handler Event handler function
|
|
147
|
-
*/
|
|
148
|
-
onEvent(eventType: string, handler: WebhookEventHandler): void;
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Get registered handler for event type
|
|
152
|
-
* @param eventType Stripe event type
|
|
153
|
-
* @returns Handler function or undefined
|
|
154
|
-
*/
|
|
155
|
-
getHandler(eventType: string): WebhookEventHandler | undefined;
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Execute event handler
|
|
159
|
-
* @param event Stripe webhook event
|
|
160
|
-
* @returns Promise that resolves when handler completes
|
|
161
|
-
*/
|
|
162
|
-
executeHandler(event: StripeWebhookEvent): Promise<void>;
|
|
91
|
+
/** Stripe API version. Defaults to "2024-11-20.acacia". */
|
|
92
|
+
apiVersion?: string;
|
|
163
93
|
}
|
|
164
94
|
|
|
165
95
|
/**
|
|
@@ -167,101 +97,77 @@ export interface XStripeService {
|
|
|
167
97
|
*/
|
|
168
98
|
declare module 'fastify' {
|
|
169
99
|
interface FastifyInstance {
|
|
170
|
-
/** xStripe service methods */
|
|
171
|
-
xStripe: XStripeService;
|
|
172
|
-
|
|
173
100
|
/** Stripe client instance */
|
|
174
101
|
stripe: Stripe;
|
|
175
102
|
}
|
|
176
103
|
}
|
|
177
104
|
|
|
178
105
|
/**
|
|
179
|
-
*
|
|
106
|
+
* Helper Functions
|
|
180
107
|
*/
|
|
181
|
-
export
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
id: string;
|
|
185
|
-
type: string;
|
|
186
|
-
data: any;
|
|
187
|
-
[key: string]: any;
|
|
188
|
-
};
|
|
189
|
-
}
|
|
108
|
+
export declare namespace helpers {
|
|
109
|
+
/** Extract customer email from various Stripe objects */
|
|
110
|
+
function getCustomerEmail(event: StripeWebhookEvent, stripe: Stripe): string | Promise<string | null> | null;
|
|
190
111
|
|
|
191
|
-
/**
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
status: 'success' | 'error';
|
|
112
|
+
/** Format amount from cents to currency string */
|
|
113
|
+
function formatAmount(cents: number, currency?: string): string;
|
|
114
|
+
|
|
115
|
+
/** Get plan name from subscription */
|
|
116
|
+
function getPlanName(subscription: Stripe.Subscription): string;
|
|
197
117
|
|
|
198
|
-
/**
|
|
199
|
-
|
|
118
|
+
/** Check if subscription is in trial */
|
|
119
|
+
function isInTrial(subscription: Stripe.Subscription): boolean;
|
|
200
120
|
|
|
201
|
-
/**
|
|
202
|
-
|
|
121
|
+
/** Check if subscription is active (including trialing) */
|
|
122
|
+
function isActiveSubscription(subscription: Stripe.Subscription): boolean;
|
|
203
123
|
|
|
204
|
-
/**
|
|
205
|
-
|
|
124
|
+
/** Get days until trial ends */
|
|
125
|
+
function getDaysUntilTrialEnd(subscription: Stripe.Subscription): number | null;
|
|
206
126
|
|
|
207
|
-
/**
|
|
208
|
-
|
|
209
|
-
}
|
|
127
|
+
/** Check if event is a subscription renewal */
|
|
128
|
+
function isRenewal(event: StripeWebhookEvent): boolean;
|
|
210
129
|
|
|
211
|
-
/**
|
|
212
|
-
|
|
213
|
-
*/
|
|
214
|
-
export interface Plan {
|
|
215
|
-
productId: string;
|
|
216
|
-
name: string;
|
|
217
|
-
description?: string;
|
|
218
|
-
images?: string[];
|
|
219
|
-
prices: PriceInfo[];
|
|
220
|
-
metadata?: Record<string, any>;
|
|
221
|
-
}
|
|
130
|
+
/** Get subscription status display text */
|
|
131
|
+
function getSubscriptionStatusText(status: string): string;
|
|
222
132
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
amount: number;
|
|
226
|
-
currency: string;
|
|
227
|
-
interval?: string;
|
|
228
|
-
intervalCount?: number;
|
|
229
|
-
trialPeriodDays?: number;
|
|
230
|
-
nickname?: string;
|
|
231
|
-
metadata?: Record<string, any>;
|
|
232
|
-
}
|
|
133
|
+
/** Extract metadata from event */
|
|
134
|
+
function getMetadata(event: StripeWebhookEvent): Record<string, string>;
|
|
233
135
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
createdAt: Date;
|
|
246
|
-
}
|
|
136
|
+
/** Check if event is a test event */
|
|
137
|
+
function isTestEvent(event: StripeWebhookEvent): boolean;
|
|
138
|
+
|
|
139
|
+
/** Get human-readable event description */
|
|
140
|
+
function getEventDescription(event: StripeWebhookEvent): string;
|
|
141
|
+
|
|
142
|
+
/** Calculate MRR from subscription (in cents) */
|
|
143
|
+
function calculateMRR(subscription: Stripe.Subscription): number;
|
|
144
|
+
|
|
145
|
+
/** Get payment method type display text */
|
|
146
|
+
function getPaymentMethodType(paymentMethod: Stripe.PaymentMethod | null): string;
|
|
247
147
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
148
|
+
/** Extract line items from invoice */
|
|
149
|
+
function getInvoiceLineItems(invoice: Stripe.Invoice): Array<{
|
|
150
|
+
description: string | null;
|
|
151
|
+
amount: number;
|
|
152
|
+
quantity: number | null;
|
|
153
|
+
priceId: string | undefined;
|
|
154
|
+
}>;
|
|
155
|
+
|
|
156
|
+
/** Check if invoice is for subscription vs one-time payment */
|
|
157
|
+
function isSubscriptionInvoice(invoice: Stripe.Invoice): boolean;
|
|
158
|
+
|
|
159
|
+
/** Get next billing date from subscription */
|
|
160
|
+
function getNextBillingDate(subscription: Stripe.Subscription): Date | null;
|
|
161
|
+
|
|
162
|
+
/** Format date from Unix timestamp */
|
|
163
|
+
function formatDate(unixTimestamp: number | null, locale?: string): string | null;
|
|
260
164
|
}
|
|
261
165
|
|
|
262
166
|
/**
|
|
263
167
|
* xStripe Plugin
|
|
264
|
-
*
|
|
168
|
+
*
|
|
169
|
+
* Registers Stripe webhook handling with signature verification and
|
|
170
|
+
* decorates the Fastify instance with a Stripe client.
|
|
265
171
|
*
|
|
266
172
|
* @example
|
|
267
173
|
* ```typescript
|
|
@@ -273,37 +179,25 @@ export interface SubscriptionInfo {
|
|
|
273
179
|
* await fastify.register(xStripe, {
|
|
274
180
|
* apiKey: process.env.STRIPE_API_KEY,
|
|
275
181
|
* webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
|
276
|
-
* webhookPath: '/
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
182
|
+
* webhookPath: '/stripe/webhook',
|
|
183
|
+
* handlers: {
|
|
184
|
+
* 'customer.subscription.created': async (event, fastify, stripe) => {
|
|
185
|
+
* const subscription = event.data.object;
|
|
186
|
+
* console.log('New subscription:', subscription.id);
|
|
187
|
+
* },
|
|
188
|
+
* },
|
|
283
189
|
* });
|
|
284
|
-
*
|
|
285
|
-
* // Webhook route automatically registered at webhookPath
|
|
286
|
-
* // POST /webhooks/stripe
|
|
287
|
-
*
|
|
288
|
-
* // Use included API endpoints
|
|
289
|
-
* // GET /plans
|
|
290
|
-
* // GET /plans/:productId
|
|
291
|
-
* // POST /create-checkout-session
|
|
292
|
-
* // POST /create-payment-session
|
|
293
|
-
* // GET /customer/:customerId/subscriptions
|
|
294
|
-
* // POST /subscription/:id/update
|
|
295
|
-
* // GET /customer/:customerId/payment-methods
|
|
296
|
-
* // POST /customer/:customerId/payment-methods
|
|
297
|
-
* // POST /customer/:customerId/payment-methods/:paymentMethodId/default
|
|
298
|
-
* // DELETE /customer/:customerId/payment-methods/:paymentMethodId
|
|
299
190
|
* ```
|
|
300
191
|
*/
|
|
301
192
|
declare const xStripe: FastifyPluginAsync<XStripePluginOptions>;
|
|
302
193
|
|
|
303
194
|
export default xStripe;
|
|
195
|
+
export { xStripe };
|
|
304
196
|
|
|
305
|
-
/**
|
|
306
|
-
* Re-export Stripe types for convenience
|
|
307
|
-
*/
|
|
197
|
+
/** Re-export for convenience */
|
|
308
198
|
export { Stripe };
|
|
309
199
|
export type { StripeWebhookEvent as WebhookEvent };
|
|
200
|
+
|
|
201
|
+
/** Default handlers */
|
|
202
|
+
export { defaultHandlers } from './src/handlers/defaultHandlers.js';
|
|
203
|
+
export { exampleHandlers } from './src/handlers/exampleHandlers.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xenterprises/fastify-xstripe",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.1",
|
|
5
5
|
"description": "Fastify plugin for Stripe webhooks with simplified, testable handlers for subscription events.",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"start": "fastify start -l info server/app.js",
|
|
14
14
|
"dev": "fastify start -w -l info -P server/app.js",
|
|
15
|
-
"test": "node --test test/handlers.test.js"
|
|
15
|
+
"test": "node --test test/handlers.test.js test/xStripe.integration.test.js"
|
|
16
16
|
},
|
|
17
17
|
"author": "Tim Mushen",
|
|
18
|
-
"license": "
|
|
18
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
19
19
|
"engines": {
|
|
20
20
|
"node": ">=20.0.0",
|
|
21
21
|
"npm": ">=10.0.0"
|
|
@@ -40,7 +40,6 @@
|
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "^22.7.4",
|
|
42
42
|
"fastify": "^5.1.0",
|
|
43
|
-
"fastify-plugin": "^5.0.0",
|
|
44
43
|
"typescript": "^5.6.3"
|
|
45
44
|
},
|
|
46
45
|
"dependencies": {
|
|
@@ -49,5 +48,8 @@
|
|
|
49
48
|
},
|
|
50
49
|
"peerDependencies": {
|
|
51
50
|
"fastify": "^5.0.0"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
52
54
|
}
|
|
53
55
|
}
|
package/src/webhooks/webhooks.js
CHANGED
|
@@ -20,8 +20,8 @@ export async function setupWebhooks(fastify, options) {
|
|
|
20
20
|
const sig = request.headers["stripe-signature"];
|
|
21
21
|
|
|
22
22
|
if (!sig) {
|
|
23
|
-
fastify.log.error("Missing stripe-signature header");
|
|
24
|
-
return reply.code(400).send({ error: "Missing stripe-signature header" });
|
|
23
|
+
fastify.log.error("[xStripe] Missing stripe-signature header");
|
|
24
|
+
return reply.code(400).send({ error: "[xStripe] Missing stripe-signature header" });
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
let event;
|
|
@@ -33,12 +33,12 @@ export async function setupWebhooks(fastify, options) {
|
|
|
33
33
|
// Verify webhook signature
|
|
34
34
|
event = stripe.webhooks.constructEvent(rawBody, sig, webhookSecret);
|
|
35
35
|
} catch (err) {
|
|
36
|
-
fastify.log.error(`Webhook signature verification failed: ${err.message}`);
|
|
37
|
-
return reply.code(400).send({ error: `Webhook
|
|
36
|
+
fastify.log.error(`[xStripe] Webhook signature verification failed: ${err.message}`);
|
|
37
|
+
return reply.code(400).send({ error: `[xStripe] Webhook signature verification failed: ${err.message}` });
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// Log the event
|
|
41
|
-
fastify.log.info(`Received
|
|
41
|
+
fastify.log.info(`[xStripe] Received webhook: ${event.type}`);
|
|
42
42
|
|
|
43
43
|
// Get handler for this event type
|
|
44
44
|
const handler = eventHandlers[event.type];
|
|
@@ -47,9 +47,9 @@ export async function setupWebhooks(fastify, options) {
|
|
|
47
47
|
try {
|
|
48
48
|
// Execute handler
|
|
49
49
|
await handler(event, fastify, stripe);
|
|
50
|
-
fastify.log.info(`Successfully processed ${event.type}`);
|
|
50
|
+
fastify.log.info(`[xStripe] Successfully processed ${event.type}`);
|
|
51
51
|
} catch (err) {
|
|
52
|
-
fastify.log.error(`Error processing ${event.type}: ${err.message}`);
|
|
52
|
+
fastify.log.error(`[xStripe] Error processing ${event.type}: ${err.message}`);
|
|
53
53
|
// Return 200 to acknowledge receipt, even if processing failed
|
|
54
54
|
// This prevents Stripe from retrying immediately
|
|
55
55
|
return reply.code(200).send({
|
|
@@ -59,7 +59,7 @@ export async function setupWebhooks(fastify, options) {
|
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
} else {
|
|
62
|
-
fastify.log.warn(`No handler registered for event type: ${event.type}`);
|
|
62
|
+
fastify.log.warn(`[xStripe] No handler registered for event type: ${event.type}`);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// Always return 200 to acknowledge receipt
|
|
@@ -67,6 +67,6 @@ export async function setupWebhooks(fastify, options) {
|
|
|
67
67
|
}
|
|
68
68
|
);
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
fastify.log.info(`[xStripe] Webhooks enabled at ${webhookPath}`);
|
|
71
|
+
fastify.log.info(`[xStripe] Registered ${Object.keys(eventHandlers).length} event handlers`);
|
|
72
72
|
}
|
package/src/xStripe.js
CHANGED
|
@@ -13,11 +13,27 @@ async function xStripe(fastify, options) {
|
|
|
13
13
|
} = options;
|
|
14
14
|
|
|
15
15
|
// Validate required options
|
|
16
|
-
if (!apiKey) {
|
|
17
|
-
throw new Error("
|
|
16
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
17
|
+
throw new Error("[xStripe] apiKey is required and must be a string");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
if (!webhookSecret || typeof webhookSecret !== "string") {
|
|
21
|
+
throw new Error("[xStripe] webhookSecret is required and must be a string");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeof webhookPath !== "string") {
|
|
25
|
+
throw new Error("[xStripe] webhookPath must be a string");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof handlers !== "object" || Array.isArray(handlers)) {
|
|
29
|
+
throw new Error("[xStripe] handlers must be a plain object");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof apiVersion !== "string") {
|
|
33
|
+
throw new Error("[xStripe] apiVersion must be a string");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fastify.log.info("[xStripe] Initializing Stripe plugin...");
|
|
21
37
|
|
|
22
38
|
// Initialize Stripe client
|
|
23
39
|
const stripe = new Stripe(apiKey, { apiVersion });
|
|
@@ -25,21 +41,18 @@ async function xStripe(fastify, options) {
|
|
|
25
41
|
// Decorate Fastify with Stripe client
|
|
26
42
|
fastify.decorate("stripe", stripe);
|
|
27
43
|
|
|
28
|
-
// Setup webhook handling
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
});
|
|
36
|
-
} else {
|
|
37
|
-
fastify.log.warn("⚠️ Stripe webhook secret not provided. Webhook handling disabled.");
|
|
38
|
-
}
|
|
44
|
+
// Setup webhook handling
|
|
45
|
+
await setupWebhooks(fastify, {
|
|
46
|
+
stripe,
|
|
47
|
+
webhookSecret,
|
|
48
|
+
webhookPath,
|
|
49
|
+
handlers,
|
|
50
|
+
});
|
|
39
51
|
|
|
40
|
-
|
|
52
|
+
fastify.log.info("[xStripe] Ready");
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
export default fp(xStripe, {
|
|
44
56
|
name: "xStripe",
|
|
57
|
+
fastify: ">=5.0.0",
|
|
45
58
|
});
|
package/.dockerignore
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# Git
|
|
2
|
-
.git
|
|
3
|
-
.gitignore
|
|
4
|
-
.gitattributes
|
|
5
|
-
|
|
6
|
-
# Node modules (will be installed in container)
|
|
7
|
-
node_modules
|
|
8
|
-
npm-debug.log*
|
|
9
|
-
yarn-debug.log*
|
|
10
|
-
yarn-error.log*
|
|
11
|
-
lerna-debug.log*
|
|
12
|
-
|
|
13
|
-
# Dependencies
|
|
14
|
-
.npm
|
|
15
|
-
.eslintcache
|
|
16
|
-
*.tsbuildinfo
|
|
17
|
-
|
|
18
|
-
# Temporary files
|
|
19
|
-
.DS_Store
|
|
20
|
-
Thumbs.db
|
|
21
|
-
*.tmp
|
|
22
|
-
.env.local
|
|
23
|
-
.env.*.local
|
|
24
|
-
|
|
25
|
-
# IDE
|
|
26
|
-
.vscode
|
|
27
|
-
.idea
|
|
28
|
-
*.swp
|
|
29
|
-
*.swo
|
|
30
|
-
*.swn
|
|
31
|
-
*~
|
|
32
|
-
|
|
33
|
-
# Test & Coverage
|
|
34
|
-
coverage
|
|
35
|
-
.nyc_output
|
|
36
|
-
test
|
|
37
|
-
*.test.js
|
|
38
|
-
|
|
39
|
-
# Build artifacts (if any)
|
|
40
|
-
dist
|
|
41
|
-
build
|
|
42
|
-
out
|
|
43
|
-
|
|
44
|
-
# Docker files (not needed in image)
|
|
45
|
-
Dockerfile*
|
|
46
|
-
docker-compose*.yml
|
|
47
|
-
.dockerignore
|
|
48
|
-
|
|
49
|
-
# CI/CD
|
|
50
|
-
.github
|
|
51
|
-
.gitlab-ci.yml
|
|
52
|
-
|
|
53
|
-
# Documentation (optional - include if you want docs in image)
|
|
54
|
-
# *.md
|
|
55
|
-
|
|
56
|
-
# Example/demo files
|
|
57
|
-
examples
|
|
58
|
-
demo
|
|
59
|
-
|
|
60
|
-
# Logs
|
|
61
|
-
logs
|
|
62
|
-
*.log
|
package/.env.example
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# ============================================================================
|
|
2
|
-
# xStripe Fastify Plugin - Environment Configuration
|
|
3
|
-
# ============================================================================
|
|
4
|
-
# This file defines environment variables for the xStripe module.
|
|
5
|
-
# Copy this to .env and update values for your environment.
|
|
6
|
-
# ============================================================================
|
|
7
|
-
|
|
8
|
-
# ============================================================================
|
|
9
|
-
# SERVER CONFIGURATION
|
|
10
|
-
# ============================================================================
|
|
11
|
-
|
|
12
|
-
# Server port (default: 3000)
|
|
13
|
-
PORT=3000
|
|
14
|
-
|
|
15
|
-
# Node environment (development | production | test)
|
|
16
|
-
NODE_ENV=development
|
|
17
|
-
|
|
18
|
-
# Server hostname/domain (used in webhook URLs and responses)
|
|
19
|
-
# Update this to your actual domain in production
|
|
20
|
-
DOMAIN=http://localhost:3000
|
|
21
|
-
|
|
22
|
-
# ============================================================================
|
|
23
|
-
# STRIPE CONFIGURATION - REQUIRED
|
|
24
|
-
# ============================================================================
|
|
25
|
-
|
|
26
|
-
# Stripe API Key (Secret Key)
|
|
27
|
-
# Get from: https://dashboard.stripe.com/apikeys
|
|
28
|
-
# Format: sk_test_... (test mode) or sk_live_... (production)
|
|
29
|
-
# NEVER commit this value - use environment variables only
|
|
30
|
-
STRIPE_API_KEY=sk_test_your_key_here
|
|
31
|
-
|
|
32
|
-
# Stripe Webhook Signing Secret
|
|
33
|
-
# Get from: https://dashboard.stripe.com/webhooks
|
|
34
|
-
# Copy the "Signing secret" for your webhook endpoint
|
|
35
|
-
# Format: whsec_...
|
|
36
|
-
STRIPE_WEBHOOK_SECRET=whsec_your_secret_here
|
|
37
|
-
|
|
38
|
-
# ============================================================================
|
|
39
|
-
# WEBHOOK CONFIGURATION
|
|
40
|
-
# ============================================================================
|
|
41
|
-
|
|
42
|
-
# Webhook endpoint path (relative to domain)
|
|
43
|
-
# Default: /webhooks/stripe
|
|
44
|
-
# STRIPE_WEBHOOK_PATH=/webhooks/stripe
|
|
45
|
-
|
|
46
|
-
# Webhook events to listen for (space or comma-separated)
|
|
47
|
-
# Default: all supported events
|
|
48
|
-
# Examples:
|
|
49
|
-
# customer.created,customer.updated,customer.deleted
|
|
50
|
-
# invoice.created,invoice.finalized,invoice.paid
|
|
51
|
-
# charge.succeeded,charge.failed
|
|
52
|
-
# STRIPE_WEBHOOK_EVENTS=*
|
|
53
|
-
|
|
54
|
-
# ============================================================================
|
|
55
|
-
# LOGGING & MONITORING
|
|
56
|
-
# ============================================================================
|
|
57
|
-
|
|
58
|
-
# Fastify logger level (trace, debug, info, warn, error, fatal)
|
|
59
|
-
LOG_LEVEL=info
|
|
60
|
-
|
|
61
|
-
# Enable webhook event logging
|
|
62
|
-
# LOG_WEBHOOK_EVENTS=true
|
|
63
|
-
|
|
64
|
-
# Enable handler execution logging
|
|
65
|
-
# LOG_HANDLER_EXECUTION=true
|
|
66
|
-
|
|
67
|
-
# ============================================================================
|
|
68
|
-
# ERROR HANDLING & RECOVERY
|
|
69
|
-
# ============================================================================
|
|
70
|
-
|
|
71
|
-
# Retry failed webhook handler executions
|
|
72
|
-
# ENABLE_HANDLER_RETRY=false
|
|
73
|
-
|
|
74
|
-
# Max retry attempts for failed handlers
|
|
75
|
-
# HANDLER_RETRY_MAX_ATTEMPTS=3
|
|
76
|
-
|
|
77
|
-
# Retry delay in milliseconds
|
|
78
|
-
# HANDLER_RETRY_DELAY_MS=5000
|
|
79
|
-
|
|
80
|
-
# ============================================================================
|
|
81
|
-
# SECURITY
|
|
82
|
-
# ============================================================================
|
|
83
|
-
|
|
84
|
-
# Enable HTTPS enforcement (if behind reverse proxy)
|
|
85
|
-
# HTTPS_ONLY=false
|
|
86
|
-
|
|
87
|
-
# Rate limiting: max webhook requests per minute
|
|
88
|
-
# WEBHOOK_RATE_LIMIT_WINDOW=1m
|
|
89
|
-
# WEBHOOK_RATE_LIMIT_MAX=1000
|
|
90
|
-
|
|
91
|
-
# Timeout for webhook handler execution (milliseconds)
|
|
92
|
-
# Default: 30000 (30 seconds)
|
|
93
|
-
# WEBHOOK_HANDLER_TIMEOUT=30000
|
|
94
|
-
|
|
95
|
-
# ============================================================================
|
|
96
|
-
# STRIPE ACCOUNT CONFIGURATION
|
|
97
|
-
# ============================================================================
|
|
98
|
-
|
|
99
|
-
# Stripe API Version (optional - uses account default if not set)
|
|
100
|
-
# Format: YYYY-MM-DD or leave empty
|
|
101
|
-
# STRIPE_API_VERSION=
|
|
102
|
-
|
|
103
|
-
# Enable Stripe CLI webhooks in development
|
|
104
|
-
# Set to true if using: stripe listen --forward-to localhost:3000/webhooks/stripe
|
|
105
|
-
# STRIPE_CLI_MODE=false
|
|
106
|
-
|
|
107
|
-
# ============================================================================
|
|
108
|
-
# DATABASE/STORAGE (optional - for persisting event data)
|
|
109
|
-
# ============================================================================
|
|
110
|
-
|
|
111
|
-
# Database URL for event persistence (optional)
|
|
112
|
-
# Example: postgresql://user:pass@localhost/stripe_events
|
|
113
|
-
# DATABASE_URL=
|
|
114
|
-
|
|
115
|
-
# Enable event persistence
|
|
116
|
-
# PERSIST_WEBHOOK_EVENTS=false
|
package/.gitlab-ci.yml
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# ============================================================================
|
|
2
|
-
# GitLab CI/CD Pipeline - xStripe
|
|
3
|
-
# ============================================================================
|
|
4
|
-
# Runs tests on merge requests and commits to main/master
|
|
5
|
-
|
|
6
|
-
stages:
|
|
7
|
-
- test
|
|
8
|
-
|
|
9
|
-
variables:
|
|
10
|
-
NODE_ENV: test
|
|
11
|
-
|
|
12
|
-
# ============================================================================
|
|
13
|
-
# Shared Configuration
|
|
14
|
-
# ============================================================================
|
|
15
|
-
.shared_rules: &shared_rules
|
|
16
|
-
rules:
|
|
17
|
-
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
18
|
-
- if: '$CI_COMMIT_BRANCH == "main"'
|
|
19
|
-
- if: '$CI_COMMIT_BRANCH == "master"'
|
|
20
|
-
- if: '$CI_COMMIT_TAG'
|
|
21
|
-
|
|
22
|
-
# ============================================================================
|
|
23
|
-
# STAGE: TEST
|
|
24
|
-
# ============================================================================
|
|
25
|
-
test:
|
|
26
|
-
stage: test
|
|
27
|
-
image: node:20-alpine
|
|
28
|
-
<<: *shared_rules
|
|
29
|
-
|
|
30
|
-
cache:
|
|
31
|
-
key: ${CI_COMMIT_REF_SLUG}
|
|
32
|
-
paths:
|
|
33
|
-
- node_modules/
|
|
34
|
-
|
|
35
|
-
before_script:
|
|
36
|
-
- npm ci
|
|
37
|
-
|
|
38
|
-
script:
|
|
39
|
-
- echo "Running xStripe tests..."
|
|
40
|
-
- npm test
|
|
41
|
-
- npm audit --audit-level=high || true
|
|
42
|
-
|
|
43
|
-
retry:
|
|
44
|
-
max: 2
|
|
45
|
-
when: runner_system_failure
|