chargebee 3.20.0 → 3.21.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/CHANGELOG.md +14 -0
- package/README.md +304 -23
- package/cjs/chargebee.cjs.js +11 -4
- package/cjs/createChargebee.js +12 -0
- package/cjs/environment.js +1 -1
- package/cjs/resources/webhook/auth.js +53 -0
- package/cjs/resources/webhook/content.js +4 -0
- package/cjs/resources/webhook/errors.js +94 -0
- package/cjs/resources/webhook/eventType.js +11 -1
- package/cjs/resources/webhook/handler.js +483 -0
- package/esm/chargebee.esm.js +4 -2
- package/esm/createChargebee.js +12 -0
- package/esm/environment.js +1 -1
- package/esm/resources/webhook/auth.js +49 -0
- package/esm/resources/webhook/content.js +2 -0
- package/esm/resources/webhook/errors.js +87 -0
- package/esm/resources/webhook/eventType.js +11 -1
- package/esm/resources/webhook/handler.js +472 -0
- package/package.json +11 -6
- package/types/index.d.ts +190 -0
- package/types/resources/WebhookEvent.d.ts +2 -2
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.basicAuthValidator = exports.WebhookHandler = exports.WebhookPayloadParseError = exports.WebhookPayloadValidationError = exports.WebhookAuthenticationError = exports.WebhookError = exports.WebhookContentType = exports.WebhookEventType = void 0;
|
|
4
|
+
exports.createHandler = createHandler;
|
|
5
|
+
exports.createDefaultHandler = createDefaultHandler;
|
|
6
|
+
const node_events_1 = require("node:events");
|
|
7
|
+
const auth_js_1 = require("./auth.js");
|
|
8
|
+
const eventType_js_1 = require("./eventType.js");
|
|
9
|
+
Object.defineProperty(exports, "WebhookEventType", { enumerable: true, get: function () { return eventType_js_1.WebhookEventType; } });
|
|
10
|
+
Object.defineProperty(exports, "WebhookContentType", { enumerable: true, get: function () { return eventType_js_1.WebhookContentType; } });
|
|
11
|
+
const errors_js_1 = require("./errors.js");
|
|
12
|
+
Object.defineProperty(exports, "WebhookError", { enumerable: true, get: function () { return errors_js_1.WebhookError; } });
|
|
13
|
+
Object.defineProperty(exports, "WebhookAuthenticationError", { enumerable: true, get: function () { return errors_js_1.WebhookAuthenticationError; } });
|
|
14
|
+
Object.defineProperty(exports, "WebhookPayloadValidationError", { enumerable: true, get: function () { return errors_js_1.WebhookPayloadValidationError; } });
|
|
15
|
+
Object.defineProperty(exports, "WebhookPayloadParseError", { enumerable: true, get: function () { return errors_js_1.WebhookPayloadParseError; } });
|
|
16
|
+
/**
|
|
17
|
+
* Webhook handler for processing Chargebee webhook events.
|
|
18
|
+
*
|
|
19
|
+
* Extends Node.js `EventEmitter` to provide a familiar, event-driven API for
|
|
20
|
+
* handling webhooks. Supports type-safe event listeners with full TypeScript
|
|
21
|
+
* autocomplete for all Chargebee event types.
|
|
22
|
+
*
|
|
23
|
+
* @typeParam ReqT - Framework-specific request type (e.g., `express.Request`)
|
|
24
|
+
* @typeParam ResT - Framework-specific response type (e.g., `express.Response`)
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* **Lifecycle Warning:** Event listeners persist for the lifetime of the handler
|
|
28
|
+
* instance. Register handlers once at application startup, not per-request.
|
|
29
|
+
*
|
|
30
|
+
* @example Basic Usage with Express
|
|
31
|
+
* ```typescript
|
|
32
|
+
* import express from 'express';
|
|
33
|
+
* import { createHandler, basicAuthValidator } from 'chargebee/webhook';
|
|
34
|
+
*
|
|
35
|
+
* const app = express();
|
|
36
|
+
* app.use(express.json());
|
|
37
|
+
*
|
|
38
|
+
* // Create handler with Basic Auth
|
|
39
|
+
* const webhookHandler = createHandler({
|
|
40
|
+
* requestValidator: basicAuthValidator((u, p) => u === 'admin' && p === 'secret'),
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* // Register event listeners ONCE at startup (not per-request!)
|
|
44
|
+
* webhookHandler.on('subscription_created', async ({ event, response }) => {
|
|
45
|
+
* console.log('New subscription:', event.content.subscription.id);
|
|
46
|
+
* response?.status(200).send('OK');
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* webhookHandler.on('error', (error, { response }) => {
|
|
50
|
+
* if (error instanceof WebhookAuthenticationError) {
|
|
51
|
+
* response?.status(401).send('Unauthorized');
|
|
52
|
+
* } else if (error instanceof WebhookPayloadValidationError || error instanceof WebhookPayloadParseError) {
|
|
53
|
+
* response?.status(400).send('Bad Request');
|
|
54
|
+
* } else {
|
|
55
|
+
* response?.status(500).send('Internal Server Error');
|
|
56
|
+
* }
|
|
57
|
+
* });
|
|
58
|
+
*
|
|
59
|
+
* // Route handler
|
|
60
|
+
* app.post('/webhooks', async (req, res) => {
|
|
61
|
+
* await webhookHandler.handle({
|
|
62
|
+
* body: req.body,
|
|
63
|
+
* headers: req.headers,
|
|
64
|
+
* request: req,
|
|
65
|
+
* response: res,
|
|
66
|
+
* });
|
|
67
|
+
* });
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @example Available Event Types
|
|
71
|
+
* ```typescript
|
|
72
|
+
* // Subscription events
|
|
73
|
+
* handler.on('subscription_created', ({ event }) => { ... });
|
|
74
|
+
* handler.on('subscription_changed', ({ event }) => { ... });
|
|
75
|
+
* handler.on('subscription_cancelled', ({ event }) => { ... });
|
|
76
|
+
*
|
|
77
|
+
* // Customer events
|
|
78
|
+
* handler.on('customer_created', ({ event }) => { ... });
|
|
79
|
+
* handler.on('customer_changed', ({ event }) => { ... });
|
|
80
|
+
*
|
|
81
|
+
* // Payment events
|
|
82
|
+
* handler.on('payment_succeeded', ({ event }) => { ... });
|
|
83
|
+
* handler.on('payment_failed', ({ event }) => { ... });
|
|
84
|
+
*
|
|
85
|
+
* // Special events
|
|
86
|
+
* handler.on('unhandled_event', ({ event }) => {
|
|
87
|
+
* console.log('Unhandled event type:', event.event_type);
|
|
88
|
+
* });
|
|
89
|
+
*
|
|
90
|
+
* handler.on('error', (error, { response }) => {
|
|
91
|
+
* if (error instanceof WebhookAuthenticationError) {
|
|
92
|
+
* response?.status(401).send('Unauthorized');
|
|
93
|
+
* } else if (error instanceof WebhookPayloadValidationError || error instanceof WebhookPayloadParseError) {
|
|
94
|
+
* response?.status(400).send('Bad Request');
|
|
95
|
+
* } else {
|
|
96
|
+
* response?.status(500).send('Internal Server Error');
|
|
97
|
+
* }
|
|
98
|
+
* });
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* @example Async Handlers
|
|
102
|
+
* ```typescript
|
|
103
|
+
* // Handlers can be async - errors are captured and emitted to 'error' event
|
|
104
|
+
* handler.on('subscription_created', async ({ event, response }) => {
|
|
105
|
+
* await saveToDatabase(event.content.subscription);
|
|
106
|
+
* await sendWelcomeEmail(event.content.customer);
|
|
107
|
+
* response?.status(200).send('OK');
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
class WebhookHandler extends node_events_1.EventEmitter {
|
|
112
|
+
/**
|
|
113
|
+
* Creates a new WebhookHandler instance.
|
|
114
|
+
*
|
|
115
|
+
* @param options - Optional configuration options
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* // Without authentication (not recommended for production)
|
|
120
|
+
* const handler = new WebhookHandler();
|
|
121
|
+
*
|
|
122
|
+
* // With Basic Auth
|
|
123
|
+
* const handler = new WebhookHandler({
|
|
124
|
+
* requestValidator: basicAuthValidator((u, p) => u === 'user' && p === 'pass'),
|
|
125
|
+
* });
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
constructor(options) {
|
|
129
|
+
super({ captureRejections: true });
|
|
130
|
+
this._noAuthWarningShown = false;
|
|
131
|
+
this._requestValidator = options === null || options === void 0 ? void 0 : options.requestValidator;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Gets the current request validator function.
|
|
135
|
+
*
|
|
136
|
+
* @returns The configured validator or `undefined` if no authentication is set
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* if (handler.requestValidator) {
|
|
141
|
+
* console.log('Authentication is configured');
|
|
142
|
+
* }
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
get requestValidator() {
|
|
146
|
+
return this._requestValidator;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Sets or updates the request validator function.
|
|
150
|
+
*
|
|
151
|
+
* Use this to configure authentication after handler creation,
|
|
152
|
+
* or to change validators at runtime.
|
|
153
|
+
*
|
|
154
|
+
* @param validator - The validator function, or `undefined` to disable authentication
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* // Set up Basic Auth after creation
|
|
159
|
+
* handler.requestValidator = basicAuthValidator((u, p) => u === 'admin' && p === 'secret');
|
|
160
|
+
*
|
|
161
|
+
* // Custom header validation
|
|
162
|
+
* handler.requestValidator = (headers) => {
|
|
163
|
+
* if (headers['x-webhook-secret'] !== process.env.WEBHOOK_SECRET) {
|
|
164
|
+
* throw new Error('Invalid webhook secret');
|
|
165
|
+
* }
|
|
166
|
+
* };
|
|
167
|
+
*
|
|
168
|
+
* // Disable authentication (not recommended)
|
|
169
|
+
* handler.requestValidator = undefined;
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
set requestValidator(validator) {
|
|
173
|
+
this._requestValidator = validator;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Registers an event listener for a specific webhook event type.
|
|
177
|
+
*
|
|
178
|
+
* This method is inherited from Node.js `EventEmitter` but is documented here
|
|
179
|
+
* for clarity on available Chargebee webhook events.
|
|
180
|
+
*
|
|
181
|
+
* @param eventName - The Chargebee event type to listen for (e.g., `'subscription_created'`)
|
|
182
|
+
* @param listener - Callback function invoked when the event occurs
|
|
183
|
+
* @returns This handler instance for method chaining
|
|
184
|
+
*
|
|
185
|
+
* @remarks
|
|
186
|
+
* **Memory Leak Warning:** Listeners persist for the handler's lifetime.
|
|
187
|
+
* Always register listeners once at application startup, never inside
|
|
188
|
+
* request handlers or loops.
|
|
189
|
+
*
|
|
190
|
+
* @example Available Events
|
|
191
|
+
* ```typescript
|
|
192
|
+
* // Chargebee business events
|
|
193
|
+
* handler.on('subscription_created', ({ event, response }) => { ... });
|
|
194
|
+
* handler.on('subscription_changed', ({ event, response }) => { ... });
|
|
195
|
+
* handler.on('subscription_cancelled', ({ event, response }) => { ... });
|
|
196
|
+
* handler.on('customer_created', ({ event, response }) => { ... });
|
|
197
|
+
* handler.on('payment_succeeded', ({ event, response }) => { ... });
|
|
198
|
+
* handler.on('invoice_generated', ({ event, response }) => { ... });
|
|
199
|
+
* // ... and many more - see WebhookEventType enum for full list
|
|
200
|
+
*
|
|
201
|
+
* // Special events
|
|
202
|
+
* handler.on('unhandled_event', ({ event }) => {
|
|
203
|
+
* // Called when no listener exists for the event type
|
|
204
|
+
* console.log('Unhandled:', event.event_type);
|
|
205
|
+
* });
|
|
206
|
+
*
|
|
207
|
+
* handler.on('error', (error, { response }) => {
|
|
208
|
+
* // Called on validation errors, parse errors, or handler errors
|
|
209
|
+
* if (error instanceof WebhookAuthenticationError) {
|
|
210
|
+
* response?.status(401).send('Unauthorized');
|
|
211
|
+
* } else if (error instanceof WebhookPayloadValidationError || error instanceof WebhookPayloadParseError) {
|
|
212
|
+
* response?.status(400).send('Bad Request');
|
|
213
|
+
* } else {
|
|
214
|
+
* response?.status(500).send('Internal Server Error');
|
|
215
|
+
* }
|
|
216
|
+
* });
|
|
217
|
+
* ```
|
|
218
|
+
*
|
|
219
|
+
* @example Correct Usage
|
|
220
|
+
* ```typescript
|
|
221
|
+
* // ✅ GOOD: Register once at startup
|
|
222
|
+
* const handler = createHandler();
|
|
223
|
+
* handler.on('subscription_created', handleSubscription);
|
|
224
|
+
* handler.on('error', handleError);
|
|
225
|
+
*
|
|
226
|
+
* app.post('/webhooks', async (req, res) => {
|
|
227
|
+
* await handler.handle({ body: req.body, headers: req.headers, response: res });
|
|
228
|
+
* });
|
|
229
|
+
* ```
|
|
230
|
+
*
|
|
231
|
+
* @example Incorrect Usage
|
|
232
|
+
* ```typescript
|
|
233
|
+
* // ❌ BAD: Don't register inside request handlers - causes memory leak!
|
|
234
|
+
* app.post('/webhooks', async (req, res) => {
|
|
235
|
+
* handler.on('subscription_created', async () => { ... }); // Memory leak!
|
|
236
|
+
* await handler.handle({ ... });
|
|
237
|
+
* });
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
// Note: on() is inherited from EventEmitter with proper typing via WebhookEventMap
|
|
241
|
+
/**
|
|
242
|
+
* Handles an incoming webhook request from Chargebee.
|
|
243
|
+
*
|
|
244
|
+
* This method:
|
|
245
|
+
* 1. Validates the request using the configured `requestValidator` (if any)
|
|
246
|
+
* 2. Parses the request body (if it's a string)
|
|
247
|
+
* 3. Validates required fields (`event_type`, `id`)
|
|
248
|
+
* 4. Emits the appropriate event to registered listeners
|
|
249
|
+
*
|
|
250
|
+
* @param options - The webhook request options
|
|
251
|
+
* @returns A promise that resolves when the event has been emitted
|
|
252
|
+
*
|
|
253
|
+
* @throws Error if no `error` listener is registered and an error occurs
|
|
254
|
+
*
|
|
255
|
+
* @remarks
|
|
256
|
+
* **Async Behavior:** This method emits events but does not wait for async
|
|
257
|
+
* listeners to complete. Errors in async listeners are captured via
|
|
258
|
+
* `captureRejections` and emitted to the `error` event.
|
|
259
|
+
*
|
|
260
|
+
* **Response Handling:** The handler does NOT automatically send HTTP responses.
|
|
261
|
+
* Your event listeners must call `response.status(200).send('OK')` or similar.
|
|
262
|
+
*
|
|
263
|
+
* @example Express Integration
|
|
264
|
+
* ```typescript
|
|
265
|
+
* app.post('/webhooks', async (req, res) => {
|
|
266
|
+
* try {
|
|
267
|
+
* await handler.handle({
|
|
268
|
+
* body: req.body,
|
|
269
|
+
* headers: req.headers,
|
|
270
|
+
* request: req,
|
|
271
|
+
* response: res,
|
|
272
|
+
* });
|
|
273
|
+
* } catch (error) {
|
|
274
|
+
* // Only reached if no 'error' listener is registered
|
|
275
|
+
* res.status(500).send('Internal error');
|
|
276
|
+
* }
|
|
277
|
+
* });
|
|
278
|
+
* ```
|
|
279
|
+
*
|
|
280
|
+
* @example Fastify Integration
|
|
281
|
+
* ```typescript
|
|
282
|
+
* fastify.post('/webhooks', async (request, reply) => {
|
|
283
|
+
* await handler.handle({
|
|
284
|
+
* body: request.body,
|
|
285
|
+
* headers: request.headers,
|
|
286
|
+
* request,
|
|
287
|
+
* response: reply,
|
|
288
|
+
* });
|
|
289
|
+
* });
|
|
290
|
+
* ```
|
|
291
|
+
*
|
|
292
|
+
* @example Raw Node.js HTTP
|
|
293
|
+
* ```typescript
|
|
294
|
+
* http.createServer(async (req, res) => {
|
|
295
|
+
* if (req.method === 'POST' && req.url === '/webhooks') {
|
|
296
|
+
* const body = await getBody(req);
|
|
297
|
+
* await handler.handle({ body, headers: req.headers, response: res });
|
|
298
|
+
* }
|
|
299
|
+
* });
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
async handle(options) {
|
|
303
|
+
const { body, headers, request, response } = options;
|
|
304
|
+
try {
|
|
305
|
+
if (this._requestValidator) {
|
|
306
|
+
if (!headers) {
|
|
307
|
+
console.warn('[chargebee] Warning: Request validator is configured but no headers were passed. ' +
|
|
308
|
+
'Authentication check skipped. If this is intentional (no-auth webhook), ' +
|
|
309
|
+
'you can remove the requestValidator or ignore this warning.');
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
await this._requestValidator(headers);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else if (!this._noAuthWarningShown) {
|
|
316
|
+
this._noAuthWarningShown = true;
|
|
317
|
+
console.warn('[chargebee] Warning: No webhook authentication configured. ' +
|
|
318
|
+
'Consider using basicAuthValidator() or a custom requestValidator for production. ' +
|
|
319
|
+
'See: https://www.chargebee.com/docs/billing/2.0/site-configuration/webhook_settings#basic-authentication');
|
|
320
|
+
}
|
|
321
|
+
let event;
|
|
322
|
+
try {
|
|
323
|
+
event =
|
|
324
|
+
typeof body === 'string' ? JSON.parse(body) : body;
|
|
325
|
+
}
|
|
326
|
+
catch (parseErr) {
|
|
327
|
+
const parseError = parseErr instanceof Error ? parseErr : new Error(String(parseErr));
|
|
328
|
+
throw new errors_js_1.WebhookPayloadParseError(`Failed to parse webhook body: ${parseError.message}`, typeof body === 'string' ? body : undefined);
|
|
329
|
+
}
|
|
330
|
+
// Validate required fields
|
|
331
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) {
|
|
332
|
+
throw new errors_js_1.WebhookPayloadValidationError('Invalid webhook payload: body must be a JSON object');
|
|
333
|
+
}
|
|
334
|
+
if (!event.event_type || typeof event.event_type !== 'string') {
|
|
335
|
+
throw new errors_js_1.WebhookPayloadValidationError('Invalid webhook payload: missing or invalid event_type');
|
|
336
|
+
}
|
|
337
|
+
if (!event.id) {
|
|
338
|
+
throw new errors_js_1.WebhookPayloadValidationError('Invalid webhook payload: missing event id');
|
|
339
|
+
}
|
|
340
|
+
const context = {
|
|
341
|
+
event,
|
|
342
|
+
request,
|
|
343
|
+
response,
|
|
344
|
+
};
|
|
345
|
+
const eventType = event.event_type;
|
|
346
|
+
if (this.listenerCount(eventType) > 0) {
|
|
347
|
+
this.emit(eventType, context);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
this.emit('unhandled_event', context);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
355
|
+
if (this.listenerCount('error') === 0) {
|
|
356
|
+
console.warn('[chargebee] Webhook error with no handler:', error.message);
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
this.emit('error', error, { request, response });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
exports.WebhookHandler = WebhookHandler;
|
|
364
|
+
/**
|
|
365
|
+
* Creates a new WebhookHandler with custom configuration.
|
|
366
|
+
*
|
|
367
|
+
* This is the recommended factory function for creating webhook handlers.
|
|
368
|
+
* Use this when you need explicit control over authentication configuration.
|
|
369
|
+
*
|
|
370
|
+
* @typeParam ReqT - Framework-specific request type (e.g., `express.Request`)
|
|
371
|
+
* @typeParam ResT - Framework-specific response type (e.g., `express.Response`)
|
|
372
|
+
*
|
|
373
|
+
* @param options - Optional configuration for the handler
|
|
374
|
+
* @returns A new WebhookHandler instance
|
|
375
|
+
*
|
|
376
|
+
* @remarks
|
|
377
|
+
* For multi-route or multi-tenant scenarios, create separate handler instances
|
|
378
|
+
* to maintain isolation and avoid event listener conflicts.
|
|
379
|
+
*
|
|
380
|
+
* @example Basic Auth Configuration
|
|
381
|
+
* ```typescript
|
|
382
|
+
* import { createHandler, basicAuthValidator } from 'chargebee/webhook';
|
|
383
|
+
*
|
|
384
|
+
* const handler = createHandler({
|
|
385
|
+
* requestValidator: basicAuthValidator((username, password) => {
|
|
386
|
+
* return username === 'admin' && password === 'secret';
|
|
387
|
+
* }),
|
|
388
|
+
* });
|
|
389
|
+
* ```
|
|
390
|
+
*
|
|
391
|
+
* @example Custom Authentication
|
|
392
|
+
* ```typescript
|
|
393
|
+
* const handler = createHandler({
|
|
394
|
+
* requestValidator: async (headers) => {
|
|
395
|
+
* const signature = headers['x-chargebee-signature'];
|
|
396
|
+
* const isValid = await verifySignature(signature);
|
|
397
|
+
* if (!isValid) throw new Error('Invalid signature');
|
|
398
|
+
* },
|
|
399
|
+
* });
|
|
400
|
+
* ```
|
|
401
|
+
*
|
|
402
|
+
* @example Multi-Route Setup
|
|
403
|
+
* ```typescript
|
|
404
|
+
* // Separate handlers for different webhook endpoints
|
|
405
|
+
* const billingHandler = createHandler({ ... });
|
|
406
|
+
* const notificationHandler = createHandler({ ... });
|
|
407
|
+
*
|
|
408
|
+
* billingHandler.on('subscription_created', handleBillingEvent);
|
|
409
|
+
* notificationHandler.on('subscription_created', sendNotification);
|
|
410
|
+
*
|
|
411
|
+
* app.post('/webhooks/billing', (req, res) => billingHandler.handle({ ... }));
|
|
412
|
+
* app.post('/webhooks/notifications', (req, res) => notificationHandler.handle({ ... }));
|
|
413
|
+
* ```
|
|
414
|
+
*
|
|
415
|
+
* @example Without Authentication (Development Only)
|
|
416
|
+
* ```typescript
|
|
417
|
+
* // ⚠️ Not recommended for production
|
|
418
|
+
* const handler = createHandler();
|
|
419
|
+
* ```
|
|
420
|
+
*/
|
|
421
|
+
function createHandler(options) {
|
|
422
|
+
return new WebhookHandler(options);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Creates a WebhookHandler with auto-configured Basic Auth from environment variables.
|
|
426
|
+
*
|
|
427
|
+
* This is a convenience function that automatically configures Basic Auth
|
|
428
|
+
* when the following environment variables are set:
|
|
429
|
+
* - `CHARGEBEE_WEBHOOK_USERNAME`
|
|
430
|
+
* - `CHARGEBEE_WEBHOOK_PASSWORD`
|
|
431
|
+
*
|
|
432
|
+
* If these environment variables are not set, the handler is created without
|
|
433
|
+
* authentication (a warning will be logged on first webhook handling).
|
|
434
|
+
*
|
|
435
|
+
* @typeParam ReqT - Framework-specific request type (e.g., `express.Request`)
|
|
436
|
+
* @typeParam ResT - Framework-specific response type (e.g., `express.Response`)
|
|
437
|
+
*
|
|
438
|
+
* @returns A new WebhookHandler instance with optional auto-configured auth
|
|
439
|
+
*
|
|
440
|
+
* @remarks
|
|
441
|
+
* This function is used internally by `chargebee.webhooks` to provide a
|
|
442
|
+
* pre-configured handler on the Chargebee instance.
|
|
443
|
+
*
|
|
444
|
+
* @example Environment-Based Setup
|
|
445
|
+
* ```bash
|
|
446
|
+
* # .env file
|
|
447
|
+
* CHARGEBEE_WEBHOOK_USERNAME=webhook_user
|
|
448
|
+
* CHARGEBEE_WEBHOOK_PASSWORD=webhook_secret
|
|
449
|
+
* ```
|
|
450
|
+
*
|
|
451
|
+
* ```typescript
|
|
452
|
+
* import { createDefaultHandler } from 'chargebee/webhook';
|
|
453
|
+
*
|
|
454
|
+
* // Auth is automatically configured from env vars
|
|
455
|
+
* const handler = createDefaultHandler();
|
|
456
|
+
*
|
|
457
|
+
* handler.on('subscription_created', ({ event, response }) => {
|
|
458
|
+
* console.log('Subscription:', event.content.subscription.id);
|
|
459
|
+
* response?.status(200).send('OK');
|
|
460
|
+
* });
|
|
461
|
+
* ```
|
|
462
|
+
*
|
|
463
|
+
* @example Overriding Auto-Configured Auth
|
|
464
|
+
* ```typescript
|
|
465
|
+
* const handler = createDefaultHandler();
|
|
466
|
+
*
|
|
467
|
+
* // Override with custom validator if needed
|
|
468
|
+
* handler.requestValidator = myCustomValidator;
|
|
469
|
+
* ```
|
|
470
|
+
*
|
|
471
|
+
* @see {@link createHandler} for explicit configuration without environment variables
|
|
472
|
+
*/
|
|
473
|
+
function createDefaultHandler() {
|
|
474
|
+
const handler = new WebhookHandler();
|
|
475
|
+
const username = process.env.CHARGEBEE_WEBHOOK_USERNAME;
|
|
476
|
+
const password = process.env.CHARGEBEE_WEBHOOK_PASSWORD;
|
|
477
|
+
if (username && password) {
|
|
478
|
+
handler.requestValidator = (0, auth_js_1.basicAuthValidator)((u, p) => u === username && p === password);
|
|
479
|
+
}
|
|
480
|
+
return handler;
|
|
481
|
+
}
|
|
482
|
+
var auth_js_2 = require("./auth.js");
|
|
483
|
+
Object.defineProperty(exports, "basicAuthValidator", { enumerable: true, get: function () { return auth_js_2.basicAuthValidator; } });
|
package/esm/chargebee.esm.js
CHANGED
|
@@ -3,5 +3,7 @@ import { FetchHttpClient } from './net/FetchClient.js';
|
|
|
3
3
|
const httpClient = new FetchHttpClient();
|
|
4
4
|
const Chargebee = CreateChargebee(httpClient);
|
|
5
5
|
export default Chargebee;
|
|
6
|
-
// Export webhook
|
|
7
|
-
export { WebhookEventType, WebhookContentType, } from './resources/webhook/
|
|
6
|
+
// Export webhook utilities
|
|
7
|
+
export { WebhookEventType, WebhookContentType, } from './resources/webhook/handler.js';
|
|
8
|
+
export { basicAuthValidator } from './resources/webhook/auth.js';
|
|
9
|
+
export { WebhookError, WebhookAuthenticationError, WebhookPayloadValidationError, WebhookPayloadParseError, } from './resources/webhook/handler.js';
|
package/esm/createChargebee.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Environment } from './environment.js';
|
|
|
3
3
|
import { Endpoints } from './resources/api_endpoints.js';
|
|
4
4
|
import { extend, sleep } from './util.js';
|
|
5
5
|
import { waitForProcessToComplete } from './asyncApiSupport.js';
|
|
6
|
+
import { WebhookHandler, createDefaultHandler, } from './resources/webhook/handler.js';
|
|
6
7
|
export const CreateChargebee = (httpClient) => {
|
|
7
8
|
const Chargebee = function (conf) {
|
|
8
9
|
this._env = Object.assign({}, Environment);
|
|
@@ -12,6 +13,17 @@ export const CreateChargebee = (httpClient) => {
|
|
|
12
13
|
conf.httpClient != null ? conf.httpClient : httpClient;
|
|
13
14
|
this._buildResources();
|
|
14
15
|
this._endpoints = Endpoints;
|
|
16
|
+
// Initialize webhooks handler with auto-configured Basic Auth (if env vars are set)
|
|
17
|
+
const handler = createDefaultHandler();
|
|
18
|
+
this.__clientIdentifier = (serviceName) => {
|
|
19
|
+
extend(true, this._env, { userAgentSuffix: serviceName });
|
|
20
|
+
};
|
|
21
|
+
// Create webhooks namespace with handler methods + createHandler factory
|
|
22
|
+
this.webhooks = Object.assign(handler, {
|
|
23
|
+
createHandler(options) {
|
|
24
|
+
return new WebhookHandler(options);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
15
27
|
};
|
|
16
28
|
Chargebee.prototype = {
|
|
17
29
|
_createApiFunc(apiCall, env) {
|
package/esm/environment.js
CHANGED
|
@@ -8,7 +8,7 @@ export const Environment = {
|
|
|
8
8
|
hostSuffix: '.chargebee.com',
|
|
9
9
|
apiPath: '/api/v2',
|
|
10
10
|
timeout: DEFAULT_TIME_OUT,
|
|
11
|
-
clientVersion: 'v3.
|
|
11
|
+
clientVersion: 'v3.21.1',
|
|
12
12
|
port: DEFAULT_PORT,
|
|
13
13
|
timemachineWaitInMillis: DEFAULT_TIME_MACHINE_WAIT,
|
|
14
14
|
exportWaitInMillis: DEFAULT_EXPORT_WAIT,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { WebhookAuthenticationError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a Basic Auth validator for webhook requests.
|
|
4
|
+
* Parses the Authorization header and validates credentials.
|
|
5
|
+
*
|
|
6
|
+
* @param validateCredentials - Function to validate username/password.
|
|
7
|
+
* Can be sync or async (e.g., for database lookups).
|
|
8
|
+
* @returns A request validator function for use with WebhookHandler
|
|
9
|
+
*
|
|
10
|
+
* @throws {WebhookAuthenticationError} When authentication fails
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Simple sync validation
|
|
14
|
+
* const validator = basicAuthValidator((u, p) => u === 'admin' && p === 'secret');
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Async validation (e.g., database lookup)
|
|
18
|
+
* const validator = basicAuthValidator(async (u, p) => {
|
|
19
|
+
* const user = await db.findUser(u);
|
|
20
|
+
* return user && await bcrypt.compare(p, user.passwordHash);
|
|
21
|
+
* });
|
|
22
|
+
*/
|
|
23
|
+
export const basicAuthValidator = (validateCredentials) => {
|
|
24
|
+
return async (headers) => {
|
|
25
|
+
const authHeader = headers['authorization'] || headers['Authorization'];
|
|
26
|
+
if (!authHeader) {
|
|
27
|
+
throw new WebhookAuthenticationError('Missing authorization header');
|
|
28
|
+
}
|
|
29
|
+
const authStr = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
30
|
+
if (!authStr) {
|
|
31
|
+
throw new WebhookAuthenticationError('Invalid authorization header');
|
|
32
|
+
}
|
|
33
|
+
const parts = authStr.split(' ');
|
|
34
|
+
if (parts.length !== 2 || parts[0] !== 'Basic') {
|
|
35
|
+
throw new WebhookAuthenticationError('Invalid authorization header format');
|
|
36
|
+
}
|
|
37
|
+
const decoded = Buffer.from(parts[1], 'base64').toString();
|
|
38
|
+
const separatorIndex = decoded.indexOf(':');
|
|
39
|
+
if (separatorIndex === -1) {
|
|
40
|
+
throw new WebhookAuthenticationError('Invalid credentials format');
|
|
41
|
+
}
|
|
42
|
+
const username = decoded.substring(0, separatorIndex);
|
|
43
|
+
const password = decoded.substring(separatorIndex + 1);
|
|
44
|
+
const isValid = await validateCredentials(username, password);
|
|
45
|
+
if (!isValid) {
|
|
46
|
+
throw new WebhookAuthenticationError('Invalid credentials');
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for all webhook-related errors.
|
|
3
|
+
* Extends the standard Error class with proper stack trace support.
|
|
4
|
+
*/
|
|
5
|
+
export class WebhookError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
var _a;
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'WebhookError';
|
|
10
|
+
// Maintains proper stack trace for where error was thrown (V8 only)
|
|
11
|
+
(_a = Error.captureStackTrace) === null || _a === void 0 ? void 0 : _a.call(Error, this, this.constructor);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Authentication error thrown when webhook request authentication fails.
|
|
16
|
+
*
|
|
17
|
+
* Common scenarios:
|
|
18
|
+
* - Missing authorization header
|
|
19
|
+
* - Invalid authorization header format
|
|
20
|
+
* - Invalid credentials
|
|
21
|
+
*
|
|
22
|
+
* Typically maps to HTTP 401 Unauthorized.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* handler.on('error', (error, { response }) => {
|
|
27
|
+
* if (error instanceof WebhookAuthenticationError) {
|
|
28
|
+
* response?.status(401).json({ error: 'Unauthorized' });
|
|
29
|
+
* }
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class WebhookAuthenticationError extends WebhookError {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = 'WebhookAuthenticationError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Payload validation error thrown when the webhook payload structure is invalid.
|
|
41
|
+
*
|
|
42
|
+
* Common scenarios:
|
|
43
|
+
* - Missing required fields (event_type, id)
|
|
44
|
+
* - Invalid field types
|
|
45
|
+
* - Malformed payload structure
|
|
46
|
+
*
|
|
47
|
+
* Typically maps to HTTP 400 Bad Request.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* handler.on('error', (error, { response }) => {
|
|
52
|
+
* if (error instanceof WebhookPayloadValidationError) {
|
|
53
|
+
* response?.status(400).json({ error: 'Bad Request', message: error.message });
|
|
54
|
+
* }
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export class WebhookPayloadValidationError extends WebhookError {
|
|
59
|
+
constructor(message) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = 'WebhookPayloadValidationError';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* JSON parsing error thrown when the webhook body cannot be parsed as JSON.
|
|
66
|
+
*
|
|
67
|
+
* Includes the raw body that failed to parse (if available) for debugging.
|
|
68
|
+
*
|
|
69
|
+
* Typically maps to HTTP 400 Bad Request.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* handler.on('error', (error, { response }) => {
|
|
74
|
+
* if (error instanceof WebhookPayloadParseError) {
|
|
75
|
+
* console.error('Failed to parse:', error.rawBody);
|
|
76
|
+
* response?.status(400).json({ error: 'Invalid JSON' });
|
|
77
|
+
* }
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export class WebhookPayloadParseError extends WebhookError {
|
|
82
|
+
constructor(message, rawBody) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.rawBody = rawBody;
|
|
85
|
+
this.name = 'WebhookPayloadParseError';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -231,6 +231,16 @@ export var WebhookEventType;
|
|
|
231
231
|
WebhookEventType["VoucherExpired"] = "voucher_expired";
|
|
232
232
|
})(WebhookEventType || (WebhookEventType = {}));
|
|
233
233
|
/**
|
|
234
|
-
* @deprecated Use WebhookEventType instead.
|
|
234
|
+
* @deprecated Renamed to `WebhookEventType` for clarity. Use `WebhookEventType` instead.
|
|
235
|
+
* This alias will be removed in the next major version.
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* // Before (deprecated)
|
|
239
|
+
* import { WebhookContentType } from 'chargebee';
|
|
240
|
+
* if (event.event_type === WebhookContentType.SubscriptionCreated) { ... }
|
|
241
|
+
*
|
|
242
|
+
* // After (recommended)
|
|
243
|
+
* import { WebhookEventType } from 'chargebee';
|
|
244
|
+
* if (event.event_type === WebhookEventType.SubscriptionCreated) { ... }
|
|
235
245
|
*/
|
|
236
246
|
export const WebhookContentType = WebhookEventType;
|