From 64194a88628f9e56148f963a8125646f8ec4fc27 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Wed, 21 Jan 2026 13:49:18 +0530 Subject: [PATCH 1/9] local changes --- .gitignore | 1 - README.md | 198 ++++- package-lock.json | 1245 +++++++++++++++++++++++++- package.json | 11 +- src/chargebee.cjs.ts | 18 +- src/chargebee.esm.ts | 16 +- src/resources/webhook/auth.ts | 66 ++ src/resources/webhook/content.ts | 1402 ++++++++++++++++++++++++++++++ src/resources/webhook/handler.ts | 139 +++ test/webhook.test.ts | 395 +++++++++ tsconfig.cjs.json | 4 +- tsconfig.json | 4 +- 12 files changed, 3460 insertions(+), 39 deletions(-) create mode 100644 src/resources/webhook/auth.ts create mode 100644 src/resources/webhook/content.ts create mode 100644 src/resources/webhook/handler.ts create mode 100644 test/webhook.test.ts diff --git a/.gitignore b/.gitignore index 7386b6e..76588e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ node_modules -test* *js cjs esm diff --git a/README.md b/README.md index d8934b2..7acca88 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,174 @@ const chargebeeSiteEU = new Chargebee({ }); ``` +### Handle webhooks + +Use the webhook handlers to parse and route webhook payloads from Chargebee with full TypeScript support. + +#### Quick Start: Using the default `webhook` instance + +The simplest way to handle webhooks is using the pre-configured `webhook` instance: + +```typescript +import express from 'express'; +import { webhook, type WebhookEvent } from 'chargebee'; + +const app = express(); +app.use(express.json()); + +webhook.on('subscription_created', async (event: WebhookEvent) => { + console.log(`Subscription created: ${event.id}`); + const subscription = event.content.subscription; + console.log(`Customer: ${subscription.customer_id}`); +}); + +webhook.on('error', (err: Error) => { + console.error('Webhook error:', err.message); +}); + +app.post('/chargebee/webhooks', (req, res) => { + webhook.handle(req.body, req.headers); + res.status(200).send('OK'); +}); + +app.listen(8080); +``` + +**Auto-configured Basic Auth:** The default `webhook` instance automatically configures Basic Auth validation if the following environment variables are set: + +- `CHARGEBEE_WEBHOOK_USERNAME` - The expected username +- `CHARGEBEE_WEBHOOK_PASSWORD` - The expected password + +When both are present, incoming webhook requests will be validated against these credentials. If not set, no authentication is applied. + +#### Creating custom `WebhookHandler` instances + +For more control or multiple webhook endpoints, create your own instances: + +```typescript +import express from 'express'; +import { WebhookHandler, basicAuthValidator } from 'chargebee'; + +const app = express(); +app.use(express.json()); + +const handler = new WebhookHandler(); + +// Register event listeners using .on() - events are fully typed +handler.on('subscription_created', async (event) => { + console.log(`Subscription created: ${event.id}`); + const subscription = event.content.subscription; + console.log(`Customer: ${subscription.customer_id}`); + console.log(`Plan: ${subscription.plan_id}`); +}); + +handler.on('payment_succeeded', async (event) => { + console.log(`Payment succeeded: ${event.id}`); + const transaction = event.content.transaction; + const customer = event.content.customer; + console.log(`Amount: ${transaction.amount}, Customer: ${customer.email}`); +}); + +// Optional: Add request validator (e.g., Basic Auth) +handler.requestValidator = basicAuthValidator((username, password) => { + return username === 'admin' && password === 'secret'; +}); + +app.post('/chargebee/webhooks', (req, res) => { + handler.handle(req.body, req.headers); + res.status(200).send('OK'); +}); + +app.listen(8080); +``` + +#### Low-level: Parse and handle events manually + +For more control, you can parse webhook events manually: + +```typescript +import express from 'express'; +import Chargebee, { type WebhookEvent } from 'chargebee'; + +const app = express(); +app.use(express.json()); + +app.post('/chargebee/webhooks', async (req, res) => { + try { + const event = req.body as WebhookEvent; + + switch (event.event_type) { + case 'subscription_created': + // Access event content with proper typing + const subscription = event.content.subscription; + console.log('Subscription created:', subscription.id); + break; + + case 'payment_succeeded': + const transaction = event.content.transaction; + console.log('Payment succeeded:', transaction.amount); + break; + + default: + console.log('Unhandled event type:', event.event_type); + } + + res.status(200).send('OK'); + } catch (err) { + console.error('Error processing webhook:', err); + res.status(500).send('Error processing webhook'); + } +}); + +app.listen(8080); +``` + +#### Handling Unhandled Events + +By default, if an incoming webhook event type is not recognized or you haven't registered a corresponding callback handler, the SDK provides flexible options to handle these scenarios: + +**Using the `unhandled_event` listener:** + +```typescript +import { WebhookHandler } from 'chargebee'; + +const handler = new WebhookHandler(); + +handler.on('subscription_created', async (event) => { + // Handle subscription created +}); + +// Gracefully handle events without registered listeners +handler.on('unhandled_event', async (event) => { + console.log(`Received unhandled event: ${event.event_type}`); + // Log for monitoring or store for later processing +}); +``` + +**Using the `error` listener for error handling:** + +If an error occurs during webhook processing (e.g., invalid JSON, validator failure), the SDK will emit an `error` event: + +```typescript +const handler = new WebhookHandler(); + +handler.on('subscription_created', async (event) => { + // Handle subscription created +}); + +// Catch any errors during webhook processing +handler.on('error', (err) => { + console.error('Webhook processing error:', err); + // Log to monitoring service, alert team, etc. +}); +``` + +**Best Practices:** + +- Use `unhandled_event` listener to acknowledge unknown events (return 200 OK) and log them +- Use `error` listener to catch and handle exceptions thrown during event processing +- Both listeners help ensure your webhook endpoint remains stable even when new event types are introduced by Chargebee + ### Processing Webhooks - API Version Check An attribute `api_version` is added to the [Event](https://apidocs.chargebee.com/docs/api/events) resource, which indicates the API version based on which the event content is structured. In your webhook servers, ensure this `api_version` is the same as the [API version](https://apidocs.chargebee.com/docs/api#versions) used by your webhook server's client library. @@ -224,38 +392,18 @@ To improve type safety and gain better autocompletion when working with webhooks #### Example ```ts -import Chargebee, { WebhookEventType, WebhookEvent } from "chargebee"; +import Chargebee, { type WebhookContentType, WebhookEvent } from "chargebee"; const result = await chargebeeInstance.event.retrieve("{event-id}"); -const subscriptionActivatedEvent: WebhookEvent = result.event; -const subscription = subscriptionActivatedEvent.content.subscription; -``` - -You can also use `WebhookEventType` in switch statements for runtime event handling: - -```ts -import { WebhookEventType, WebhookEvent } from "chargebee"; - -function handleWebhook(event: WebhookEvent) { - switch (event.event_type) { - case WebhookEventType.SubscriptionCreated: - console.log("Subscription created:", event.content.subscription?.id); - break; - case WebhookEventType.PaymentSucceeded: - console.log("Payment succeeded:", event.content.transaction?.id); - break; - default: - console.log("Unhandled event:", event.event_type); - } -} +const subscripitonActivatedEvent: WebhookEvent = result.event; +const subscription = subscripitonActivatedEvent.content.subscription; ``` #### Notes * `WebhookEvent` provides type hinting for the event payload, making it easier to work with specific event structures. -* Use `WebhookEventType` to specify the exact event type (e.g., `SubscriptionCreated`, `InvoiceGenerated`, etc.). -* `WebhookEventType` is available at runtime, so you can use it in switch statements and comparisons. -* `WebhookContentType` is deprecated but still available for backward compatibility. +* Use the `WebhookContentType` to specify the exact event type (e.g., `SubscriptionCreated`, `InvoiceGenerated`, etc.). +* This approach ensures you get proper IntelliSense and compile-time checks when accessing event fields. ### Custom HTTP Client diff --git a/package-lock.json b/package-lock.json index e72cfc7..93328e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,19 +8,930 @@ "name": "chargebee", "version": "3.19.0", "devDependencies": { - "@types/node": "20.0.0", + "@types/chai": "^4.3.5", + "@types/mocha": "^10.0.10", + "@types/node": "20.12.0", + "chai": "^4.3.7", + "mocha": "^10.2.0", "prettier": "^3.3.3", - "typescript": "^5.5.4" + "ts-node": "^10.9.1", + "typescript": "^5.5.4", + "undici-types": "^7.16.0" }, "engines": { "node": ">=18.*" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.0.0.tgz", - "integrity": "sha512-cD2uPTDnQQCVpmRefonO98/PPijuOnnEy5oytWJFPY1N9aJCz2wJ5kSGWO+zJoed2cY2JxQh6yBuUq4vIn61hw==", - "dev": true + "version": "20.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.0.tgz", + "integrity": "sha512-jVC7fWX1Did5TNn8mmGsE81mdyv+7a+nHNlUiNVys8G392CfNfhqAVRd+cuY0+OBU2vN6GzpiRX/MgJ9b3rtpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, "node_modules/prettier": { "version": "3.3.3", @@ -37,6 +948,204 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", @@ -49,6 +1158,130 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 31fbd72..6c26350 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "A library for integrating with Chargebee.", "scripts": { "prepack": "npm install && npm run build", + "test": "mocha -r ts-node/register 'test/**/*.test.ts'", "build": "npm run build-esm && npm run build-cjs", "build-esm": "rm -rf esm && mkdir -p esm && tsc -p tsconfig.esm.json && echo '{\"type\":\"module\"}' > esm/package.json", "build-cjs": "rm -rf cjs && mkdir -p cjs && tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > cjs/package.json", @@ -62,9 +63,15 @@ } }, "devDependencies": { - "@types/node": "20.0.0", + "@types/chai": "^4.3.5", + "@types/mocha": "^10.0.10", + "@types/node": "20.12.0", + "chai": "^4.3.7", + "mocha": "^10.2.0", "prettier": "^3.3.3", - "typescript": "^5.5.4" + "ts-node": "^10.9.1", + "typescript": "^5.5.4", + "undici-types": "^7.16.0" }, "prettier": { "semi": true, diff --git a/src/chargebee.cjs.ts b/src/chargebee.cjs.ts index df07a5f..d095c85 100644 --- a/src/chargebee.cjs.ts +++ b/src/chargebee.cjs.ts @@ -1,9 +1,12 @@ import { CreateChargebee } from './createChargebee.js'; import { FetchHttpClient } from './net/FetchClient.js'; import { + WebhookHandler, WebhookEventType, WebhookContentType, -} from './resources/webhook/eventType.js'; +} from './resources/webhook/handler.js'; +import webhookInstance from './resources/webhook/handler.js'; +import { basicAuthValidator } from './resources/webhook/auth.js'; const httpClient = new FetchHttpClient(); const Chargebee = CreateChargebee(httpClient); @@ -11,6 +14,19 @@ module.exports = Chargebee; module.exports.Chargebee = Chargebee; module.exports.default = Chargebee; +// Export webhook modules +module.exports.WebhookHandler = WebhookHandler; // Export webhook event types module.exports.WebhookEventType = WebhookEventType; module.exports.WebhookContentType = WebhookContentType; +module.exports.webhook = webhookInstance; +module.exports.basicAuthValidator = basicAuthValidator; + +// Export webhook types +export type { + WebhookEvent, + WebhookContext, + WebhookHandlerOptions, + RequestValidator, +} from './resources/webhook/handler.js'; +export type { CredentialValidator } from './resources/webhook/auth.js'; diff --git a/src/chargebee.esm.ts b/src/chargebee.esm.ts index 680b2b5..803ca7c 100644 --- a/src/chargebee.esm.ts +++ b/src/chargebee.esm.ts @@ -6,8 +6,20 @@ const Chargebee = CreateChargebee(httpClient); export default Chargebee; -// Export webhook event types +// Export webhook modules export { + WebhookHandler, WebhookEventType, WebhookContentType, -} from './resources/webhook/eventType.js'; +} from './resources/webhook/handler.js'; +export { default as webhook } from './resources/webhook/handler.js'; +export { basicAuthValidator } from './resources/webhook/auth.js'; + +// Export webhook types +export type { + WebhookEvent, + WebhookContext, + WebhookHandlerOptions, + RequestValidator, +} from './resources/webhook/handler.js'; +export type { CredentialValidator } from './resources/webhook/auth.js'; diff --git a/src/resources/webhook/auth.ts b/src/resources/webhook/auth.ts new file mode 100644 index 0000000..bf8e184 --- /dev/null +++ b/src/resources/webhook/auth.ts @@ -0,0 +1,66 @@ +/** + * Credential validator function type. + * Can be synchronous or asynchronous (e.g., for database lookups). + */ +export type CredentialValidator = ( + username: string, + password: string, +) => boolean | Promise; + +/** + * Creates a Basic Auth validator for webhook requests. + * Parses the Authorization header and validates credentials. + * + * @param validateCredentials - Function to validate username/password. + * Can be sync or async (e.g., for database lookups). + * @returns A request validator function for use with WebhookHandler + * + * @example + * // Simple sync validation + * const validator = basicAuthValidator((u, p) => u === 'admin' && p === 'secret'); + * + * @example + * // Async validation (e.g., database lookup) + * const validator = basicAuthValidator(async (u, p) => { + * const user = await db.findUser(u); + * return user && await bcrypt.compare(p, user.passwordHash); + * }); + */ +export const basicAuthValidator = ( + validateCredentials: CredentialValidator, +) => { + return async ( + headers: Record, + ): Promise => { + const authHeader = headers['authorization'] || headers['Authorization']; + + if (!authHeader) { + throw new Error('Missing authorization header'); + } + + const authStr = Array.isArray(authHeader) ? authHeader[0] : authHeader; + if (!authStr) { + throw new Error('Invalid authorization header'); + } + + const parts = authStr.split(' '); + if (parts.length !== 2 || parts[0] !== 'Basic') { + throw new Error('Invalid authorization header format'); + } + + const decoded = Buffer.from(parts[1], 'base64').toString(); + const separatorIndex = decoded.indexOf(':'); + + if (separatorIndex === -1) { + throw new Error('Invalid credentials format'); + } + + const username = decoded.substring(0, separatorIndex); + const password = decoded.substring(separatorIndex + 1); + + const isValid = await validateCredentials(username, password); + if (!isValid) { + throw new Error('Invalid credentials'); + } + }; +}; diff --git a/src/resources/webhook/content.ts b/src/resources/webhook/content.ts new file mode 100644 index 0000000..74321a6 --- /dev/null +++ b/src/resources/webhook/content.ts @@ -0,0 +1,1402 @@ +/// + +export interface AddUsagesReminderContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + UsageReminderInfo: import('chargebee').UsageReminderInfo; +} + +export interface AddonCreatedContent { + Addon: import('chargebee').Addon; +} + +export interface AddonDeletedContent { + Addon: import('chargebee').Addon; +} + +export interface AddonUpdatedContent { + Addon: import('chargebee').Addon; +} + +export interface AttachedItemCreatedContent { + AttachedItem: import('chargebee').AttachedItem; +} + +export interface AttachedItemDeletedContent { + AttachedItem: import('chargebee').AttachedItem; +} + +export interface AttachedItemUpdatedContent { + AttachedItem: import('chargebee').AttachedItem; +} + +export interface AuthorizationSucceededContent { + Transaction: import('chargebee').Transaction; +} + +export interface AuthorizationVoidedContent { + Transaction: import('chargebee').Transaction; +} + +export interface BusinessEntityCreatedContent { + BusinessEntity: import('chargebee').BusinessEntity; +} + +export interface BusinessEntityDeletedContent { + BusinessEntity: import('chargebee').BusinessEntity; +} + +export interface BusinessEntityUpdatedContent { + BusinessEntity: import('chargebee').BusinessEntity; +} + +export interface CardAddedContent { + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; +} + +export interface CardDeletedContent { + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; +} + +export interface CardExpiredContent { + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; +} + +export interface CardExpiryReminderContent { + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; +} + +export interface CardUpdatedContent { + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; +} + +export interface ContractTermCancelledContent { + ContractTerm: import('chargebee').ContractTerm; +} + +export interface ContractTermCompletedContent { + ContractTerm: import('chargebee').ContractTerm; +} + +export interface ContractTermCreatedContent { + ContractTerm: import('chargebee').ContractTerm; +} + +export interface ContractTermRenewedContent { + ContractTerm: import('chargebee').ContractTerm; +} + +export interface ContractTermTerminatedContent { + ContractTerm: import('chargebee').ContractTerm; +} + +export interface CouponCodesAddedContent { + Coupon: import('chargebee').Coupon; + + CouponSet: import('chargebee').CouponSet; +} + +export interface CouponCodesDeletedContent { + Coupon: import('chargebee').Coupon; + + CouponSet: import('chargebee').CouponSet; + + CouponCode: import('chargebee').CouponCode; +} + +export interface CouponCodesUpdatedContent { + Coupon: import('chargebee').Coupon; + + CouponSet: import('chargebee').CouponSet; +} + +export interface CouponCreatedContent { + Coupon: import('chargebee').Coupon; +} + +export interface CouponDeletedContent { + Coupon: import('chargebee').Coupon; +} + +export interface CouponSetCreatedContent { + Coupon: import('chargebee').Coupon; + + CouponSet: import('chargebee').CouponSet; +} + +export interface CouponSetDeletedContent { + Coupon: import('chargebee').Coupon; + + CouponSet: import('chargebee').CouponSet; +} + +export interface CouponSetUpdatedContent { + Coupon: import('chargebee').Coupon; + + CouponSet: import('chargebee').CouponSet; +} + +export interface CouponUpdatedContent { + Coupon: import('chargebee').Coupon; +} + +export interface CreditNoteCreatedContent { + CreditNote: import('chargebee').CreditNote; +} + +export interface CreditNoteCreatedWithBackdatingContent { + CreditNote: import('chargebee').CreditNote; +} + +export interface CreditNoteDeletedContent { + CreditNote: import('chargebee').CreditNote; +} + +export interface CreditNoteUpdatedContent { + CreditNote: import('chargebee').CreditNote; +} + +export interface CustomerBusinessEntityChangedContent { + BusinessEntityTransfer: import('chargebee').BusinessEntityTransfer; + + Customer: import('chargebee').Customer; +} + +export interface CustomerChangedContent { + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; +} + +export interface CustomerCreatedContent { + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; +} + +export interface CustomerDeletedContent { + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; +} + +export interface CustomerEntitlementsUpdatedContent { + ImpactedCustomer: import('chargebee').ImpactedCustomer; +} + +export interface CustomerMovedInContent { + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; +} + +export interface CustomerMovedOutContent { + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; +} + +export interface DifferentialPriceCreatedContent { + DifferentialPrice: import('chargebee').DifferentialPrice; +} + +export interface DifferentialPriceDeletedContent { + DifferentialPrice: import('chargebee').DifferentialPrice; +} + +export interface DifferentialPriceUpdatedContent { + DifferentialPrice: import('chargebee').DifferentialPrice; +} + +export interface DunningUpdatedContent { + Invoice: import('chargebee').Invoice; +} + +export interface EntitlementOverridesAutoRemovedContent { + Feature: import('chargebee').Feature; + + Metadata: import('chargebee').Metadata; + + ImpactedItem: import('chargebee').ImpactedItem; + + ImpactedSubscription: import('chargebee').ImpactedSubscription; +} + +export interface EntitlementOverridesRemovedContent { + ImpactedSubscription: import('chargebee').ImpactedSubscription; + + Metadata: import('chargebee').Metadata; +} + +export interface EntitlementOverridesUpdatedContent { + ImpactedSubscription: import('chargebee').ImpactedSubscription; + + Metadata: import('chargebee').Metadata; +} + +export interface FeatureActivatedContent { + Feature: import('chargebee').Feature; + + Metadata: import('chargebee').Metadata; + + ImpactedItem: import('chargebee').ImpactedItem; + + ImpactedSubscription: import('chargebee').ImpactedSubscription; +} + +export interface FeatureArchivedContent { + Feature: import('chargebee').Feature; + + Metadata: import('chargebee').Metadata; +} + +export interface FeatureCreatedContent { + Feature: import('chargebee').Feature; + + Metadata: import('chargebee').Metadata; + + ImpactedItem: import('chargebee').ImpactedItem; + + ImpactedSubscription: import('chargebee').ImpactedSubscription; +} + +export interface FeatureDeletedContent { + Feature: import('chargebee').Feature; + + Metadata: import('chargebee').Metadata; + + ImpactedItem: import('chargebee').ImpactedItem; + + ImpactedSubscription: import('chargebee').ImpactedSubscription; +} + +export interface FeatureReactivatedContent { + Feature: import('chargebee').Feature; + + Metadata: import('chargebee').Metadata; +} + +export interface FeatureUpdatedContent { + Feature: import('chargebee').Feature; + + Metadata: import('chargebee').Metadata; +} + +export interface GiftCancelledContent { + Gift: import('chargebee').Gift; +} + +export interface GiftClaimedContent { + Gift: import('chargebee').Gift; +} + +export interface GiftExpiredContent { + Gift: import('chargebee').Gift; +} + +export interface GiftScheduledContent { + Gift: import('chargebee').Gift; +} + +export interface GiftUnclaimedContent { + Gift: import('chargebee').Gift; +} + +export interface GiftUpdatedContent { + Gift: import('chargebee').Gift; +} + +export interface HierarchyCreatedContent { + Customer: import('chargebee').Customer; +} + +export interface HierarchyDeletedContent { + Customer: import('chargebee').Customer; +} + +export interface InvoiceDeletedContent { + Invoice: import('chargebee').Invoice; +} + +export interface InvoiceGeneratedContent { + Invoice: import('chargebee').Invoice; +} + +export interface InvoiceGeneratedWithBackdatingContent { + Invoice: import('chargebee').Invoice; +} + +export interface InvoiceUpdatedContent { + Invoice: import('chargebee').Invoice; +} + +export interface ItemCreatedContent { + Item: import('chargebee').Item; +} + +export interface ItemDeletedContent { + Item: import('chargebee').Item; +} + +export interface ItemEntitlementsRemovedContent { + Feature: import('chargebee').Feature; + + Metadata: import('chargebee').Metadata; + + ImpactedItem: import('chargebee').ImpactedItem; + + ImpactedSubscription: import('chargebee').ImpactedSubscription; +} + +export interface ItemEntitlementsUpdatedContent { + Feature: import('chargebee').Feature; + + Metadata: import('chargebee').Metadata; + + ImpactedItem: import('chargebee').ImpactedItem; + + ImpactedSubscription: import('chargebee').ImpactedSubscription; +} + +export interface ItemFamilyCreatedContent { + ItemFamily: import('chargebee').ItemFamily; +} + +export interface ItemFamilyDeletedContent { + ItemFamily: import('chargebee').ItemFamily; +} + +export interface ItemFamilyUpdatedContent { + ItemFamily: import('chargebee').ItemFamily; +} + +export interface ItemPriceCreatedContent { + ItemPrice: import('chargebee').ItemPrice; +} + +export interface ItemPriceDeletedContent { + ItemPrice: import('chargebee').ItemPrice; +} + +export interface ItemPriceEntitlementsRemovedContent { + Feature: import('chargebee').Feature; + + Metadata: import('chargebee').Metadata; + + ImpactedItemPrice: import('chargebee').ImpactedItemPrice; + + ImpactedSubscription: import('chargebee').ImpactedSubscription; +} + +export interface ItemPriceEntitlementsUpdatedContent { + Feature: import('chargebee').Feature; + + Metadata: import('chargebee').Metadata; + + ImpactedItemPrice: import('chargebee').ImpactedItemPrice; + + ImpactedSubscription: import('chargebee').ImpactedSubscription; +} + +export interface ItemPriceUpdatedContent { + ItemPrice: import('chargebee').ItemPrice; +} + +export interface ItemUpdatedContent { + Item: import('chargebee').Item; +} + +export interface MrrUpdatedContent { + Subscription: import('chargebee').Subscription; +} + +export interface NetdPaymentDueReminderContent { + Invoice: import('chargebee').Invoice; +} + +export interface OmnichannelOneTimeOrderCreatedContent { + OmnichannelOneTimeOrder: import('chargebee').OmnichannelOneTimeOrder; + + OmnichannelOneTimeOrderItem: import('chargebee').OmnichannelOneTimeOrderItem; + + OmnichannelTransaction: import('chargebee').OmnichannelTransaction; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelOneTimeOrderItemCancelledContent { + OmnichannelOneTimeOrder: import('chargebee').OmnichannelOneTimeOrder; + + OmnichannelOneTimeOrderItem: import('chargebee').OmnichannelOneTimeOrderItem; + + OmnichannelTransaction: import('chargebee').OmnichannelTransaction; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionCreatedContent { + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelTransaction: import('chargebee').OmnichannelTransaction; + + OmnichannelSubscriptionItemScheduledChange: import('chargebee').OmnichannelSubscriptionItemScheduledChange; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionImportedContent { + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelTransaction: import('chargebee').OmnichannelTransaction; + + OmnichannelSubscriptionItemScheduledChange: import('chargebee').OmnichannelSubscriptionItemScheduledChange; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemCancellationScheduledContent { + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemCancelledContent { + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemChangeScheduledContent { + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + OmnichannelSubscriptionItemScheduledChange: import('chargebee').OmnichannelSubscriptionItemScheduledChange; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemChangedContent { + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelTransaction: import('chargebee').OmnichannelTransaction; + + OmnichannelSubscriptionItemScheduledChange: import('chargebee').OmnichannelSubscriptionItemScheduledChange; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemDowngradeScheduledContent { + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemDowngradedContent { + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelTransaction: import('chargebee').OmnichannelTransaction; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemDunningExpiredContent { + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemDunningStartedContent { + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemExpiredContent { + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemGracePeriodExpiredContent { + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemGracePeriodStartedContent { + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemPauseScheduledContent { + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + OmnichannelSubscriptionItemScheduledChange: import('chargebee').OmnichannelSubscriptionItemScheduledChange; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemPausedContent { + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemReactivatedContent { + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemRenewedContent { + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelTransaction: import('chargebee').OmnichannelTransaction; + + OmnichannelSubscriptionItemScheduledChange: import('chargebee').OmnichannelSubscriptionItemScheduledChange; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemResubscribedContent { + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelTransaction: import('chargebee').OmnichannelTransaction; + + OmnichannelSubscriptionItemScheduledChange: import('chargebee').OmnichannelSubscriptionItemScheduledChange; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemResumedContent { + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemScheduledCancellationRemovedContent { + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemScheduledChangeRemovedContent { + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + OmnichannelSubscriptionItemScheduledChange: import('chargebee').OmnichannelSubscriptionItemScheduledChange; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemScheduledDowngradeRemovedContent { + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionItemUpgradedContent { + OmnichannelSubscriptionItem: import('chargebee').OmnichannelSubscriptionItem; + + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + OmnichannelTransaction: import('chargebee').OmnichannelTransaction; + + OmnichannelSubscriptionItemScheduledChange: import('chargebee').OmnichannelSubscriptionItemScheduledChange; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelSubscriptionMovedInContent { + OmnichannelSubscription: import('chargebee').OmnichannelSubscription; + + Customer: import('chargebee').Customer; +} + +export interface OmnichannelTransactionCreatedContent { + OmnichannelTransaction: import('chargebee').OmnichannelTransaction; +} + +export interface OrderCancelledContent { + Order: import('chargebee').Order; +} + +export interface OrderCreatedContent { + Order: import('chargebee').Order; +} + +export interface OrderDeletedContent { + Order: import('chargebee').Order; +} + +export interface OrderDeliveredContent { + Order: import('chargebee').Order; +} + +export interface OrderReadyToProcessContent { + Order: import('chargebee').Order; +} + +export interface OrderReadyToShipContent { + Order: import('chargebee').Order; +} + +export interface OrderResentContent { + Order: import('chargebee').Order; +} + +export interface OrderReturnedContent { + Order: import('chargebee').Order; +} + +export interface OrderUpdatedContent { + Order: import('chargebee').Order; +} + +export interface PaymentFailedContent { + Transaction: import('chargebee').Transaction; + + Invoice: import('chargebee').Invoice; + + Customer: import('chargebee').Customer; + + Subscription: import('chargebee').Subscription; + + Card: import('chargebee').Card; +} + +export interface PaymentInitiatedContent { + Transaction: import('chargebee').Transaction; + + Invoice: import('chargebee').Invoice; + + Customer: import('chargebee').Customer; + + Subscription: import('chargebee').Subscription; + + Card: import('chargebee').Card; +} + +export interface PaymentIntentCreatedContent { + PaymentIntent: import('chargebee').PaymentIntent; +} + +export interface PaymentIntentUpdatedContent { + PaymentIntent: import('chargebee').PaymentIntent; +} + +export interface PaymentRefundedContent { + Transaction: import('chargebee').Transaction; + + Invoice: import('chargebee').Invoice; + + CreditNote: import('chargebee').CreditNote; + + Customer: import('chargebee').Customer; + + Subscription: import('chargebee').Subscription; + + Card: import('chargebee').Card; +} + +export interface PaymentScheduleSchemeCreatedContent { + PaymentScheduleScheme: import('chargebee').PaymentScheduleScheme; +} + +export interface PaymentScheduleSchemeDeletedContent { + PaymentScheduleScheme: import('chargebee').PaymentScheduleScheme; +} + +export interface PaymentSchedulesCreatedContent { + PaymentSchedule: import('chargebee').PaymentSchedule; +} + +export interface PaymentSchedulesUpdatedContent { + PaymentSchedule: import('chargebee').PaymentSchedule; +} + +export interface PaymentSourceAddedContent { + Customer: import('chargebee').Customer; + + PaymentSource: import('chargebee').PaymentSource; +} + +export interface PaymentSourceDeletedContent { + Customer: import('chargebee').Customer; + + PaymentSource: import('chargebee').PaymentSource; +} + +export interface PaymentSourceExpiredContent { + Customer: import('chargebee').Customer; + + PaymentSource: import('chargebee').PaymentSource; +} + +export interface PaymentSourceExpiringContent { + Customer: import('chargebee').Customer; + + PaymentSource: import('chargebee').PaymentSource; +} + +export interface PaymentSourceLocallyDeletedContent { + Customer: import('chargebee').Customer; + + PaymentSource: import('chargebee').PaymentSource; +} + +export interface PaymentSourceUpdatedContent { + Customer: import('chargebee').Customer; + + PaymentSource: import('chargebee').PaymentSource; +} + +export interface PaymentSucceededContent { + Transaction: import('chargebee').Transaction; + + Invoice: import('chargebee').Invoice; + + Customer: import('chargebee').Customer; + + Subscription: import('chargebee').Subscription; + + Card: import('chargebee').Card; +} + +export interface PendingInvoiceCreatedContent { + Invoice: import('chargebee').Invoice; +} + +export interface PendingInvoiceUpdatedContent { + Invoice: import('chargebee').Invoice; +} + +export interface PlanCreatedContent { + Plan: import('chargebee').Plan; +} + +export interface PlanDeletedContent { + Plan: import('chargebee').Plan; +} + +export interface PlanUpdatedContent { + Plan: import('chargebee').Plan; +} + +export interface PriceVariantCreatedContent { + PriceVariant: import('chargebee').PriceVariant; + + Attribute: import('chargebee').Attribute; +} + +export interface PriceVariantDeletedContent { + PriceVariant: import('chargebee').PriceVariant; + + Attribute: import('chargebee').Attribute; +} + +export interface PriceVariantUpdatedContent { + PriceVariant: import('chargebee').PriceVariant; + + Attribute: import('chargebee').Attribute; +} + +export interface ProductCreatedContent { + Product: import('chargebee').Product; +} + +export interface ProductDeletedContent { + Product: import('chargebee').Product; +} + +export interface ProductUpdatedContent { + Product: import('chargebee').Product; +} + +export interface PromotionalCreditsAddedContent { + Customer: import('chargebee').Customer; + + PromotionalCredit: import('chargebee').PromotionalCredit; +} + +export interface PromotionalCreditsDeductedContent { + Customer: import('chargebee').Customer; + + PromotionalCredit: import('chargebee').PromotionalCredit; +} + +export interface PurchaseCreatedContent { + Purchase: import('chargebee').Purchase; +} + +export interface QuoteCreatedContent { + Quote: import('chargebee').Quote; +} + +export interface QuoteDeletedContent { + Quote: import('chargebee').Quote; +} + +export interface QuoteUpdatedContent { + Quote: import('chargebee').Quote; +} + +export interface RecordPurchaseFailedContent { + RecordedPurchase: import('chargebee').RecordedPurchase; + + Customer: import('chargebee').Customer; +} + +export interface RefundInitiatedContent { + Transaction: import('chargebee').Transaction; + + Invoice: import('chargebee').Invoice; + + CreditNote: import('chargebee').CreditNote; + + Customer: import('chargebee').Customer; + + Subscription: import('chargebee').Subscription; + + Card: import('chargebee').Card; +} + +export interface RuleCreatedContent { + Rule: import('chargebee').Rule; +} + +export interface RuleDeletedContent { + Rule: import('chargebee').Rule; +} + +export interface RuleUpdatedContent { + Rule: import('chargebee').Rule; +} + +export interface SalesOrderCreatedContent { + SalesOrder: import('chargebee').SalesOrder; +} + +export interface SalesOrderUpdatedContent { + SalesOrder: import('chargebee').SalesOrder; +} + +export interface SubscriptionActivatedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionActivatedWithBackdatingContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionAdvanceInvoiceScheduleAddedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; +} + +export interface SubscriptionAdvanceInvoiceScheduleRemovedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; +} + +export interface SubscriptionAdvanceInvoiceScheduleUpdatedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; +} + +export interface SubscriptionBusinessEntityChangedContent { + BusinessEntityTransfer: import('chargebee').BusinessEntityTransfer; + + Subscription: import('chargebee').Subscription; +} + +export interface SubscriptionCanceledWithBackdatingContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionCancellationReminderContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + AdvanceInvoiceSchedule: import('chargebee').AdvanceInvoiceSchedule; +} + +export interface SubscriptionCancellationScheduledContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + AdvanceInvoiceSchedule: import('chargebee').AdvanceInvoiceSchedule; +} + +export interface SubscriptionCancelledContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionChangedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionChangedWithBackdatingContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionChangesScheduledContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + AdvanceInvoiceSchedule: import('chargebee').AdvanceInvoiceSchedule; +} + +export interface SubscriptionCreatedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionCreatedWithBackdatingContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionDeletedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + AdvanceInvoiceSchedule: import('chargebee').AdvanceInvoiceSchedule; +} + +export interface SubscriptionEntitlementsCreatedContent { + SubscriptionEntitlementsCreatedDetail: import('chargebee').SubscriptionEntitlementsCreatedDetail; +} + +export interface SubscriptionEntitlementsUpdatedContent { + SubscriptionEntitlementsUpdatedDetail: import('chargebee').SubscriptionEntitlementsUpdatedDetail; +} + +export interface SubscriptionItemsRenewedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionMovedInContent { + Subscription: import('chargebee').Subscription; +} + +export interface SubscriptionMovedOutContent { + Subscription: import('chargebee').Subscription; +} + +export interface SubscriptionMovementFailedContent { + Subscription: import('chargebee').Subscription; +} + +export interface SubscriptionPauseScheduledContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + AdvanceInvoiceSchedule: import('chargebee').AdvanceInvoiceSchedule; +} + +export interface SubscriptionPausedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionRampAppliedContent { + Ramp: import('chargebee').Ramp; +} + +export interface SubscriptionRampCreatedContent { + Ramp: import('chargebee').Ramp; +} + +export interface SubscriptionRampDeletedContent { + Ramp: import('chargebee').Ramp; +} + +export interface SubscriptionRampDraftedContent { + Ramp: import('chargebee').Ramp; +} + +export interface SubscriptionRampUpdatedContent { + Ramp: import('chargebee').Ramp; +} + +export interface SubscriptionReactivatedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionReactivatedWithBackdatingContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionRenewalReminderContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + AdvanceInvoiceSchedule: import('chargebee').AdvanceInvoiceSchedule; +} + +export interface SubscriptionRenewedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionResumedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionResumptionScheduledContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + AdvanceInvoiceSchedule: import('chargebee').AdvanceInvoiceSchedule; +} + +export interface SubscriptionScheduledCancellationRemovedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + AdvanceInvoiceSchedule: import('chargebee').AdvanceInvoiceSchedule; +} + +export interface SubscriptionScheduledChangesRemovedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + AdvanceInvoiceSchedule: import('chargebee').AdvanceInvoiceSchedule; +} + +export interface SubscriptionScheduledPauseRemovedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + AdvanceInvoiceSchedule: import('chargebee').AdvanceInvoiceSchedule; +} + +export interface SubscriptionScheduledResumptionRemovedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + AdvanceInvoiceSchedule: import('chargebee').AdvanceInvoiceSchedule; +} + +export interface SubscriptionShippingAddressUpdatedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + AdvanceInvoiceSchedule: import('chargebee').AdvanceInvoiceSchedule; +} + +export interface SubscriptionStartedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + Invoice: import('chargebee').Invoice; +} + +export interface SubscriptionTrialEndReminderContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + AdvanceInvoiceSchedule: import('chargebee').AdvanceInvoiceSchedule; +} + +export interface SubscriptionTrialExtendedContent { + Subscription: import('chargebee').Subscription; + + Customer: import('chargebee').Customer; + + Card: import('chargebee').Card; + + AdvanceInvoiceSchedule: import('chargebee').AdvanceInvoiceSchedule; +} + +export interface TaxWithheldDeletedContent { + TaxWithheld: import('chargebee').TaxWithheld; + + Invoice: import('chargebee').Invoice; + + CreditNote: import('chargebee').CreditNote; +} + +export interface TaxWithheldRecordedContent { + TaxWithheld: import('chargebee').TaxWithheld; + + Invoice: import('chargebee').Invoice; + + CreditNote: import('chargebee').CreditNote; +} + +export interface TaxWithheldRefundedContent { + TaxWithheld: import('chargebee').TaxWithheld; + + Invoice: import('chargebee').Invoice; + + CreditNote: import('chargebee').CreditNote; +} + +export interface TokenConsumedContent { + Token: import('chargebee').Token; +} + +export interface TokenCreatedContent { + Token: import('chargebee').Token; +} + +export interface TokenExpiredContent { + Token: import('chargebee').Token; +} + +export interface TransactionCreatedContent { + Transaction: import('chargebee').Transaction; +} + +export interface TransactionDeletedContent { + Transaction: import('chargebee').Transaction; +} + +export interface TransactionUpdatedContent { + Transaction: import('chargebee').Transaction; +} + +export interface UnbilledChargesCreatedContent {} + +export interface UnbilledChargesDeletedContent {} + +export interface UnbilledChargesInvoicedContent { + Invoice: import('chargebee').Invoice; +} + +export interface UnbilledChargesVoidedContent {} + +export interface UsageFileIngestedContent { + UsageFile: import('chargebee').UsageFile; +} + +export interface VariantCreatedContent { + Variant: import('chargebee').Variant; +} + +export interface VariantDeletedContent { + Variant: import('chargebee').Variant; +} + +export interface VariantUpdatedContent { + Variant: import('chargebee').Variant; +} + +export interface VirtualBankAccountAddedContent { + Customer: import('chargebee').Customer; + + VirtualBankAccount: import('chargebee').VirtualBankAccount; +} + +export interface VirtualBankAccountDeletedContent { + Customer: import('chargebee').Customer; + + VirtualBankAccount: import('chargebee').VirtualBankAccount; +} + +export interface VirtualBankAccountUpdatedContent { + Customer: import('chargebee').Customer; + + VirtualBankAccount: import('chargebee').VirtualBankAccount; +} + +export interface VoucherCreateFailedContent { + PaymentVoucher: import('chargebee').PaymentVoucher; +} + +export interface VoucherCreatedContent { + PaymentVoucher: import('chargebee').PaymentVoucher; +} + +export interface VoucherExpiredContent { + PaymentVoucher: import('chargebee').PaymentVoucher; +} + +export interface WebhookEvent { + id: string; + occurred_at: number; + source: string; + user?: string; + webhook_status: string; + webhook_failure_reason?: string; + webhooks?: any[]; + event_type: string; + api_version: string; + content: any; +} diff --git a/src/resources/webhook/handler.ts b/src/resources/webhook/handler.ts new file mode 100644 index 0000000..33fc6cb --- /dev/null +++ b/src/resources/webhook/handler.ts @@ -0,0 +1,139 @@ +import { EventEmitter } from 'node:events'; +import { WebhookEvent } from './content.js'; +import { basicAuthValidator } from './auth.js'; +import { WebhookEventType, WebhookContentType } from './eventType.js'; + +export { WebhookEventType, WebhookContentType }; + +export type EventType = import('chargebee').EventTypeEnum; + +/** + * Context object passed to webhook event listeners. + * Wraps the event data with optional framework-specific request/response objects. + */ +export interface WebhookContext { + /** The parsed webhook event from Chargebee */ + event: WebhookEvent; + /** Framework-specific request object (Express, Fastify, etc.) */ + request?: ReqT; + /** Framework-specific response object (Express, Fastify, etc.) */ + response?: ResT; +} + +export interface WebhookEventMap + extends Record]> { + unhandled_event: [WebhookContext]; + error: [Error]; +} + +export type WebhookEventListener< + ReqT, + ResT, + K extends keyof WebhookEventMap, +> = (...args: WebhookEventMap[K]) => Promise | void; + +/** + * Validator function type for authenticating webhook requests. + * Can be synchronous or asynchronous. + */ +export type RequestValidator = ( + headers: Record, +) => void | Promise; + +/** + * Configuration options for WebhookHandler. + */ +export interface WebhookHandlerOptions { + /** + * Optional validator function to authenticate incoming webhook requests. + * Typically used for Basic Auth validation. + * Can be sync or async - throw an error to reject the request. + */ + requestValidator?: RequestValidator; +} + +export class WebhookHandler< + ReqT = unknown, + ResT = unknown, +> extends EventEmitter> { + private _requestValidator?: RequestValidator; + + constructor(options?: WebhookHandlerOptions) { + super({ captureRejections: true }); + this._requestValidator = options?.requestValidator; + } + + /** + * Gets the current request validator. + */ + get requestValidator(): RequestValidator | undefined { + return this._requestValidator; + } + + /** + * Sets a new request validator. + */ + set requestValidator(validator: RequestValidator | undefined) { + this._requestValidator = validator; + } + + /** + * Handles an incoming webhook request. + * Validates the request (if validator configured), parses the body, + * and emits the appropriate event. + * + * @param body - The raw request body (string) or pre-parsed object + * @param headers - Optional HTTP headers for validation + * @param request - Optional framework-specific request object + * @param response - Optional framework-specific response object + */ + async handle( + body: string | object, + headers?: Record, + request?: ReqT, + response?: ResT, + ): Promise { + try { + if (this._requestValidator && headers) { + await this._requestValidator(headers); + } + + const event: WebhookEvent = + typeof body === 'string' ? JSON.parse(body) : (body as WebhookEvent); + + const context: WebhookContext = { + event, + request, + response, + }; + + const eventType = event.event_type as keyof WebhookEventMap; + + if (this.listenerCount(eventType) > 0) { + this.emit(eventType, context); + } else { + this.emit('unhandled_event', context); + } + } catch (err) { + this.emit('error', err instanceof Error ? err : new Error(String(err))); + } + } +} + +// Default instance for simple use cases +const webhook = new WebhookHandler(); + +// Auto-configure basic auth if env vars are present +const username = process.env.CHARGEBEE_WEBHOOK_USERNAME; +const password = process.env.CHARGEBEE_WEBHOOK_PASSWORD; + +if (username && password) { + webhook.requestValidator = basicAuthValidator( + (u, p) => u === username && p === password, + ); +} + +export default webhook; + +export type { WebhookEvent } from './content.js'; +export { basicAuthValidator, type CredentialValidator } from './auth.js'; diff --git a/test/webhook.test.ts b/test/webhook.test.ts new file mode 100644 index 0000000..1f363f1 --- /dev/null +++ b/test/webhook.test.ts @@ -0,0 +1,395 @@ +import { expect } from 'chai'; +import { WebhookHandler } from '../src/resources/webhook/handler.js'; +import { basicAuthValidator } from '../src/resources/webhook/auth.js'; + +// Helper to re-import the default webhook instance with fresh env vars +async function getDefaultWebhookWithEnv( + env: Record, +): Promise { + // Save original env + const originalEnv = { ...process.env }; + + // Clear module cache for handler + const modulePath = require.resolve('../src/resources/webhook/handler.js'); + delete require.cache[modulePath]; + + // Set new env vars + Object.assign(process.env, env); + + // Re-import + const { default: webhook } = await import( + '../src/resources/webhook/handler.js' + ); + + // Restore original env + process.env = originalEnv; + + return webhook; +} + +const makeEventBody = (eventType: string, content: string = '{}') => { + return JSON.stringify({ + id: 'evt_test_1', + occurred_at: Math.floor(Date.now() / 1000), + event_type: eventType, + api_version: 'v2', + content: JSON.parse(content), + }); +}; + +describe('WebhookHandler', () => { + it('should route to callback successfully', async () => { + let called = false; + const handler = new WebhookHandler(); + handler.on('pending_invoice_created', async (event) => { + called = true; + expect(event.id).to.not.be.empty; + expect(event.event_type).to.equal('pending_invoice_created'); + expect(event.content).to.not.be.null; + }); + + handler.handle(makeEventBody('pending_invoice_created')); + expect(called).to.be.true; + }); + + it('should handle validator error', async () => { + let onErrorCalled = false; + const handler = new WebhookHandler(); + handler.requestValidator = () => { + throw new Error('bad signature'); + }; + handler.on('error', (err: unknown) => { + onErrorCalled = true; + expect((err as Error).message).to.equal('bad signature'); + }); + + handler.handle(makeEventBody('pending_invoice_created'), {}); + expect(onErrorCalled).to.be.true; + }); + + it('should handle sync callback error', () => { + let onErrorCalled = false; + const handler = new WebhookHandler(); + handler.on('pending_invoice_created', () => { + throw new Error('user code failed'); + }); + handler.on('error', (err: unknown) => { + onErrorCalled = true; + expect((err as Error).message).to.equal('user code failed'); + }); + + handler.handle(makeEventBody('pending_invoice_created')); + expect(onErrorCalled).to.be.true; + }); + + it('should handle unknown event', async () => { + let onUnhandledCalled = false; + const handler = new WebhookHandler(); + handler.on('unhandled_event', async (event) => { + onUnhandledCalled = true; + expect(event.event_type).to.equal('non_existing_event'); + }); + + handler.handle(makeEventBody('non_existing_event')); + expect(onUnhandledCalled).to.be.true; + }); + + it('should handle multiple event types', async () => { + let pendingInvoiceCalled = false; + let subscriptionCalled = false; + + const handler = new WebhookHandler(); + handler.on('pending_invoice_created', async () => { + pendingInvoiceCalled = true; + }); + handler.on('subscription_created', async () => { + subscriptionCalled = true; + }); + + handler.handle(makeEventBody('pending_invoice_created')); + expect(pendingInvoiceCalled).to.be.true; + expect(subscriptionCalled).to.be.false; + + pendingInvoiceCalled = false; + handler.handle(makeEventBody('subscription_created')); + expect(pendingInvoiceCalled).to.be.false; + expect(subscriptionCalled).to.be.true; + }); + + it('should handle invalid JSON body', async () => { + let onErrorCalled = false; + const handler = new WebhookHandler(); + handler.on('error', () => { + onErrorCalled = true; + }); + + handler.handle('invalid json'); + expect(onErrorCalled).to.be.true; + }); + + it('should support custom validator', async () => { + let validatorCalled = false; + const handler = new WebhookHandler(); + handler.requestValidator = (headers) => { + validatorCalled = true; + if (headers?.['x-custom-header'] !== 'expected-value') { + throw new Error('missing required header'); + } + }; + + let onErrorCalled = false; + handler.on('error', (err: unknown) => { + onErrorCalled = true; + expect((err as Error).message).to.equal('missing required header'); + }); + + // Fail case + handler.handle(makeEventBody('pending_invoice_created'), {}); + expect(validatorCalled).to.be.true; + expect(onErrorCalled).to.be.true; + + // Success case + validatorCalled = false; + onErrorCalled = false; + handler.handle(makeEventBody('pending_invoice_created'), { + 'x-custom-header': 'expected-value', + }); + expect(validatorCalled).to.be.true; + expect(onErrorCalled).to.be.false; + }); + + it('should support multiple listeners for same event', async () => { + let listener1Called = false; + let listener2Called = false; + + const handler = new WebhookHandler(); + handler.on('customer_created', async () => { + listener1Called = true; + }); + handler.on('customer_created', async () => { + listener2Called = true; + }); + + handler.handle(makeEventBody('customer_created')); + expect(listener1Called).to.be.true; + expect(listener2Called).to.be.true; + }); + + it('should support once() for one-time listeners', async () => { + let callCount = 0; + + const handler = new WebhookHandler(); + handler.once('customer_created', async () => { + callCount++; + }); + + handler.handle(makeEventBody('customer_created')); + handler.handle(makeEventBody('customer_created')); + + expect(callCount).to.equal(1); + }); + + it('should support off() to remove listeners', async () => { + let callCount = 0; + + const handler = new WebhookHandler(); + const listener = async () => { + callCount++; + }; + + handler.on('customer_created', listener); + handler.handle(makeEventBody('customer_created')); + expect(callCount).to.equal(1); + + handler.off('customer_created', listener); + handler.handle(makeEventBody('customer_created')); + expect(callCount).to.equal(1); // Should not increment + }); + + it('should support removeAllListeners()', async () => { + let callCount = 0; + + const handler = new WebhookHandler(); + handler.on('customer_created', async () => { + callCount++; + }); + handler.on('customer_created', async () => { + callCount++; + }); + + handler.removeAllListeners('customer_created'); + handler.handle(makeEventBody('customer_created')); + expect(callCount).to.equal(0); + }); + + it('should correctly report listenerCount', () => { + const handler = new WebhookHandler(); + expect(handler.listenerCount('customer_created')).to.equal(0); + + handler.on('customer_created', async () => {}); + expect(handler.listenerCount('customer_created')).to.equal(1); + + handler.on('customer_created', async () => {}); + expect(handler.listenerCount('customer_created')).to.equal(2); + }); + + it('should support method chaining', async () => { + let customerCreatedCalled = false; + let subscriptionCreatedCalled = false; + + const handler = new WebhookHandler() + .on('customer_created', async () => { + customerCreatedCalled = true; + }) + .on('subscription_created', async () => { + subscriptionCreatedCalled = true; + }); + + handler.handle(makeEventBody('customer_created')); + expect(customerCreatedCalled).to.be.true; + expect(subscriptionCreatedCalled).to.be.false; + }); +}); + +describe('BasicAuthValidator', () => { + const validator = basicAuthValidator((username, password) => { + return username === 'testuser' && password === 'testpass'; + }); + + it('should validate valid credentials', () => { + const auth = 'Basic ' + Buffer.from('testuser:testpass').toString('base64'); + expect(() => validator({ authorization: auth })).to.not.throw(); + }); + + it('should reject invalid credentials', () => { + const auth = 'Basic ' + Buffer.from('wrong:wrong').toString('base64'); + expect(() => validator({ authorization: auth })).to.throw( + 'Invalid credentials', + ); + }); + + it('should reject missing header', () => { + expect(() => validator({})).to.throw('Invalid authorization header'); + }); + + it('should reject invalid scheme', () => { + expect(() => validator({ authorization: 'Bearer token' })).to.throw( + 'Invalid authorization header', + ); + }); + + it('should reject invalid credentials format', () => { + // Node's Buffer.from() decodes base64 leniently, so this tests the credentials format check + expect(() => validator({ authorization: 'Basic invalid!!!' })).to.throw( + 'Invalid credentials', + ); + }); + + it('should integrate with WebhookHandler', async () => { + let callbackCalled = false; + const handler = new WebhookHandler(); + handler.requestValidator = validator; + handler.on('pending_invoice_created', async () => { + callbackCalled = true; + }); + + const auth = 'Basic ' + Buffer.from('testuser:testpass').toString('base64'); + const body = JSON.stringify({ + event_type: 'pending_invoice_created', + content: {}, + }); + + handler.handle(body, { authorization: auth }); + expect(callbackCalled).to.be.true; + }); +}); + +describe('Default webhook instance', () => { + it('should auto-configure basic auth when env vars are set', async () => { + const webhook = await getDefaultWebhookWithEnv({ + CHARGEBEE_WEBHOOK_USERNAME: 'envuser', + CHARGEBEE_WEBHOOK_PASSWORD: 'envpass', + }); + + expect(webhook.requestValidator).to.not.be.undefined; + + // Valid credentials should pass + const validAuth = + 'Basic ' + Buffer.from('envuser:envpass').toString('base64'); + expect(() => + webhook.requestValidator!({ authorization: validAuth }), + ).to.not.throw(); + + // Invalid credentials should fail + const invalidAuth = + 'Basic ' + Buffer.from('wrong:wrong').toString('base64'); + expect(() => + webhook.requestValidator!({ authorization: invalidAuth }), + ).to.throw('Invalid credentials'); + }); + + it('should not configure auth when env vars are missing', async () => { + const webhook = await getDefaultWebhookWithEnv({ + CHARGEBEE_WEBHOOK_USERNAME: undefined, + CHARGEBEE_WEBHOOK_PASSWORD: undefined, + }); + + expect(webhook.requestValidator).to.be.undefined; + }); + + it('should not configure auth when only username is set', async () => { + const webhook = await getDefaultWebhookWithEnv({ + CHARGEBEE_WEBHOOK_USERNAME: 'envuser', + CHARGEBEE_WEBHOOK_PASSWORD: undefined, + }); + + expect(webhook.requestValidator).to.be.undefined; + }); + + it('should not configure auth when only password is set', async () => { + const webhook = await getDefaultWebhookWithEnv({ + CHARGEBEE_WEBHOOK_USERNAME: undefined, + CHARGEBEE_WEBHOOK_PASSWORD: 'envpass', + }); + + expect(webhook.requestValidator).to.be.undefined; + }); + + it('should work end-to-end with env-configured auth', async () => { + const webhook = await getDefaultWebhookWithEnv({ + CHARGEBEE_WEBHOOK_USERNAME: 'testuser', + CHARGEBEE_WEBHOOK_PASSWORD: 'testpass', + }); + + let callbackCalled = false; + let errorCalled = false; + + webhook.on('customer_created', async () => { + callbackCalled = true; + }); + webhook.on('error', () => { + errorCalled = true; + }); + + const validAuth = + 'Basic ' + Buffer.from('testuser:testpass').toString('base64'); + const body = JSON.stringify({ + id: 'evt_test', + event_type: 'customer_created', + content: {}, + }); + + // With valid auth, callback should be called + webhook.handle(body, { authorization: validAuth }); + expect(callbackCalled).to.be.true; + expect(errorCalled).to.be.false; + + // With invalid auth, error should be emitted + callbackCalled = false; + const invalidAuth = + 'Basic ' + Buffer.from('wrong:wrong').toString('base64'); + webhook.handle(body, { authorization: invalidAuth }); + expect(callbackCalled).to.be.false; + expect(errorCalled).to.be.true; + }); +}); diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index 59884f4..3c2768a 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -2,12 +2,14 @@ "compilerOptions": { "outDir": "./cjs", "module": "commonjs", + "moduleResolution": "node", "target": "es2017", "strict": true, "types": [ "node" ], - "esModuleInterop": false + "esModuleInterop": false, + "skipLibCheck": true }, "include": [ "./src/**/*" diff --git a/tsconfig.json b/tsconfig.json index be0fef8..0dc8a91 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "noEmit": true, "types": [ - "node" + "node", + "mocha" ] }, "ts-node": { @@ -11,5 +12,6 @@ }, "include": [ "src/**/*.ts", + "test/**/*.ts" ] } \ No newline at end of file From 72bef8e0ed3a49a8a037a1df52757307455959bb Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Thu, 22 Jan 2026 11:29:16 +0530 Subject: [PATCH 2/9] add typing for webhook handlers --- types/index.d.ts | 123 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/types/index.d.ts b/types/index.d.ts index 1f4c38a..ed13e58 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -250,4 +250,127 @@ declare module 'chargebee' { virtualBankAccount: VirtualBankAccount.VirtualBankAccountResource; webhookEndpoint: WebhookEndpoint.WebhookEndpointResource; } + + // Webhook Handler Types + export type WebhookEventName = EventTypeEnum | 'unhandled_event'; + export type WebhookEventTypeValue = `${WebhookEventType}`; + /** @deprecated Use WebhookEventTypeValue instead */ + export type WebhookContentTypeValue = WebhookEventTypeValue; + + /** + * Context object passed to webhook event listeners. + * Wraps the event data with optional framework-specific request/response objects. + */ + export interface WebhookContext { + /** The parsed webhook event from Chargebee */ + event: WebhookEvent; + /** Framework-specific request object (Express, Fastify, etc.) */ + request?: ReqT; + /** Framework-specific response object (Express, Fastify, etc.) */ + response?: ResT; + } + + /** + * Validator function type for authenticating webhook requests. + * Can be synchronous or asynchronous. + */ + export type RequestValidator = ( + headers: Record, + ) => void | Promise; + + /** + * Configuration options for WebhookHandler. + */ + export interface WebhookHandlerOptions { + /** + * Optional validator function to authenticate incoming webhook requests. + * Typically used for Basic Auth validation. + * Can be sync or async - throw an error to reject the request. + */ + requestValidator?: RequestValidator; + } + + export type WebhookEventListener< + ReqT = unknown, + ResT = unknown, + T extends WebhookEventType = WebhookEventType, + > = ( + context: WebhookContext & { event: WebhookEvent }, + ) => Promise | void; + export type WebhookErrorListener = (error: Error) => Promise | void; + + // Helper type to map string literal to enum member + type StringToWebhookEventType = { + [K in WebhookEventType]: `${K}` extends S ? K : never; + }[WebhookEventType]; + + export class WebhookHandler { + constructor(options?: WebhookHandlerOptions); + on( + eventName: T, + listener: WebhookEventListener, + ): this; + on( + eventName: S, + listener: WebhookEventListener>, + ): this; + on( + eventName: 'unhandled_event', + listener: WebhookEventListener, + ): this; + on(eventName: 'error', listener: WebhookErrorListener): this; + once( + eventName: T, + listener: WebhookEventListener, + ): this; + once( + eventName: S, + listener: WebhookEventListener>, + ): this; + once( + eventName: 'unhandled_event', + listener: WebhookEventListener, + ): this; + once(eventName: 'error', listener: WebhookErrorListener): this; + off( + eventName: T, + listener: WebhookEventListener, + ): this; + off( + eventName: S, + listener: WebhookEventListener>, + ): this; + off( + eventName: 'unhandled_event', + listener: WebhookEventListener, + ): this; + off(eventName: 'error', listener: WebhookErrorListener): this; + handle( + body: string | object, + headers?: Record, + request?: ReqT, + response?: ResT, + ): Promise; + requestValidator: RequestValidator | undefined; + } + + // Webhook Auth + /** + * Credential validator function type. + * Can be synchronous or asynchronous (e.g., for database lookups). + */ + export type CredentialValidator = ( + username: string, + password: string, + ) => boolean | Promise; + + /** + * Creates a Basic Auth validator for webhook requests. + */ + export function basicAuthValidator( + validateCredentials: CredentialValidator, + ): (headers: Record) => Promise; + + // Default webhook handler instance + export const webhook: WebhookHandler; } From 4b21b4fc87442eae56e09c4d338d605b4491fca6 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Thu, 22 Jan 2026 11:34:42 +0530 Subject: [PATCH 3/9] correct eventtype import --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7acca88..8e0e17d 100644 --- a/README.md +++ b/README.md @@ -392,17 +392,17 @@ To improve type safety and gain better autocompletion when working with webhooks #### Example ```ts -import Chargebee, { type WebhookContentType, WebhookEvent } from "chargebee"; +import Chargebee, { WebhookEventType, WebhookEvent } from "chargebee"; const result = await chargebeeInstance.event.retrieve("{event-id}"); -const subscripitonActivatedEvent: WebhookEvent = result.event; -const subscription = subscripitonActivatedEvent.content.subscription; +const subscriptionActivatedEvent: WebhookEvent = result.event; +const subscription = subscriptionActivatedEvent.content.subscription; ``` #### Notes * `WebhookEvent` provides type hinting for the event payload, making it easier to work with specific event structures. -* Use the `WebhookContentType` to specify the exact event type (e.g., `SubscriptionCreated`, `InvoiceGenerated`, etc.). +* Use the `WebhookEventType` to specify the exact event type (e.g., `SubscriptionCreated`, `InvoiceGenerated`, etc.). * This approach ensures you get proper IntelliSense and compile-time checks when accessing event fields. ### Custom HTTP Client From a19517d8e045afd0564058c1689d84060752e297 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Wed, 4 Feb 2026 10:43:33 +0530 Subject: [PATCH 4/9] refactor(webhooks): use object params for handle() method --- README.md | 91 ++++++++++++++++----------- package.json | 4 +- src/chargebee.cjs.ts | 1 + src/chargebee.esm.ts | 1 + src/resources/webhook/handler.ts | 27 +++++--- test/webhook.test.ts | 105 ++++++++++++++++++------------- 6 files changed, 134 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 8e0e17d..a174192 100644 --- a/README.md +++ b/README.md @@ -158,15 +158,16 @@ The simplest way to handle webhooks is using the pre-configured `webhook` instan ```typescript import express from 'express'; -import { webhook, type WebhookEvent } from 'chargebee'; +import { webhook } from 'chargebee'; const app = express(); app.use(express.json()); -webhook.on('subscription_created', async (event: WebhookEvent) => { +webhook.on('subscription_created', async ({ event, response }) => { console.log(`Subscription created: ${event.id}`); const subscription = event.content.subscription; console.log(`Customer: ${subscription.customer_id}`); + response?.status(200).send('OK'); }); webhook.on('error', (err: Error) => { @@ -174,8 +175,12 @@ webhook.on('error', (err: Error) => { }); app.post('/chargebee/webhooks', (req, res) => { - webhook.handle(req.body, req.headers); - res.status(200).send('OK'); + webhook.handle({ + body: req.body, + headers: req.headers, + request: req, + response: res, + }); }); app.listen(8080); @@ -193,27 +198,30 @@ When both are present, incoming webhook requests will be validated against these For more control or multiple webhook endpoints, create your own instances: ```typescript -import express from 'express'; +import express, { Request, Response } from 'express'; import { WebhookHandler, basicAuthValidator } from 'chargebee'; const app = express(); app.use(express.json()); -const handler = new WebhookHandler(); +// Create a typed handler for Express +const handler = new WebhookHandler(); // Register event listeners using .on() - events are fully typed -handler.on('subscription_created', async (event) => { +handler.on('subscription_created', async ({ event, response }) => { console.log(`Subscription created: ${event.id}`); const subscription = event.content.subscription; console.log(`Customer: ${subscription.customer_id}`); console.log(`Plan: ${subscription.plan_id}`); + response?.status(200).send('OK'); }); -handler.on('payment_succeeded', async (event) => { +handler.on('payment_succeeded', async ({ event, response }) => { console.log(`Payment succeeded: ${event.id}`); const transaction = event.content.transaction; const customer = event.content.customer; console.log(`Amount: ${transaction.amount}, Customer: ${customer.email}`); + response?.status(200).send('OK'); }); // Optional: Add request validator (e.g., Basic Auth) @@ -222,8 +230,12 @@ handler.requestValidator = basicAuthValidator((username, password) => { }); app.post('/chargebee/webhooks', (req, res) => { - handler.handle(req.body, req.headers); - res.status(200).send('OK'); + handler.handle({ + body: req.body, + headers: req.headers, + request: req, + response: res, + }); }); app.listen(8080); @@ -270,52 +282,55 @@ app.post('/chargebee/webhooks', async (req, res) => { app.listen(8080); ``` -#### Handling Unhandled Events +#### Responding to Webhooks -By default, if an incoming webhook event type is not recognized or you haven't registered a corresponding callback handler, the SDK provides flexible options to handle these scenarios: - -**Using the `unhandled_event` listener:** +**Respond with 200** to acknowledge receipt: ```typescript -import { WebhookHandler } from 'chargebee'; +handler.on('subscription_created', async ({ event, response }) => { + await provisionAccess(event.content.subscription); + response?.status(200).json({ received: true }); +}); +``` -const handler = new WebhookHandler(); +**Respond with 5xx** so Chargebee retries on failure: -handler.on('subscription_created', async (event) => { - // Handle subscription created +```typescript +handler.on('payment_succeeded', async ({ event, response }) => { + try { + await recordPayment(event.content.transaction); + response?.status(200).send('OK'); + } catch (err) { + response?.status(500).json({ error: 'Processing failed' }); + } }); +``` + +**Access request context** (headers, middleware data): -// Gracefully handle events without registered listeners -handler.on('unhandled_event', async (event) => { - console.log(`Received unhandled event: ${event.event_type}`); - // Log for monitoring or store for later processing +```typescript +handler.on('customer_created', async ({ event, request, response }) => { + const tenantId = (request as any)?.tenant?.id; + await createCustomerForTenant(tenantId, event.content.customer); + response?.status(200).send('OK'); }); ``` -**Using the `error` listener for error handling:** - -If an error occurs during webhook processing (e.g., invalid JSON, validator failure), the SDK will emit an `error` event: +#### Handling Unhandled Events and Errors ```typescript -const handler = new WebhookHandler(); - -handler.on('subscription_created', async (event) => { - // Handle subscription created +// Handle events without registered listeners +handler.on('unhandled_event', async ({ event, response }) => { + console.log(`Unhandled: ${event.event_type}`); + response?.status(200).send('OK'); }); -// Catch any errors during webhook processing +// Catch processing errors (invalid JSON, validator failure, etc.) handler.on('error', (err) => { - console.error('Webhook processing error:', err); - // Log to monitoring service, alert team, etc. + console.error('Webhook error:', err.message); }); ``` -**Best Practices:** - -- Use `unhandled_event` listener to acknowledge unknown events (return 200 OK) and log them -- Use `error` listener to catch and handle exceptions thrown during event processing -- Both listeners help ensure your webhook endpoint remains stable even when new event types are introduced by Chargebee - ### Processing Webhooks - API Version Check An attribute `api_version` is added to the [Event](https://apidocs.chargebee.com/docs/api/events) resource, which indicates the API version based on which the event content is structured. In your webhook servers, ensure this `api_version` is the same as the [API version](https://apidocs.chargebee.com/docs/api#versions) used by your webhook server's client library. diff --git a/package.json b/package.json index 6c26350..6de7d9b 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,6 @@ "url": "http://github.com/chargebee/chargebee-node/blob/master/LICENSE" } ], - "dependencies": { - }, "exports": { "types": "./types/index.d.ts", "browser": { @@ -78,4 +76,4 @@ "singleQuote": true, "parser": "typescript" } -} \ No newline at end of file +} diff --git a/src/chargebee.cjs.ts b/src/chargebee.cjs.ts index d095c85..e6ad26d 100644 --- a/src/chargebee.cjs.ts +++ b/src/chargebee.cjs.ts @@ -27,6 +27,7 @@ export type { WebhookEvent, WebhookContext, WebhookHandlerOptions, + HandleOptions, RequestValidator, } from './resources/webhook/handler.js'; export type { CredentialValidator } from './resources/webhook/auth.js'; diff --git a/src/chargebee.esm.ts b/src/chargebee.esm.ts index 803ca7c..f6fd4da 100644 --- a/src/chargebee.esm.ts +++ b/src/chargebee.esm.ts @@ -20,6 +20,7 @@ export type { WebhookEvent, WebhookContext, WebhookHandlerOptions, + HandleOptions, RequestValidator, } from './resources/webhook/handler.js'; export type { CredentialValidator } from './resources/webhook/auth.js'; diff --git a/src/resources/webhook/handler.ts b/src/resources/webhook/handler.ts index 33fc6cb..4f17c92 100644 --- a/src/resources/webhook/handler.ts +++ b/src/resources/webhook/handler.ts @@ -52,6 +52,20 @@ export interface WebhookHandlerOptions { requestValidator?: RequestValidator; } +/** + * Options for the handle() method. + */ +export interface HandleOptions { + /** The raw request body (string) or pre-parsed object */ + body: string | object; + /** Optional HTTP headers for validation */ + headers?: Record; + /** Framework-specific request object (Express, Fastify, etc.) */ + request?: ReqT; + /** Framework-specific response object (Express, Fastify, etc.) */ + response?: ResT; +} + export class WebhookHandler< ReqT = unknown, ResT = unknown, @@ -82,17 +96,10 @@ export class WebhookHandler< * Validates the request (if validator configured), parses the body, * and emits the appropriate event. * - * @param body - The raw request body (string) or pre-parsed object - * @param headers - Optional HTTP headers for validation - * @param request - Optional framework-specific request object - * @param response - Optional framework-specific response object + * @param options - The handle options containing body, headers, request, and response */ - async handle( - body: string | object, - headers?: Record, - request?: ReqT, - response?: ResT, - ): Promise { + async handle(options: HandleOptions): Promise { + const { body, headers, request, response } = options; try { if (this._requestValidator && headers) { await this._requestValidator(headers); diff --git a/test/webhook.test.ts b/test/webhook.test.ts index 1f363f1..8758143 100644 --- a/test/webhook.test.ts +++ b/test/webhook.test.ts @@ -41,14 +41,14 @@ describe('WebhookHandler', () => { it('should route to callback successfully', async () => { let called = false; const handler = new WebhookHandler(); - handler.on('pending_invoice_created', async (event) => { + handler.on('pending_invoice_created', async ({ event }) => { called = true; expect(event.id).to.not.be.empty; expect(event.event_type).to.equal('pending_invoice_created'); expect(event.content).to.not.be.null; }); - handler.handle(makeEventBody('pending_invoice_created')); + handler.handle({ body: makeEventBody('pending_invoice_created') }); expect(called).to.be.true; }); @@ -63,7 +63,7 @@ describe('WebhookHandler', () => { expect((err as Error).message).to.equal('bad signature'); }); - handler.handle(makeEventBody('pending_invoice_created'), {}); + handler.handle({ body: makeEventBody('pending_invoice_created'), headers: {} }); expect(onErrorCalled).to.be.true; }); @@ -78,19 +78,19 @@ describe('WebhookHandler', () => { expect((err as Error).message).to.equal('user code failed'); }); - handler.handle(makeEventBody('pending_invoice_created')); + handler.handle({ body: makeEventBody('pending_invoice_created') }); expect(onErrorCalled).to.be.true; }); it('should handle unknown event', async () => { let onUnhandledCalled = false; const handler = new WebhookHandler(); - handler.on('unhandled_event', async (event) => { + handler.on('unhandled_event', async ({ event }) => { onUnhandledCalled = true; expect(event.event_type).to.equal('non_existing_event'); }); - handler.handle(makeEventBody('non_existing_event')); + handler.handle({ body: makeEventBody('non_existing_event') }); expect(onUnhandledCalled).to.be.true; }); @@ -106,12 +106,12 @@ describe('WebhookHandler', () => { subscriptionCalled = true; }); - handler.handle(makeEventBody('pending_invoice_created')); + handler.handle({ body: makeEventBody('pending_invoice_created') }); expect(pendingInvoiceCalled).to.be.true; expect(subscriptionCalled).to.be.false; pendingInvoiceCalled = false; - handler.handle(makeEventBody('subscription_created')); + handler.handle({ body: makeEventBody('subscription_created') }); expect(pendingInvoiceCalled).to.be.false; expect(subscriptionCalled).to.be.true; }); @@ -123,7 +123,7 @@ describe('WebhookHandler', () => { onErrorCalled = true; }); - handler.handle('invalid json'); + handler.handle({ body: 'invalid json' }); expect(onErrorCalled).to.be.true; }); @@ -144,15 +144,16 @@ describe('WebhookHandler', () => { }); // Fail case - handler.handle(makeEventBody('pending_invoice_created'), {}); + handler.handle({ body: makeEventBody('pending_invoice_created'), headers: {} }); expect(validatorCalled).to.be.true; expect(onErrorCalled).to.be.true; // Success case validatorCalled = false; onErrorCalled = false; - handler.handle(makeEventBody('pending_invoice_created'), { - 'x-custom-header': 'expected-value', + handler.handle({ + body: makeEventBody('pending_invoice_created'), + headers: { 'x-custom-header': 'expected-value' }, }); expect(validatorCalled).to.be.true; expect(onErrorCalled).to.be.false; @@ -170,7 +171,7 @@ describe('WebhookHandler', () => { listener2Called = true; }); - handler.handle(makeEventBody('customer_created')); + handler.handle({ body: makeEventBody('customer_created') }); expect(listener1Called).to.be.true; expect(listener2Called).to.be.true; }); @@ -183,8 +184,8 @@ describe('WebhookHandler', () => { callCount++; }); - handler.handle(makeEventBody('customer_created')); - handler.handle(makeEventBody('customer_created')); + handler.handle({ body: makeEventBody('customer_created') }); + handler.handle({ body: makeEventBody('customer_created') }); expect(callCount).to.equal(1); }); @@ -198,11 +199,11 @@ describe('WebhookHandler', () => { }; handler.on('customer_created', listener); - handler.handle(makeEventBody('customer_created')); + handler.handle({ body: makeEventBody('customer_created') }); expect(callCount).to.equal(1); handler.off('customer_created', listener); - handler.handle(makeEventBody('customer_created')); + handler.handle({ body: makeEventBody('customer_created') }); expect(callCount).to.equal(1); // Should not increment }); @@ -218,7 +219,7 @@ describe('WebhookHandler', () => { }); handler.removeAllListeners('customer_created'); - handler.handle(makeEventBody('customer_created')); + handler.handle({ body: makeEventBody('customer_created') }); expect(callCount).to.equal(0); }); @@ -245,7 +246,7 @@ describe('WebhookHandler', () => { subscriptionCreatedCalled = true; }); - handler.handle(makeEventBody('customer_created')); + handler.handle({ body: makeEventBody('customer_created') }); expect(customerCreatedCalled).to.be.true; expect(subscriptionCreatedCalled).to.be.false; }); @@ -256,33 +257,48 @@ describe('BasicAuthValidator', () => { return username === 'testuser' && password === 'testpass'; }); - it('should validate valid credentials', () => { + it('should validate valid credentials', async () => { const auth = 'Basic ' + Buffer.from('testuser:testpass').toString('base64'); - expect(() => validator({ authorization: auth })).to.not.throw(); + // Should not throw + await validator({ authorization: auth }); }); - it('should reject invalid credentials', () => { + it('should reject invalid credentials', async () => { const auth = 'Basic ' + Buffer.from('wrong:wrong').toString('base64'); - expect(() => validator({ authorization: auth })).to.throw( - 'Invalid credentials', - ); + try { + await validator({ authorization: auth }); + expect.fail('Expected validator to throw'); + } catch (err) { + expect((err as Error).message).to.equal('Invalid credentials'); + } }); - it('should reject missing header', () => { - expect(() => validator({})).to.throw('Invalid authorization header'); + it('should reject missing header', async () => { + try { + await validator({}); + expect.fail('Expected validator to throw'); + } catch (err) { + expect((err as Error).message).to.equal('Missing authorization header'); + } }); - it('should reject invalid scheme', () => { - expect(() => validator({ authorization: 'Bearer token' })).to.throw( - 'Invalid authorization header', - ); + it('should reject invalid scheme', async () => { + try { + await validator({ authorization: 'Bearer token' }); + expect.fail('Expected validator to throw'); + } catch (err) { + expect((err as Error).message).to.equal('Invalid authorization header format'); + } }); - it('should reject invalid credentials format', () => { + it('should reject invalid credentials format', async () => { // Node's Buffer.from() decodes base64 leniently, so this tests the credentials format check - expect(() => validator({ authorization: 'Basic invalid!!!' })).to.throw( - 'Invalid credentials', - ); + try { + await validator({ authorization: 'Basic invalid!!!' }); + expect.fail('Expected validator to throw'); + } catch (err) { + expect((err as Error).message).to.equal('Invalid credentials format'); + } }); it('should integrate with WebhookHandler', async () => { @@ -299,7 +315,7 @@ describe('BasicAuthValidator', () => { content: {}, }); - handler.handle(body, { authorization: auth }); + await handler.handle({ body, headers: { authorization: auth } }); expect(callbackCalled).to.be.true; }); }); @@ -316,16 +332,17 @@ describe('Default webhook instance', () => { // Valid credentials should pass const validAuth = 'Basic ' + Buffer.from('envuser:envpass').toString('base64'); - expect(() => - webhook.requestValidator!({ authorization: validAuth }), - ).to.not.throw(); + await webhook.requestValidator!({ authorization: validAuth }); // Invalid credentials should fail const invalidAuth = 'Basic ' + Buffer.from('wrong:wrong').toString('base64'); - expect(() => - webhook.requestValidator!({ authorization: invalidAuth }), - ).to.throw('Invalid credentials'); + try { + await webhook.requestValidator!({ authorization: invalidAuth }); + expect.fail('Expected validator to throw'); + } catch (err) { + expect((err as Error).message).to.equal('Invalid credentials'); + } }); it('should not configure auth when env vars are missing', async () => { @@ -380,7 +397,7 @@ describe('Default webhook instance', () => { }); // With valid auth, callback should be called - webhook.handle(body, { authorization: validAuth }); + await webhook.handle({ body, headers: { authorization: validAuth } }); expect(callbackCalled).to.be.true; expect(errorCalled).to.be.false; @@ -388,7 +405,7 @@ describe('Default webhook instance', () => { callbackCalled = false; const invalidAuth = 'Basic ' + Buffer.from('wrong:wrong').toString('base64'); - webhook.handle(body, { authorization: invalidAuth }); + await webhook.handle({ body, headers: { authorization: invalidAuth } }); expect(callbackCalled).to.be.false; expect(errorCalled).to.be.true; }); From 6a4ae59db23e6e43917fb6195267ae044386f2e9 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Wed, 4 Feb 2026 11:00:07 +0530 Subject: [PATCH 5/9] chargebee.webhooks instance and createHandler() --- README.md | 34 +++-- src/chargebee.cjs.ts | 5 - src/chargebee.esm.ts | 2 - src/createChargebee.ts | 26 ++++ test/webhook.test.ts | 294 +++++++++++++++++++++++++++++++++++++++++ types/index.d.ts | 33 +++-- 6 files changed, 364 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index a174192..6e95aab 100644 --- a/README.md +++ b/README.md @@ -152,30 +152,35 @@ const chargebeeSiteEU = new Chargebee({ Use the webhook handlers to parse and route webhook payloads from Chargebee with full TypeScript support. -#### Quick Start: Using the default `webhook` instance +#### Quick Start: Using the instance `webhooks` handler -The simplest way to handle webhooks is using the pre-configured `webhook` instance: +The simplest way to handle webhooks is using the `webhooks` property on your initialized Chargebee client: ```typescript import express from 'express'; -import { webhook } from 'chargebee'; +import Chargebee from 'chargebee'; + +const chargebee = new Chargebee({ + site: '{{site}}', + apiKey: '{{api-key}}', +}); const app = express(); app.use(express.json()); -webhook.on('subscription_created', async ({ event, response }) => { +chargebee.webhooks.on('subscription_created', async ({ event, response }) => { console.log(`Subscription created: ${event.id}`); const subscription = event.content.subscription; console.log(`Customer: ${subscription.customer_id}`); response?.status(200).send('OK'); }); -webhook.on('error', (err: Error) => { +chargebee.webhooks.on('error', (err: Error) => { console.error('Webhook error:', err.message); }); app.post('/chargebee/webhooks', (req, res) => { - webhook.handle({ + chargebee.webhooks.handle({ body: req.body, headers: req.headers, request: req, @@ -186,26 +191,31 @@ app.post('/chargebee/webhooks', (req, res) => { app.listen(8080); ``` -**Auto-configured Basic Auth:** The default `webhook` instance automatically configures Basic Auth validation if the following environment variables are set: +**Auto-configured Basic Auth:** The `webhooks` handler automatically configures Basic Auth validation if the following environment variables are set: - `CHARGEBEE_WEBHOOK_USERNAME` - The expected username - `CHARGEBEE_WEBHOOK_PASSWORD` - The expected password -When both are present, incoming webhook requests will be validated against these credentials. If not set, no authentication is applied. +When both are present, incoming webhook requests will be validated against these credentials. -#### Creating custom `WebhookHandler` instances +#### Creating typed webhook handlers -For more control or multiple webhook endpoints, create your own instances: +For more control or multiple webhook endpoints, use `chargebee.webhooks.createHandler()`: ```typescript import express, { Request, Response } from 'express'; -import { WebhookHandler, basicAuthValidator } from 'chargebee'; +import Chargebee, { basicAuthValidator } from 'chargebee'; + +const chargebee = new Chargebee({ + site: '{{site}}', + apiKey: '{{api-key}}', +}); const app = express(); app.use(express.json()); // Create a typed handler for Express -const handler = new WebhookHandler(); +const handler = chargebee.webhooks.createHandler(); // Register event listeners using .on() - events are fully typed handler.on('subscription_created', async ({ event, response }) => { diff --git a/src/chargebee.cjs.ts b/src/chargebee.cjs.ts index e6ad26d..97136ab 100644 --- a/src/chargebee.cjs.ts +++ b/src/chargebee.cjs.ts @@ -1,11 +1,9 @@ import { CreateChargebee } from './createChargebee.js'; import { FetchHttpClient } from './net/FetchClient.js'; import { - WebhookHandler, WebhookEventType, WebhookContentType, } from './resources/webhook/handler.js'; -import webhookInstance from './resources/webhook/handler.js'; import { basicAuthValidator } from './resources/webhook/auth.js'; const httpClient = new FetchHttpClient(); @@ -15,11 +13,8 @@ module.exports.Chargebee = Chargebee; module.exports.default = Chargebee; // Export webhook modules -module.exports.WebhookHandler = WebhookHandler; -// Export webhook event types module.exports.WebhookEventType = WebhookEventType; module.exports.WebhookContentType = WebhookContentType; -module.exports.webhook = webhookInstance; module.exports.basicAuthValidator = basicAuthValidator; // Export webhook types diff --git a/src/chargebee.esm.ts b/src/chargebee.esm.ts index f6fd4da..426bd83 100644 --- a/src/chargebee.esm.ts +++ b/src/chargebee.esm.ts @@ -8,11 +8,9 @@ export default Chargebee; // Export webhook modules export { - WebhookHandler, WebhookEventType, WebhookContentType, } from './resources/webhook/handler.js'; -export { default as webhook } from './resources/webhook/handler.js'; export { basicAuthValidator } from './resources/webhook/auth.js'; // Export webhook types diff --git a/src/createChargebee.ts b/src/createChargebee.ts index 1f4e907..2033c31 100644 --- a/src/createChargebee.ts +++ b/src/createChargebee.ts @@ -11,6 +11,11 @@ import { EndpointTuple, HttpClientInterface, } from './types.js'; +import { + WebhookHandler, + type WebhookHandlerOptions, + basicAuthValidator, +} from './resources/webhook/handler.js'; export const CreateChargebee = (httpClient: HttpClientInterface) => { const Chargebee = function (this: ChargebeeType, conf: Config) { @@ -21,6 +26,27 @@ export const CreateChargebee = (httpClient: HttpClientInterface) => { conf.httpClient != null ? conf.httpClient : httpClient; this._buildResources(); this._endpoints = Endpoints; + + // Initialize webhooks handler for this instance + const handler = new WebhookHandler(); + + // Auto-configure basic auth if env vars are present + const username = process.env.CHARGEBEE_WEBHOOK_USERNAME; + const password = process.env.CHARGEBEE_WEBHOOK_PASSWORD; + if (username && password) { + handler.requestValidator = basicAuthValidator( + (u, p) => u === username && p === password, + ); + } + + // Create webhooks namespace with handler methods + createHandler factory + this.webhooks = Object.assign(handler, { + createHandler( + options?: WebhookHandlerOptions, + ): WebhookHandler { + return new WebhookHandler(options); + }, + }); } as any as { new (): ChargebeeType }; Chargebee.prototype = { _createApiFunc(apiCall: ResourceType, env: EnvType) { diff --git a/test/webhook.test.ts b/test/webhook.test.ts index 8758143..2db7d71 100644 --- a/test/webhook.test.ts +++ b/test/webhook.test.ts @@ -1,6 +1,37 @@ import { expect } from 'chai'; import { WebhookHandler } from '../src/resources/webhook/handler.js'; import { basicAuthValidator } from '../src/resources/webhook/auth.js'; +import { CreateChargebee } from '../src/createChargebee.js'; + +// Mock HTTP client for Chargebee instance +const mockHttpClient = { + makeApiRequest: async () => new Response('{}'), +}; + +// Create Chargebee class +const Chargebee = CreateChargebee(mockHttpClient); + +// Helper to create a fresh Chargebee instance with env vars +function createChargebeeWithEnv(env: Record) { + const originalEnv = { ...process.env }; + Object.assign(process.env, env); + + const chargebee = new (Chargebee as any)({ + site: 'test-site', + apiKey: 'test-key', + }); + + // Restore original env + Object.keys(env).forEach((key) => { + if (originalEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = originalEnv[key]; + } + }); + + return chargebee; +} // Helper to re-import the default webhook instance with fresh env vars async function getDefaultWebhookWithEnv( @@ -410,3 +441,266 @@ describe('Default webhook instance', () => { expect(errorCalled).to.be.true; }); }); + +describe('chargebee.webhooks (instance property)', () => { + it('should have webhooks property on Chargebee instance', () => { + const chargebee = new (Chargebee as any)({ + site: 'test-site', + apiKey: 'test-key', + }); + + expect(chargebee.webhooks).to.not.be.undefined; + expect(chargebee.webhooks.on).to.be.a('function'); + expect(chargebee.webhooks.handle).to.be.a('function'); + expect(chargebee.webhooks.createHandler).to.be.a('function'); + }); + + it('should handle events using chargebee.webhooks.on()', async () => { + const chargebee = new (Chargebee as any)({ + site: 'test-site', + apiKey: 'test-key', + }); + + let called = false; + chargebee.webhooks.on('subscription_created', async ({ event }: any) => { + called = true; + expect(event.event_type).to.equal('subscription_created'); + }); + + const body = JSON.stringify({ + id: 'evt_test', + event_type: 'subscription_created', + content: { subscription: { id: 'sub_123' } }, + }); + + await chargebee.webhooks.handle({ body }); + expect(called).to.be.true; + }); + + it('should pass request and response to callbacks', async () => { + const chargebee = new (Chargebee as any)({ + site: 'test-site', + apiKey: 'test-key', + }); + + let receivedRequest: any; + let receivedResponse: any; + + chargebee.webhooks.on('payment_succeeded', async ({ event, request, response }: any) => { + receivedRequest = request; + receivedResponse = response; + }); + + const mockReq = { headers: { 'x-custom': 'value' } }; + const mockRes = { status: () => ({ send: () => {} }) }; + const body = JSON.stringify({ + id: 'evt_test', + event_type: 'payment_succeeded', + content: {}, + }); + + await chargebee.webhooks.handle({ + body, + request: mockReq, + response: mockRes, + }); + + expect(receivedRequest).to.equal(mockReq); + expect(receivedResponse).to.equal(mockRes); + }); + + it('should auto-configure basic auth from env vars', async () => { + const chargebee = createChargebeeWithEnv({ + CHARGEBEE_WEBHOOK_USERNAME: 'envuser', + CHARGEBEE_WEBHOOK_PASSWORD: 'envpass', + }); + + expect(chargebee.webhooks.requestValidator).to.not.be.undefined; + + // Valid credentials should pass + const validAuth = 'Basic ' + Buffer.from('envuser:envpass').toString('base64'); + await chargebee.webhooks.requestValidator!({ authorization: validAuth }); + + // Invalid credentials should fail + const invalidAuth = 'Basic ' + Buffer.from('wrong:wrong').toString('base64'); + try { + await chargebee.webhooks.requestValidator!({ authorization: invalidAuth }); + expect.fail('Expected validator to throw'); + } catch (err) { + expect((err as Error).message).to.equal('Invalid credentials'); + } + }); + + it('should not configure auth when env vars are missing', () => { + const chargebee = createChargebeeWithEnv({ + CHARGEBEE_WEBHOOK_USERNAME: undefined, + CHARGEBEE_WEBHOOK_PASSWORD: undefined, + }); + + expect(chargebee.webhooks.requestValidator).to.be.undefined; + }); +}); + +describe('chargebee.webhooks.createHandler()', () => { + it('should create a new handler instance', () => { + const chargebee = new (Chargebee as any)({ + site: 'test-site', + apiKey: 'test-key', + }); + + const handler = chargebee.webhooks.createHandler(); + + expect(handler).to.not.be.undefined; + expect(handler.on).to.be.a('function'); + expect(handler.handle).to.be.a('function'); + // Should be a different instance than chargebee.webhooks + expect(handler).to.not.equal(chargebee.webhooks); + }); + + it('should create handler with custom validator', async () => { + const chargebee = new (Chargebee as any)({ + site: 'test-site', + apiKey: 'test-key', + }); + + let validatorCalled = false; + const handler = chargebee.webhooks.createHandler({ + requestValidator: () => { + validatorCalled = true; + }, + }); + + const body = JSON.stringify({ + id: 'evt_test', + event_type: 'customer_created', + content: {}, + }); + + await handler.handle({ body, headers: {} }); + expect(validatorCalled).to.be.true; + }); + + it('should handle events independently from main webhooks', async () => { + const chargebee = new (Chargebee as any)({ + site: 'test-site', + apiKey: 'test-key', + }); + + let mainHandlerCalled = false; + let customHandlerCalled = false; + + // Register on main webhooks + chargebee.webhooks.on('invoice_generated', async () => { + mainHandlerCalled = true; + }); + + // Create separate handler + const customHandler = chargebee.webhooks.createHandler(); + customHandler.on('invoice_generated', async () => { + customHandlerCalled = true; + }); + + const body = JSON.stringify({ + id: 'evt_test', + event_type: 'invoice_generated', + content: {}, + }); + + // Only call custom handler + await customHandler.handle({ body }); + expect(mainHandlerCalled).to.be.false; + expect(customHandlerCalled).to.be.true; + + // Reset and call main webhooks + customHandlerCalled = false; + await chargebee.webhooks.handle({ body }); + expect(mainHandlerCalled).to.be.true; + expect(customHandlerCalled).to.be.false; + }); + + it('should support typed request/response in callbacks', async () => { + const chargebee = new (Chargebee as any)({ + site: 'test-site', + apiKey: 'test-key', + }); + + // Simulating Express-like types + interface MockRequest { + body: any; + headers: Record; + } + interface MockResponse { + status: (code: number) => MockResponse; + json: (data: any) => void; + } + + const handler = chargebee.webhooks.createHandler(); + + let responseStatusCalled = false; + const mockRes: MockResponse = { + status: (code: number) => { + responseStatusCalled = true; + expect(code).to.equal(200); + return mockRes; + }, + json: () => {}, + }; + + handler.on('subscription_cancelled', async ({ response }: any) => { + response?.status(200).json({ received: true }); + }); + + const body = JSON.stringify({ + id: 'evt_test', + event_type: 'subscription_cancelled', + content: {}, + }); + + await handler.handle({ body, response: mockRes }); + expect(responseStatusCalled).to.be.true; + }); + + it('should create multiple independent handlers', async () => { + const chargebee = new (Chargebee as any)({ + site: 'test-site', + apiKey: 'test-key', + }); + + let handler1Called = false; + let handler2Called = false; + + const customerHandler = chargebee.webhooks.createHandler(); + const paymentHandler = chargebee.webhooks.createHandler(); + + customerHandler.on('customer_created', async () => { + handler1Called = true; + }); + + paymentHandler.on('payment_succeeded', async () => { + handler2Called = true; + }); + + // Customer event should only trigger customerHandler + await customerHandler.handle({ + body: JSON.stringify({ + id: 'evt_1', + event_type: 'customer_created', + content: {}, + }), + }); + expect(handler1Called).to.be.true; + expect(handler2Called).to.be.false; + + // Payment event should only trigger paymentHandler + handler1Called = false; + await paymentHandler.handle({ + body: JSON.stringify({ + id: 'evt_2', + event_type: 'payment_succeeded', + content: {}, + }), + }); + expect(handler1Called).to.be.false; + expect(handler2Called).to.be.true; + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index ed13e58..00716f6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -249,6 +249,12 @@ declare module 'chargebee' { variant: Variant.VariantResource; virtualBankAccount: VirtualBankAccount.VirtualBankAccountResource; webhookEndpoint: WebhookEndpoint.WebhookEndpointResource; + /** Webhook handler instance with createHandler factory */ + webhooks: WebhookHandler & { + createHandler( + options?: WebhookHandlerOptions, + ): WebhookHandler; + }; } // Webhook Handler Types @@ -290,6 +296,20 @@ declare module 'chargebee' { requestValidator?: RequestValidator; } + /** + * Options for the handle() method. + */ + export interface HandleOptions { + /** The raw request body (string) or pre-parsed object */ + body: string | object; + /** Optional HTTP headers for validation */ + headers?: Record; + /** Framework-specific request object (Express, Fastify, etc.) */ + request?: ReqT; + /** Framework-specific response object (Express, Fastify, etc.) */ + response?: ResT; + } + export type WebhookEventListener< ReqT = unknown, ResT = unknown, @@ -304,8 +324,7 @@ declare module 'chargebee' { [K in WebhookEventType]: `${K}` extends S ? K : never; }[WebhookEventType]; - export class WebhookHandler { - constructor(options?: WebhookHandlerOptions); + export interface WebhookHandler { on( eventName: T, listener: WebhookEventListener, @@ -345,12 +364,7 @@ declare module 'chargebee' { listener: WebhookEventListener, ): this; off(eventName: 'error', listener: WebhookErrorListener): this; - handle( - body: string | object, - headers?: Record, - request?: ReqT, - response?: ResT, - ): Promise; + handle(options: HandleOptions): Promise; requestValidator: RequestValidator | undefined; } @@ -370,7 +384,4 @@ declare module 'chargebee' { export function basicAuthValidator( validateCredentials: CredentialValidator, ): (headers: Record) => Promise; - - // Default webhook handler instance - export const webhook: WebhookHandler; } From 1a9ab3424b827e518a8137a1278ec06b6bef044e Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Wed, 4 Feb 2026 11:42:07 +0530 Subject: [PATCH 6/9] utils update --- src/chargebee.cjs.ts | 2 +- src/chargebee.esm.ts | 2 +- src/resources/webhook/handler.ts | 21 +++------------------ types/index.d.ts | 8 +++++--- types/resources/WebhookEvent.d.ts | 4 ++++ 5 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/chargebee.cjs.ts b/src/chargebee.cjs.ts index 97136ab..f1d4ad2 100644 --- a/src/chargebee.cjs.ts +++ b/src/chargebee.cjs.ts @@ -12,7 +12,7 @@ module.exports = Chargebee; module.exports.Chargebee = Chargebee; module.exports.default = Chargebee; -// Export webhook modules +// Export webhook utilities module.exports.WebhookEventType = WebhookEventType; module.exports.WebhookContentType = WebhookContentType; module.exports.basicAuthValidator = basicAuthValidator; diff --git a/src/chargebee.esm.ts b/src/chargebee.esm.ts index 426bd83..f4e1572 100644 --- a/src/chargebee.esm.ts +++ b/src/chargebee.esm.ts @@ -6,7 +6,7 @@ const Chargebee = CreateChargebee(httpClient); export default Chargebee; -// Export webhook modules +// Export webhook utilities export { WebhookEventType, WebhookContentType, diff --git a/src/resources/webhook/handler.ts b/src/resources/webhook/handler.ts index 4f17c92..bf39bff 100644 --- a/src/resources/webhook/handler.ts +++ b/src/resources/webhook/handler.ts @@ -60,9 +60,9 @@ export interface HandleOptions { body: string | object; /** Optional HTTP headers for validation */ headers?: Record; - /** Framework-specific request object (Express, Fastify, etc.) */ + /** Optional framework-specific request object (Express, Fastify, etc.) */ request?: ReqT; - /** Framework-specific response object (Express, Fastify, etc.) */ + /** Optional framework-specific response object (Express, Fastify, etc.) */ response?: ResT; } @@ -96,7 +96,7 @@ export class WebhookHandler< * Validates the request (if validator configured), parses the body, * and emits the appropriate event. * - * @param options - The handle options containing body, headers, request, and response + * @param options - Handle options containing body, headers, request, and response */ async handle(options: HandleOptions): Promise { const { body, headers, request, response } = options; @@ -127,20 +127,5 @@ export class WebhookHandler< } } -// Default instance for simple use cases -const webhook = new WebhookHandler(); - -// Auto-configure basic auth if env vars are present -const username = process.env.CHARGEBEE_WEBHOOK_USERNAME; -const password = process.env.CHARGEBEE_WEBHOOK_PASSWORD; - -if (username && password) { - webhook.requestValidator = basicAuthValidator( - (u, p) => u === username && p === password, - ); -} - -export default webhook; - export type { WebhookEvent } from './content.js'; export { basicAuthValidator, type CredentialValidator } from './auth.js'; diff --git a/types/index.d.ts b/types/index.d.ts index 00716f6..5a78af3 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -249,8 +249,10 @@ declare module 'chargebee' { variant: Variant.VariantResource; virtualBankAccount: VirtualBankAccount.VirtualBankAccountResource; webhookEndpoint: WebhookEndpoint.WebhookEndpointResource; - /** Webhook handler instance with createHandler factory */ + + /** Webhook handler instance with auto-configured Basic Auth (if env vars are set) */ webhooks: WebhookHandler & { + /** Create a new typed webhook handler instance */ createHandler( options?: WebhookHandlerOptions, ): WebhookHandler; @@ -304,9 +306,9 @@ declare module 'chargebee' { body: string | object; /** Optional HTTP headers for validation */ headers?: Record; - /** Framework-specific request object (Express, Fastify, etc.) */ + /** Optional framework-specific request object (Express, Fastify, etc.) */ request?: ReqT; - /** Framework-specific response object (Express, Fastify, etc.) */ + /** Optional framework-specific response object (Express, Fastify, etc.) */ response?: ResT; } diff --git a/types/resources/WebhookEvent.d.ts b/types/resources/WebhookEvent.d.ts index a5fe0b3..be56a32 100644 --- a/types/resources/WebhookEvent.d.ts +++ b/types/resources/WebhookEvent.d.ts @@ -220,6 +220,10 @@ declare module 'chargebee' { PlanCreated = 'plan_created', PlanUpdated = 'plan_updated', } + /** + * @deprecated Use WebhookEventType instead. + */ + export import WebhookContentType = WebhookEventType; /** * @deprecated Use WebhookEventType instead. From 0517eb79e3e13cf37237623073fb77089e10b199 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Wed, 4 Feb 2026 11:45:49 +0530 Subject: [PATCH 7/9] move default auth-validation to util --- src/createChargebee.ts | 15 ++---- src/resources/webhook/handler.ts | 20 ++++++++ test/webhook.test.ts | 81 ++++++++++++++++++++------------ 3 files changed, 75 insertions(+), 41 deletions(-) diff --git a/src/createChargebee.ts b/src/createChargebee.ts index 2033c31..5db138a 100644 --- a/src/createChargebee.ts +++ b/src/createChargebee.ts @@ -14,7 +14,7 @@ import { import { WebhookHandler, type WebhookHandlerOptions, - basicAuthValidator, + createDefaultHandler, } from './resources/webhook/handler.js'; export const CreateChargebee = (httpClient: HttpClientInterface) => { @@ -27,17 +27,8 @@ export const CreateChargebee = (httpClient: HttpClientInterface) => { this._buildResources(); this._endpoints = Endpoints; - // Initialize webhooks handler for this instance - const handler = new WebhookHandler(); - - // Auto-configure basic auth if env vars are present - const username = process.env.CHARGEBEE_WEBHOOK_USERNAME; - const password = process.env.CHARGEBEE_WEBHOOK_PASSWORD; - if (username && password) { - handler.requestValidator = basicAuthValidator( - (u, p) => u === username && p === password, - ); - } + // Initialize webhooks handler with auto-configured Basic Auth (if env vars are set) + const handler = createDefaultHandler(); // Create webhooks namespace with handler methods + createHandler factory this.webhooks = Object.assign(handler, { diff --git a/src/resources/webhook/handler.ts b/src/resources/webhook/handler.ts index bf39bff..aec45df 100644 --- a/src/resources/webhook/handler.ts +++ b/src/resources/webhook/handler.ts @@ -127,5 +127,25 @@ export class WebhookHandler< } } +/** + * Creates a WebhookHandler with auto-configured Basic Auth from environment variables. + * If CHARGEBEE_WEBHOOK_USERNAME and CHARGEBEE_WEBHOOK_PASSWORD are set, + * the handler will automatically validate incoming requests against those credentials. + */ +export function createDefaultHandler< + ReqT = unknown, + ResT = unknown, +>(): WebhookHandler { + const handler = new WebhookHandler(); + const username = process.env.CHARGEBEE_WEBHOOK_USERNAME; + const password = process.env.CHARGEBEE_WEBHOOK_PASSWORD; + if (username && password) { + handler.requestValidator = basicAuthValidator( + (u, p) => u === username && p === password, + ); + } + return handler; +} + export type { WebhookEvent } from './content.js'; export { basicAuthValidator, type CredentialValidator } from './auth.js'; diff --git a/test/webhook.test.ts b/test/webhook.test.ts index 2db7d71..023bf3c 100644 --- a/test/webhook.test.ts +++ b/test/webhook.test.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { WebhookHandler } from '../src/resources/webhook/handler.js'; +import { WebhookHandler, createDefaultHandler } from '../src/resources/webhook/handler.js'; import { basicAuthValidator } from '../src/resources/webhook/auth.js'; import { CreateChargebee } from '../src/createChargebee.js'; @@ -13,8 +13,20 @@ const Chargebee = CreateChargebee(mockHttpClient); // Helper to create a fresh Chargebee instance with env vars function createChargebeeWithEnv(env: Record) { - const originalEnv = { ...process.env }; - Object.assign(process.env, env); + // Save original env + const originalUsername = process.env.CHARGEBEE_WEBHOOK_USERNAME; + const originalPassword = process.env.CHARGEBEE_WEBHOOK_PASSWORD; + + // Clear and set new env vars + delete process.env.CHARGEBEE_WEBHOOK_USERNAME; + delete process.env.CHARGEBEE_WEBHOOK_PASSWORD; + + if (env.CHARGEBEE_WEBHOOK_USERNAME !== undefined) { + process.env.CHARGEBEE_WEBHOOK_USERNAME = env.CHARGEBEE_WEBHOOK_USERNAME; + } + if (env.CHARGEBEE_WEBHOOK_PASSWORD !== undefined) { + process.env.CHARGEBEE_WEBHOOK_PASSWORD = env.CHARGEBEE_WEBHOOK_PASSWORD; + } const chargebee = new (Chargebee as any)({ site: 'test-site', @@ -22,38 +34,49 @@ function createChargebeeWithEnv(env: Record) { }); // Restore original env - Object.keys(env).forEach((key) => { - if (originalEnv[key] === undefined) { - delete process.env[key]; - } else { - process.env[key] = originalEnv[key]; - } - }); + delete process.env.CHARGEBEE_WEBHOOK_USERNAME; + delete process.env.CHARGEBEE_WEBHOOK_PASSWORD; + if (originalUsername !== undefined) { + process.env.CHARGEBEE_WEBHOOK_USERNAME = originalUsername; + } + if (originalPassword !== undefined) { + process.env.CHARGEBEE_WEBHOOK_PASSWORD = originalPassword; + } return chargebee; } -// Helper to re-import the default webhook instance with fresh env vars -async function getDefaultWebhookWithEnv( +// Helper to create a default webhook handler with fresh env vars +function getDefaultWebhookWithEnv( env: Record, -): Promise { +): WebhookHandler { // Save original env - const originalEnv = { ...process.env }; + const originalUsername = process.env.CHARGEBEE_WEBHOOK_USERNAME; + const originalPassword = process.env.CHARGEBEE_WEBHOOK_PASSWORD; - // Clear module cache for handler - const modulePath = require.resolve('../src/resources/webhook/handler.js'); - delete require.cache[modulePath]; + // Clear and set new env vars + delete process.env.CHARGEBEE_WEBHOOK_USERNAME; + delete process.env.CHARGEBEE_WEBHOOK_PASSWORD; - // Set new env vars - Object.assign(process.env, env); + if (env.CHARGEBEE_WEBHOOK_USERNAME !== undefined) { + process.env.CHARGEBEE_WEBHOOK_USERNAME = env.CHARGEBEE_WEBHOOK_USERNAME; + } + if (env.CHARGEBEE_WEBHOOK_PASSWORD !== undefined) { + process.env.CHARGEBEE_WEBHOOK_PASSWORD = env.CHARGEBEE_WEBHOOK_PASSWORD; + } - // Re-import - const { default: webhook } = await import( - '../src/resources/webhook/handler.js' - ); + // Create handler with env-based auto-config + const webhook = createDefaultHandler(); // Restore original env - process.env = originalEnv; + delete process.env.CHARGEBEE_WEBHOOK_USERNAME; + delete process.env.CHARGEBEE_WEBHOOK_PASSWORD; + if (originalUsername !== undefined) { + process.env.CHARGEBEE_WEBHOOK_USERNAME = originalUsername; + } + if (originalPassword !== undefined) { + process.env.CHARGEBEE_WEBHOOK_PASSWORD = originalPassword; + } return webhook; } @@ -353,7 +376,7 @@ describe('BasicAuthValidator', () => { describe('Default webhook instance', () => { it('should auto-configure basic auth when env vars are set', async () => { - const webhook = await getDefaultWebhookWithEnv({ + const webhook = getDefaultWebhookWithEnv({ CHARGEBEE_WEBHOOK_USERNAME: 'envuser', CHARGEBEE_WEBHOOK_PASSWORD: 'envpass', }); @@ -377,7 +400,7 @@ describe('Default webhook instance', () => { }); it('should not configure auth when env vars are missing', async () => { - const webhook = await getDefaultWebhookWithEnv({ + const webhook = getDefaultWebhookWithEnv({ CHARGEBEE_WEBHOOK_USERNAME: undefined, CHARGEBEE_WEBHOOK_PASSWORD: undefined, }); @@ -386,7 +409,7 @@ describe('Default webhook instance', () => { }); it('should not configure auth when only username is set', async () => { - const webhook = await getDefaultWebhookWithEnv({ + const webhook = getDefaultWebhookWithEnv({ CHARGEBEE_WEBHOOK_USERNAME: 'envuser', CHARGEBEE_WEBHOOK_PASSWORD: undefined, }); @@ -395,7 +418,7 @@ describe('Default webhook instance', () => { }); it('should not configure auth when only password is set', async () => { - const webhook = await getDefaultWebhookWithEnv({ + const webhook = getDefaultWebhookWithEnv({ CHARGEBEE_WEBHOOK_USERNAME: undefined, CHARGEBEE_WEBHOOK_PASSWORD: 'envpass', }); @@ -404,7 +427,7 @@ describe('Default webhook instance', () => { }); it('should work end-to-end with env-configured auth', async () => { - const webhook = await getDefaultWebhookWithEnv({ + const webhook = getDefaultWebhookWithEnv({ CHARGEBEE_WEBHOOK_USERNAME: 'testuser', CHARGEBEE_WEBHOOK_PASSWORD: 'testpass', }); From a94576f1fad278178674a7f2fcd3178ae7dfcb3e Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Wed, 4 Feb 2026 12:41:00 +0530 Subject: [PATCH 8/9] show warning when no-auth is setup --- src/resources/webhook/handler.ts | 20 ++++++- test/webhook.test.ts | 100 +++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/src/resources/webhook/handler.ts b/src/resources/webhook/handler.ts index aec45df..dab2bc4 100644 --- a/src/resources/webhook/handler.ts +++ b/src/resources/webhook/handler.ts @@ -71,6 +71,7 @@ export class WebhookHandler< ResT = unknown, > extends EventEmitter> { private _requestValidator?: RequestValidator; + private _noAuthWarningShown = false; constructor(options?: WebhookHandlerOptions) { super({ captureRejections: true }); @@ -101,8 +102,23 @@ export class WebhookHandler< async handle(options: HandleOptions): Promise { const { body, headers, request, response } = options; try { - if (this._requestValidator && headers) { - await this._requestValidator(headers); + if (this._requestValidator) { + if (!headers) { + console.warn( + '[chargebee] Warning: Request validator is configured but no headers were passed. ' + + 'Authentication check skipped. If this is intentional (no-auth webhook), ' + + 'you can remove the requestValidator or ignore this warning.', + ); + } else { + await this._requestValidator(headers); + } + } else if (!this._noAuthWarningShown) { + this._noAuthWarningShown = true; + console.warn( + '[chargebee] Warning: No webhook authentication configured. ' + + 'Consider using basicAuthValidator() or a custom requestValidator for production. ' + + 'See: https://apidocs.chargebee.com/docs/api/webhooks', + ); } const event: WebhookEvent = diff --git a/test/webhook.test.ts b/test/webhook.test.ts index 023bf3c..05a8615 100644 --- a/test/webhook.test.ts +++ b/test/webhook.test.ts @@ -727,3 +727,103 @@ describe('chargebee.webhooks.createHandler()', () => { expect(handler2Called).to.be.true; }); }); + +describe('Webhook Auth Warnings', () => { + let originalWarn: typeof console.warn; + let warnCalls: string[]; + + beforeEach(() => { + originalWarn = console.warn; + warnCalls = []; + console.warn = (...args: any[]) => { + warnCalls.push(args.join(' ')); + }; + }); + + afterEach(() => { + console.warn = originalWarn; + }); + + it('should warn when validator is configured but headers are not passed', async () => { + const handler = new WebhookHandler(); + handler.requestValidator = () => {}; // Configure a validator + + handler.on('customer_created', async () => {}); + + // Call handle WITHOUT headers + await handler.handle({ body: makeEventBody('customer_created') }); + + expect(warnCalls.length).to.equal(1); + expect(warnCalls[0]).to.include( + 'Request validator is configured but no headers were passed', + ); + }); + + it('should not warn when validator is configured and headers are passed', async () => { + const handler = new WebhookHandler(); + handler.requestValidator = () => {}; // Configure a validator + + handler.on('customer_created', async () => {}); + + // Call handle WITH headers + await handler.handle({ + body: makeEventBody('customer_created'), + headers: { authorization: 'Basic xyz' }, + }); + + // Should not warn about missing headers + const headerWarning = warnCalls.find((msg) => + msg.includes('Request validator is configured but no headers were passed'), + ); + expect(headerWarning).to.be.undefined; + }); + + it('should warn once when no auth is configured', async () => { + const handler = new WebhookHandler(); // No validator configured + + handler.on('customer_created', async () => {}); + + // First call - should warn + await handler.handle({ body: makeEventBody('customer_created') }); + expect(warnCalls.length).to.equal(1); + expect(warnCalls[0]).to.include('No webhook authentication configured'); + + // Second call - should NOT warn again + await handler.handle({ body: makeEventBody('customer_created') }); + expect(warnCalls.length).to.equal(1); // Still only once + }); + + it('should not warn about no auth when validator is configured', async () => { + const handler = new WebhookHandler(); + handler.requestValidator = () => {}; // Configure validator immediately + + handler.on('customer_created', async () => {}); + + await handler.handle({ + body: makeEventBody('customer_created'), + headers: {}, + }); + + // Should not warn about "no auth configured" + const noAuthWarning = warnCalls.find((msg) => + msg.includes('No webhook authentication configured'), + ); + expect(noAuthWarning).to.be.undefined; + }); + + it('should still process event even when warnings are emitted', async () => { + const handler = new WebhookHandler(); + handler.requestValidator = () => {}; // Configure a validator + + let eventProcessed = false; + handler.on('customer_created', async () => { + eventProcessed = true; + }); + + // Call handle WITHOUT headers - warning should be emitted but event still processed + await handler.handle({ body: makeEventBody('customer_created') }); + + expect(warnCalls.length).to.be.greaterThan(0); + expect(eventProcessed).to.be.true; + }); +}); From f2f309f3e2de3987c5430aeb25b66ea682091ba4 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Wed, 4 Feb 2026 13:49:20 +0530 Subject: [PATCH 9/9] add event entry field validation --- src/resources/webhook/handler.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/resources/webhook/handler.ts b/src/resources/webhook/handler.ts index dab2bc4..7ea4477 100644 --- a/src/resources/webhook/handler.ts +++ b/src/resources/webhook/handler.ts @@ -124,6 +124,19 @@ export class WebhookHandler< const event: WebhookEvent = typeof body === 'string' ? JSON.parse(body) : (body as WebhookEvent); + // Validate required fields + if (!event || typeof event !== 'object' || Array.isArray(event)) { + throw new Error('Invalid webhook payload: body must be a JSON object'); + } + if (!event.event_type || typeof event.event_type !== 'string') { + throw new Error( + 'Invalid webhook payload: missing or invalid event_type', + ); + } + if (!event.id) { + throw new Error('Invalid webhook payload: missing event id'); + } + const context: WebhookContext = { event, request,