@xenterprises/fastify-xstripe 1.1.0 → 1.2.0

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 CHANGED
@@ -1,15 +1,60 @@
1
- ISC License
1
+ PROPRIETARY SOFTWARE LICENSE
2
2
 
3
- Copyright (c) 2024 Tim Mushen
3
+ Copyright (c) 2024-2026 X Enterprises LLC. All Rights Reserved.
4
4
 
5
- Permission to use, copy, modify, and/or distribute this software for any
6
- purpose with or without fee is hereby granted, provided that the above
7
- copyright notice and this permission notice appear in all copies.
5
+ This software and associated documentation files (the "Software") are the
6
+ exclusive property of X Enterprises LLC, a Washington limited liability
7
+ company.
8
8
 
9
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
9
+ TERMS AND CONDITIONS
10
+
11
+ 1. OWNERSHIP
12
+ All rights, title, and interest in and to the Software, including all
13
+ intellectual property rights, are and shall remain the exclusive property
14
+ of X Enterprises LLC.
15
+
16
+ 2. RESTRICTIONS
17
+ Without the prior written consent of X Enterprises LLC, you may not:
18
+ - Copy, modify, or distribute the Software
19
+ - Reverse engineer, decompile, or disassemble the Software
20
+ - Sublicense, sell, lease, or otherwise transfer the Software
21
+ - Remove or alter any proprietary notices or labels
22
+
23
+ 3. AUTHORIZED USE
24
+ Use of this Software is limited to authorized employees, contractors, and
25
+ agents of X Enterprises LLC, solely for purposes approved by X Enterprises
26
+ LLC.
27
+
28
+ 4. NO WARRANTY
29
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
32
+ X ENTERPRISES LLC BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
33
+ WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF,
34
+ OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
35
+ SOFTWARE.
36
+
37
+ 5. LIMITATION OF LIABILITY
38
+ IN NO EVENT SHALL X ENTERPRISES LLC BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
39
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
40
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
41
+ OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
42
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
43
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
44
+ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
45
+
46
+ 6. GOVERNING LAW
47
+ This license shall be governed by and construed in accordance with the laws
48
+ of the State of Washington, United States, without regard to its conflict
49
+ of law provisions.
50
+
51
+ 7. TERMINATION
52
+ This license is effective until terminated. X Enterprises LLC may terminate
53
+ this license at any time without notice. Upon termination, you must destroy
54
+ all copies of the Software in your possession.
55
+
56
+ For licensing inquiries, contact: legal@x.enterprises
57
+
58
+ ---
59
+ X Enterprises LLC
60
+ Bothell, Washington, United States
package/README.md CHANGED
@@ -1,26 +1,11 @@
1
- # xStripe
1
+ # @xenterprises/fastify-xstripe
2
2
 
3
- Fastify v5 plugin for simplified, testable Stripe webhook handling. Focuses on subscription lifecycle management with clean, readable code.
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
- ## Requirements
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 fastify@5
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 handlers with your business logic:
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 fastify.email.send(
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
- ## Handler Function Signature
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
- ## Supported Events
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` - New subscription
120
- - `customer.subscription.updated` - Subscription changed
121
- - `customer.subscription.deleted` - Subscription canceled
122
- - `customer.subscription.trial_will_end` - Trial ending in 3 days
123
- - `customer.subscription.paused` - Subscription paused
124
- - `customer.subscription.resumed` - 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` - Invoice created
128
- - `invoice.finalized` - Invoice ready for payment
129
- - `invoice.paid` - Payment succeeded
130
- - `invoice.payment_failed` - Payment failed
131
- - `invoice.upcoming` - Upcoming charge notification
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` - Payment successful
135
- - `payment_intent.payment_failed` - 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` - New customer
139
- - `customer.updated` - Customer details changed
140
- - `customer.deleted` - 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` - Payment method added
144
- - `payment_method.detached` - Payment method removed
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` - Checkout completed
148
- - `checkout.session.expired` - Checkout session expired
106
+ - `checkout.session.completed` logs sessionId, customerId, subscriptionId, mode, paymentStatus
107
+ - `checkout.session.expired` logs sessionId
149
108
 
150
- ## Testing Webhooks Locally
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
- ### 1. Use Stripe CLI
114
+ ## Helper Utilities
153
115
 
154
- ```bash
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
- 'customer.subscription.trial_will_end': async (event, fastify, stripe) => {
239
- const subscription = event.data.object;
240
- const customer = await stripe.customers.retrieve(subscription.customer);
241
-
242
- await fastify.email.send(
243
- customer.email,
244
- 'Your trial ends soon!',
245
- '<p>Convert to a paid plan to keep access.</p>'
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
- ### Track Failed Payments
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
- ## Configuration Options
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
- | Option | Type | Required | Default | Description |
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
- ## Security
149
+ All errors use the `[xStripe]` prefix for easy identification in logs.
289
150
 
290
- - **Signature Verification**: All webhooks are verified using Stripe's signature
291
- - **Raw Body Required**: Plugin automatically handles raw body parsing
292
- - **Error Isolation**: Handler errors don't prevent webhook acknowledgment
293
- - **Logging**: All events and errors are logged
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
- ## Best Practices
161
+ ## How It Works
296
162
 
297
- 1. **Always acknowledge webhooks quickly** - Do heavy processing async
298
- 2. **Make handlers idempotent** - Stripe may send events multiple times
299
- 3. **Log everything** - Use structured logging for debugging
300
- 4. **Test with Stripe CLI** - Test all event types before production
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
- ## Environment Variables
168
+ ## Testing Webhooks Locally
304
169
 
305
170
  ```bash
306
- STRIPE_API_KEY=sk_test_...
307
- STRIPE_WEBHOOK_SECRET=whsec_...
308
- ```
309
-
310
- ## Integration with Other xPlugins
171
+ # Install Stripe CLI
172
+ brew install stripe/stripe-cli/stripe
311
173
 
312
- Works seamlessly with other x-series plugins:
174
+ # Login and forward webhooks
175
+ stripe login
176
+ stripe listen --forward-to localhost:3000/stripe/webhook
313
177
 
314
- ```javascript
315
- await fastify.register(xStripe, { /* ... */ });
316
- await fastify.register(xTwilio, { /* ... */ }); // Send SMS notifications
317
- await fastify.register(xConfig, { /* ... */ }); // Email notifications
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
- ISC
186
+ UNLICENSED