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..a176bf9 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,202 @@ 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 instance `webhooks` handler + +The simplest way to handle webhooks is using the `webhooks` property on your initialized Chargebee client: + +```typescript +import express from 'express'; +import Chargebee from 'chargebee'; + +const chargebee = new Chargebee({ + site: '{{site}}', + apiKey: '{{api-key}}', +}); + +const app = express(); +app.use(express.json()); + +// ⚠️ Register listeners once at startup, not inside request handlers +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'); +}); + +chargebee.webhooks.on('error', (err: Error) => { + console.error('Webhook error:', err.message); +}); + +app.post('/chargebee/webhooks', async (req, res) => { + await chargebee.webhooks.handle({ + body: req.body, + headers: req.headers, + request: req, + response: res, + }); +}); + +app.listen(8080); +``` + +**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. + +#### Creating typed webhook handlers + +For more control or multiple webhook endpoints, use `chargebee.webhooks.createHandler()`: + +```typescript +import express, { Request, Response } from 'express'; +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 = chargebee.webhooks.createHandler(); + +// Optional: Add request validator (e.g., Basic Auth) +handler.requestValidator = basicAuthValidator((username, password) => { + return username === 'admin' && password === 'secret'; +}); + +// ⚠️ Register event listeners once at startup, not inside request handlers +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, 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'); +}); + +app.post('/chargebee/webhooks', async (req, res) => { + await handler.handle({ + body: req.body, + headers: req.headers, + request: req, + response: res, + }); +}); + +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); +``` + +#### Responding to Webhooks + +> ⚠️ **Important:** Always send an HTTP response from your webhook handlers. If you don't respond with a 2xx status, Chargebee will retry the webhook, potentially causing duplicate processing. + +**Respond with 200** to acknowledge receipt: + +```typescript +handler.on('subscription_created', async ({ event, response }) => { + await provisionAccess(event.content.subscription); + response?.status(200).json({ received: true }); +}); +``` + +**Respond with 5xx** so Chargebee retries on failure: + +```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): + +```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'); +}); +``` + +#### Handling Unhandled Events and Errors + +```typescript +// Handle events without registered listeners +handler.on('unhandled_event', async ({ event, response }) => { + console.log(`Unhandled: ${event.event_type}`); + response?.status(200).send('OK'); +}); + +// Catch processing errors (invalid JSON, validator failure, etc.) +handler.on('error', (err) => { + console.error('Webhook error:', err.message); +}); +``` + ### 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. @@ -227,35 +423,15 @@ To improve type safety and gain better autocompletion when working with webhooks import Chargebee, { WebhookEventType, WebhookEvent } from "chargebee"; const result = await chargebeeInstance.event.retrieve("{event-id}"); -const subscriptionActivatedEvent: WebhookEvent = result.event; +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); - } -} -``` - #### 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 `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 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..6de7d9b 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", @@ -32,8 +33,6 @@ "url": "http://github.com/chargebee/chargebee-node/blob/master/LICENSE" } ], - "dependencies": { - }, "exports": { "types": "./types/index.d.ts", "browser": { @@ -62,13 +61,19 @@ } }, "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, "singleQuote": true, "parser": "typescript" } -} \ No newline at end of file +} diff --git a/src/chargebee.cjs.ts b/src/chargebee.cjs.ts index df07a5f..f1d4ad2 100644 --- a/src/chargebee.cjs.ts +++ b/src/chargebee.cjs.ts @@ -3,7 +3,8 @@ import { FetchHttpClient } from './net/FetchClient.js'; import { WebhookEventType, WebhookContentType, -} from './resources/webhook/eventType.js'; +} from './resources/webhook/handler.js'; +import { basicAuthValidator } from './resources/webhook/auth.js'; const httpClient = new FetchHttpClient(); const Chargebee = CreateChargebee(httpClient); @@ -11,6 +12,17 @@ module.exports = Chargebee; module.exports.Chargebee = Chargebee; module.exports.default = Chargebee; -// Export webhook event types +// Export webhook utilities module.exports.WebhookEventType = WebhookEventType; module.exports.WebhookContentType = WebhookContentType; +module.exports.basicAuthValidator = basicAuthValidator; + +// Export webhook types +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 680b2b5..f4e1572 100644 --- a/src/chargebee.esm.ts +++ b/src/chargebee.esm.ts @@ -6,8 +6,19 @@ const Chargebee = CreateChargebee(httpClient); export default Chargebee; -// Export webhook event types +// Export webhook utilities export { WebhookEventType, WebhookContentType, -} from './resources/webhook/eventType.js'; +} from './resources/webhook/handler.js'; +export { basicAuthValidator } from './resources/webhook/auth.js'; + +// Export webhook types +export type { + WebhookEvent, + WebhookContext, + WebhookHandlerOptions, + HandleOptions, + RequestValidator, +} from './resources/webhook/handler.js'; +export type { CredentialValidator } from './resources/webhook/auth.js'; diff --git a/src/createChargebee.ts b/src/createChargebee.ts index 1f4e907..5db138a 100644 --- a/src/createChargebee.ts +++ b/src/createChargebee.ts @@ -11,6 +11,11 @@ import { EndpointTuple, HttpClientInterface, } from './types.js'; +import { + WebhookHandler, + type WebhookHandlerOptions, + createDefaultHandler, +} from './resources/webhook/handler.js'; export const CreateChargebee = (httpClient: HttpClientInterface) => { const Chargebee = function (this: ChargebeeType, conf: Config) { @@ -21,6 +26,18 @@ export const CreateChargebee = (httpClient: HttpClientInterface) => { conf.httpClient != null ? conf.httpClient : httpClient; this._buildResources(); this._endpoints = Endpoints; + + // 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, { + createHandler( + options?: WebhookHandlerOptions, + ): WebhookHandler { + return new WebhookHandler(options); + }, + }); } as any as { new (): ChargebeeType }; Chargebee.prototype = { _createApiFunc(apiCall: ResourceType, env: EnvType) { 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..ea118a0 --- /dev/null +++ b/src/resources/webhook/content.ts @@ -0,0 +1,1879 @@ +/// + +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; +} + +import { WebhookEventType } from './eventType.js'; + +/** + * Maps webhook event types to their corresponding content types. + * Used for type-safe access to event.content based on event_type. + */ +export type WebhookContentMap = { + [WebhookEventType.AddUsagesReminder]: AddUsagesReminderContent; + + [WebhookEventType.AddonCreated]: AddonCreatedContent; + + [WebhookEventType.AddonDeleted]: AddonDeletedContent; + + [WebhookEventType.AddonUpdated]: AddonUpdatedContent; + + [WebhookEventType.AttachedItemCreated]: AttachedItemCreatedContent; + + [WebhookEventType.AttachedItemDeleted]: AttachedItemDeletedContent; + + [WebhookEventType.AttachedItemUpdated]: AttachedItemUpdatedContent; + + [WebhookEventType.AuthorizationSucceeded]: AuthorizationSucceededContent; + + [WebhookEventType.AuthorizationVoided]: AuthorizationVoidedContent; + + [WebhookEventType.BusinessEntityCreated]: BusinessEntityCreatedContent; + + [WebhookEventType.BusinessEntityDeleted]: BusinessEntityDeletedContent; + + [WebhookEventType.BusinessEntityUpdated]: BusinessEntityUpdatedContent; + + [WebhookEventType.CardAdded]: CardAddedContent; + + [WebhookEventType.CardDeleted]: CardDeletedContent; + + [WebhookEventType.CardExpired]: CardExpiredContent; + + [WebhookEventType.CardExpiryReminder]: CardExpiryReminderContent; + + [WebhookEventType.CardUpdated]: CardUpdatedContent; + + [WebhookEventType.ContractTermCancelled]: ContractTermCancelledContent; + + [WebhookEventType.ContractTermCompleted]: ContractTermCompletedContent; + + [WebhookEventType.ContractTermCreated]: ContractTermCreatedContent; + + [WebhookEventType.ContractTermRenewed]: ContractTermRenewedContent; + + [WebhookEventType.ContractTermTerminated]: ContractTermTerminatedContent; + + [WebhookEventType.CouponCodesAdded]: CouponCodesAddedContent; + + [WebhookEventType.CouponCodesDeleted]: CouponCodesDeletedContent; + + [WebhookEventType.CouponCodesUpdated]: CouponCodesUpdatedContent; + + [WebhookEventType.CouponCreated]: CouponCreatedContent; + + [WebhookEventType.CouponDeleted]: CouponDeletedContent; + + [WebhookEventType.CouponSetCreated]: CouponSetCreatedContent; + + [WebhookEventType.CouponSetDeleted]: CouponSetDeletedContent; + + [WebhookEventType.CouponSetUpdated]: CouponSetUpdatedContent; + + [WebhookEventType.CouponUpdated]: CouponUpdatedContent; + + [WebhookEventType.CreditNoteCreated]: CreditNoteCreatedContent; + + [WebhookEventType.CreditNoteCreatedWithBackdating]: CreditNoteCreatedWithBackdatingContent; + + [WebhookEventType.CreditNoteDeleted]: CreditNoteDeletedContent; + + [WebhookEventType.CreditNoteUpdated]: CreditNoteUpdatedContent; + + [WebhookEventType.CustomerBusinessEntityChanged]: CustomerBusinessEntityChangedContent; + + [WebhookEventType.CustomerChanged]: CustomerChangedContent; + + [WebhookEventType.CustomerCreated]: CustomerCreatedContent; + + [WebhookEventType.CustomerDeleted]: CustomerDeletedContent; + + [WebhookEventType.CustomerEntitlementsUpdated]: CustomerEntitlementsUpdatedContent; + + [WebhookEventType.CustomerMovedIn]: CustomerMovedInContent; + + [WebhookEventType.CustomerMovedOut]: CustomerMovedOutContent; + + [WebhookEventType.DifferentialPriceCreated]: DifferentialPriceCreatedContent; + + [WebhookEventType.DifferentialPriceDeleted]: DifferentialPriceDeletedContent; + + [WebhookEventType.DifferentialPriceUpdated]: DifferentialPriceUpdatedContent; + + [WebhookEventType.DunningUpdated]: DunningUpdatedContent; + + [WebhookEventType.EntitlementOverridesAutoRemoved]: EntitlementOverridesAutoRemovedContent; + + [WebhookEventType.EntitlementOverridesRemoved]: EntitlementOverridesRemovedContent; + + [WebhookEventType.EntitlementOverridesUpdated]: EntitlementOverridesUpdatedContent; + + [WebhookEventType.FeatureActivated]: FeatureActivatedContent; + + [WebhookEventType.FeatureArchived]: FeatureArchivedContent; + + [WebhookEventType.FeatureCreated]: FeatureCreatedContent; + + [WebhookEventType.FeatureDeleted]: FeatureDeletedContent; + + [WebhookEventType.FeatureReactivated]: FeatureReactivatedContent; + + [WebhookEventType.FeatureUpdated]: FeatureUpdatedContent; + + [WebhookEventType.GiftCancelled]: GiftCancelledContent; + + [WebhookEventType.GiftClaimed]: GiftClaimedContent; + + [WebhookEventType.GiftExpired]: GiftExpiredContent; + + [WebhookEventType.GiftScheduled]: GiftScheduledContent; + + [WebhookEventType.GiftUnclaimed]: GiftUnclaimedContent; + + [WebhookEventType.GiftUpdated]: GiftUpdatedContent; + + [WebhookEventType.HierarchyCreated]: HierarchyCreatedContent; + + [WebhookEventType.HierarchyDeleted]: HierarchyDeletedContent; + + [WebhookEventType.InvoiceDeleted]: InvoiceDeletedContent; + + [WebhookEventType.InvoiceGenerated]: InvoiceGeneratedContent; + + [WebhookEventType.InvoiceGeneratedWithBackdating]: InvoiceGeneratedWithBackdatingContent; + + [WebhookEventType.InvoiceUpdated]: InvoiceUpdatedContent; + + [WebhookEventType.ItemCreated]: ItemCreatedContent; + + [WebhookEventType.ItemDeleted]: ItemDeletedContent; + + [WebhookEventType.ItemEntitlementsRemoved]: ItemEntitlementsRemovedContent; + + [WebhookEventType.ItemEntitlementsUpdated]: ItemEntitlementsUpdatedContent; + + [WebhookEventType.ItemFamilyCreated]: ItemFamilyCreatedContent; + + [WebhookEventType.ItemFamilyDeleted]: ItemFamilyDeletedContent; + + [WebhookEventType.ItemFamilyUpdated]: ItemFamilyUpdatedContent; + + [WebhookEventType.ItemPriceCreated]: ItemPriceCreatedContent; + + [WebhookEventType.ItemPriceDeleted]: ItemPriceDeletedContent; + + [WebhookEventType.ItemPriceEntitlementsRemoved]: ItemPriceEntitlementsRemovedContent; + + [WebhookEventType.ItemPriceEntitlementsUpdated]: ItemPriceEntitlementsUpdatedContent; + + [WebhookEventType.ItemPriceUpdated]: ItemPriceUpdatedContent; + + [WebhookEventType.ItemUpdated]: ItemUpdatedContent; + + [WebhookEventType.MrrUpdated]: MrrUpdatedContent; + + [WebhookEventType.NetdPaymentDueReminder]: NetdPaymentDueReminderContent; + + [WebhookEventType.OmnichannelOneTimeOrderCreated]: OmnichannelOneTimeOrderCreatedContent; + + [WebhookEventType.OmnichannelOneTimeOrderItemCancelled]: OmnichannelOneTimeOrderItemCancelledContent; + + [WebhookEventType.OmnichannelSubscriptionCreated]: OmnichannelSubscriptionCreatedContent; + + [WebhookEventType.OmnichannelSubscriptionImported]: OmnichannelSubscriptionImportedContent; + + [WebhookEventType.OmnichannelSubscriptionItemCancellationScheduled]: OmnichannelSubscriptionItemCancellationScheduledContent; + + [WebhookEventType.OmnichannelSubscriptionItemCancelled]: OmnichannelSubscriptionItemCancelledContent; + + [WebhookEventType.OmnichannelSubscriptionItemChangeScheduled]: OmnichannelSubscriptionItemChangeScheduledContent; + + [WebhookEventType.OmnichannelSubscriptionItemChanged]: OmnichannelSubscriptionItemChangedContent; + + [WebhookEventType.OmnichannelSubscriptionItemDowngradeScheduled]: OmnichannelSubscriptionItemDowngradeScheduledContent; + + [WebhookEventType.OmnichannelSubscriptionItemDowngraded]: OmnichannelSubscriptionItemDowngradedContent; + + [WebhookEventType.OmnichannelSubscriptionItemDunningExpired]: OmnichannelSubscriptionItemDunningExpiredContent; + + [WebhookEventType.OmnichannelSubscriptionItemDunningStarted]: OmnichannelSubscriptionItemDunningStartedContent; + + [WebhookEventType.OmnichannelSubscriptionItemExpired]: OmnichannelSubscriptionItemExpiredContent; + + [WebhookEventType.OmnichannelSubscriptionItemGracePeriodExpired]: OmnichannelSubscriptionItemGracePeriodExpiredContent; + + [WebhookEventType.OmnichannelSubscriptionItemGracePeriodStarted]: OmnichannelSubscriptionItemGracePeriodStartedContent; + + [WebhookEventType.OmnichannelSubscriptionItemPauseScheduled]: OmnichannelSubscriptionItemPauseScheduledContent; + + [WebhookEventType.OmnichannelSubscriptionItemPaused]: OmnichannelSubscriptionItemPausedContent; + + [WebhookEventType.OmnichannelSubscriptionItemReactivated]: OmnichannelSubscriptionItemReactivatedContent; + + [WebhookEventType.OmnichannelSubscriptionItemRenewed]: OmnichannelSubscriptionItemRenewedContent; + + [WebhookEventType.OmnichannelSubscriptionItemResubscribed]: OmnichannelSubscriptionItemResubscribedContent; + + [WebhookEventType.OmnichannelSubscriptionItemResumed]: OmnichannelSubscriptionItemResumedContent; + + [WebhookEventType.OmnichannelSubscriptionItemScheduledCancellationRemoved]: OmnichannelSubscriptionItemScheduledCancellationRemovedContent; + + [WebhookEventType.OmnichannelSubscriptionItemScheduledChangeRemoved]: OmnichannelSubscriptionItemScheduledChangeRemovedContent; + + [WebhookEventType.OmnichannelSubscriptionItemScheduledDowngradeRemoved]: OmnichannelSubscriptionItemScheduledDowngradeRemovedContent; + + [WebhookEventType.OmnichannelSubscriptionItemUpgraded]: OmnichannelSubscriptionItemUpgradedContent; + + [WebhookEventType.OmnichannelSubscriptionMovedIn]: OmnichannelSubscriptionMovedInContent; + + [WebhookEventType.OmnichannelTransactionCreated]: OmnichannelTransactionCreatedContent; + + [WebhookEventType.OrderCancelled]: OrderCancelledContent; + + [WebhookEventType.OrderCreated]: OrderCreatedContent; + + [WebhookEventType.OrderDeleted]: OrderDeletedContent; + + [WebhookEventType.OrderDelivered]: OrderDeliveredContent; + + [WebhookEventType.OrderReadyToProcess]: OrderReadyToProcessContent; + + [WebhookEventType.OrderReadyToShip]: OrderReadyToShipContent; + + [WebhookEventType.OrderResent]: OrderResentContent; + + [WebhookEventType.OrderReturned]: OrderReturnedContent; + + [WebhookEventType.OrderUpdated]: OrderUpdatedContent; + + [WebhookEventType.PaymentFailed]: PaymentFailedContent; + + [WebhookEventType.PaymentInitiated]: PaymentInitiatedContent; + + [WebhookEventType.PaymentIntentCreated]: PaymentIntentCreatedContent; + + [WebhookEventType.PaymentIntentUpdated]: PaymentIntentUpdatedContent; + + [WebhookEventType.PaymentRefunded]: PaymentRefundedContent; + + [WebhookEventType.PaymentScheduleSchemeCreated]: PaymentScheduleSchemeCreatedContent; + + [WebhookEventType.PaymentScheduleSchemeDeleted]: PaymentScheduleSchemeDeletedContent; + + [WebhookEventType.PaymentSchedulesCreated]: PaymentSchedulesCreatedContent; + + [WebhookEventType.PaymentSchedulesUpdated]: PaymentSchedulesUpdatedContent; + + [WebhookEventType.PaymentSourceAdded]: PaymentSourceAddedContent; + + [WebhookEventType.PaymentSourceDeleted]: PaymentSourceDeletedContent; + + [WebhookEventType.PaymentSourceExpired]: PaymentSourceExpiredContent; + + [WebhookEventType.PaymentSourceExpiring]: PaymentSourceExpiringContent; + + [WebhookEventType.PaymentSourceLocallyDeleted]: PaymentSourceLocallyDeletedContent; + + [WebhookEventType.PaymentSourceUpdated]: PaymentSourceUpdatedContent; + + [WebhookEventType.PaymentSucceeded]: PaymentSucceededContent; + + [WebhookEventType.PendingInvoiceCreated]: PendingInvoiceCreatedContent; + + [WebhookEventType.PendingInvoiceUpdated]: PendingInvoiceUpdatedContent; + + [WebhookEventType.PlanCreated]: PlanCreatedContent; + + [WebhookEventType.PlanDeleted]: PlanDeletedContent; + + [WebhookEventType.PlanUpdated]: PlanUpdatedContent; + + [WebhookEventType.PriceVariantCreated]: PriceVariantCreatedContent; + + [WebhookEventType.PriceVariantDeleted]: PriceVariantDeletedContent; + + [WebhookEventType.PriceVariantUpdated]: PriceVariantUpdatedContent; + + [WebhookEventType.ProductCreated]: ProductCreatedContent; + + [WebhookEventType.ProductDeleted]: ProductDeletedContent; + + [WebhookEventType.ProductUpdated]: ProductUpdatedContent; + + [WebhookEventType.PromotionalCreditsAdded]: PromotionalCreditsAddedContent; + + [WebhookEventType.PromotionalCreditsDeducted]: PromotionalCreditsDeductedContent; + + [WebhookEventType.PurchaseCreated]: PurchaseCreatedContent; + + [WebhookEventType.QuoteCreated]: QuoteCreatedContent; + + [WebhookEventType.QuoteDeleted]: QuoteDeletedContent; + + [WebhookEventType.QuoteUpdated]: QuoteUpdatedContent; + + [WebhookEventType.RecordPurchaseFailed]: RecordPurchaseFailedContent; + + [WebhookEventType.RefundInitiated]: RefundInitiatedContent; + + [WebhookEventType.RuleCreated]: RuleCreatedContent; + + [WebhookEventType.RuleDeleted]: RuleDeletedContent; + + [WebhookEventType.RuleUpdated]: RuleUpdatedContent; + + [WebhookEventType.SalesOrderCreated]: SalesOrderCreatedContent; + + [WebhookEventType.SalesOrderUpdated]: SalesOrderUpdatedContent; + + [WebhookEventType.SubscriptionActivated]: SubscriptionActivatedContent; + + [WebhookEventType.SubscriptionActivatedWithBackdating]: SubscriptionActivatedWithBackdatingContent; + + [WebhookEventType.SubscriptionAdvanceInvoiceScheduleAdded]: SubscriptionAdvanceInvoiceScheduleAddedContent; + + [WebhookEventType.SubscriptionAdvanceInvoiceScheduleRemoved]: SubscriptionAdvanceInvoiceScheduleRemovedContent; + + [WebhookEventType.SubscriptionAdvanceInvoiceScheduleUpdated]: SubscriptionAdvanceInvoiceScheduleUpdatedContent; + + [WebhookEventType.SubscriptionBusinessEntityChanged]: SubscriptionBusinessEntityChangedContent; + + [WebhookEventType.SubscriptionCanceledWithBackdating]: SubscriptionCanceledWithBackdatingContent; + + [WebhookEventType.SubscriptionCancellationReminder]: SubscriptionCancellationReminderContent; + + [WebhookEventType.SubscriptionCancellationScheduled]: SubscriptionCancellationScheduledContent; + + [WebhookEventType.SubscriptionCancelled]: SubscriptionCancelledContent; + + [WebhookEventType.SubscriptionChanged]: SubscriptionChangedContent; + + [WebhookEventType.SubscriptionChangedWithBackdating]: SubscriptionChangedWithBackdatingContent; + + [WebhookEventType.SubscriptionChangesScheduled]: SubscriptionChangesScheduledContent; + + [WebhookEventType.SubscriptionCreated]: SubscriptionCreatedContent; + + [WebhookEventType.SubscriptionCreatedWithBackdating]: SubscriptionCreatedWithBackdatingContent; + + [WebhookEventType.SubscriptionDeleted]: SubscriptionDeletedContent; + + [WebhookEventType.SubscriptionEntitlementsCreated]: SubscriptionEntitlementsCreatedContent; + + [WebhookEventType.SubscriptionEntitlementsUpdated]: SubscriptionEntitlementsUpdatedContent; + + [WebhookEventType.SubscriptionItemsRenewed]: SubscriptionItemsRenewedContent; + + [WebhookEventType.SubscriptionMovedIn]: SubscriptionMovedInContent; + + [WebhookEventType.SubscriptionMovedOut]: SubscriptionMovedOutContent; + + [WebhookEventType.SubscriptionMovementFailed]: SubscriptionMovementFailedContent; + + [WebhookEventType.SubscriptionPauseScheduled]: SubscriptionPauseScheduledContent; + + [WebhookEventType.SubscriptionPaused]: SubscriptionPausedContent; + + [WebhookEventType.SubscriptionRampApplied]: SubscriptionRampAppliedContent; + + [WebhookEventType.SubscriptionRampCreated]: SubscriptionRampCreatedContent; + + [WebhookEventType.SubscriptionRampDeleted]: SubscriptionRampDeletedContent; + + [WebhookEventType.SubscriptionRampDrafted]: SubscriptionRampDraftedContent; + + [WebhookEventType.SubscriptionRampUpdated]: SubscriptionRampUpdatedContent; + + [WebhookEventType.SubscriptionReactivated]: SubscriptionReactivatedContent; + + [WebhookEventType.SubscriptionReactivatedWithBackdating]: SubscriptionReactivatedWithBackdatingContent; + + [WebhookEventType.SubscriptionRenewalReminder]: SubscriptionRenewalReminderContent; + + [WebhookEventType.SubscriptionRenewed]: SubscriptionRenewedContent; + + [WebhookEventType.SubscriptionResumed]: SubscriptionResumedContent; + + [WebhookEventType.SubscriptionResumptionScheduled]: SubscriptionResumptionScheduledContent; + + [WebhookEventType.SubscriptionScheduledCancellationRemoved]: SubscriptionScheduledCancellationRemovedContent; + + [WebhookEventType.SubscriptionScheduledChangesRemoved]: SubscriptionScheduledChangesRemovedContent; + + [WebhookEventType.SubscriptionScheduledPauseRemoved]: SubscriptionScheduledPauseRemovedContent; + + [WebhookEventType.SubscriptionScheduledResumptionRemoved]: SubscriptionScheduledResumptionRemovedContent; + + [WebhookEventType.SubscriptionShippingAddressUpdated]: SubscriptionShippingAddressUpdatedContent; + + [WebhookEventType.SubscriptionStarted]: SubscriptionStartedContent; + + [WebhookEventType.SubscriptionTrialEndReminder]: SubscriptionTrialEndReminderContent; + + [WebhookEventType.SubscriptionTrialExtended]: SubscriptionTrialExtendedContent; + + [WebhookEventType.TaxWithheldDeleted]: TaxWithheldDeletedContent; + + [WebhookEventType.TaxWithheldRecorded]: TaxWithheldRecordedContent; + + [WebhookEventType.TaxWithheldRefunded]: TaxWithheldRefundedContent; + + [WebhookEventType.TokenConsumed]: TokenConsumedContent; + + [WebhookEventType.TokenCreated]: TokenCreatedContent; + + [WebhookEventType.TokenExpired]: TokenExpiredContent; + + [WebhookEventType.TransactionCreated]: TransactionCreatedContent; + + [WebhookEventType.TransactionDeleted]: TransactionDeletedContent; + + [WebhookEventType.TransactionUpdated]: TransactionUpdatedContent; + + [WebhookEventType.UnbilledChargesCreated]: UnbilledChargesCreatedContent; + + [WebhookEventType.UnbilledChargesDeleted]: UnbilledChargesDeletedContent; + + [WebhookEventType.UnbilledChargesInvoiced]: UnbilledChargesInvoicedContent; + + [WebhookEventType.UnbilledChargesVoided]: UnbilledChargesVoidedContent; + + [WebhookEventType.UsageFileIngested]: UsageFileIngestedContent; + + [WebhookEventType.VariantCreated]: VariantCreatedContent; + + [WebhookEventType.VariantDeleted]: VariantDeletedContent; + + [WebhookEventType.VariantUpdated]: VariantUpdatedContent; + + [WebhookEventType.VirtualBankAccountAdded]: VirtualBankAccountAddedContent; + + [WebhookEventType.VirtualBankAccountDeleted]: VirtualBankAccountDeletedContent; + + [WebhookEventType.VirtualBankAccountUpdated]: VirtualBankAccountUpdatedContent; + + [WebhookEventType.VoucherCreateFailed]: VoucherCreateFailedContent; + + [WebhookEventType.VoucherCreated]: VoucherCreatedContent; + + [WebhookEventType.VoucherExpired]: VoucherExpiredContent; +}; + +/** + * Utility type to get the content type for a specific webhook event type. + * @example + * type SubCreatedContent = ContentFor; + */ +export type ContentFor = WebhookContentMap[T]; + +/** + * Webhook event payload from Chargebee. + * + * @typeParam T - The specific event type. When provided, `content` is strongly typed. + * Defaults to `WebhookEventType` for backward compatibility (content becomes union of all types). + * + * @example + * // Backward compatible usage (content is union of all content types) + * const event: WebhookEvent = payload; + * + * // Type-safe usage with specific event type + * const event: WebhookEvent = payload; + * event.content.Subscription; // ✓ Typed as Subscription + */ +export interface WebhookEvent { + id: string; + occurred_at: number; + source: string; + user?: string; + webhook_status: string; + webhook_failure_reason?: string; + webhooks?: any[]; + event_type: T; + api_version: string; + content: ContentFor; +} diff --git a/src/resources/webhook/eventType.ts b/src/resources/webhook/eventType.ts index 72ddbe6..e5a6783 100644 --- a/src/resources/webhook/eventType.ts +++ b/src/resources/webhook/eventType.ts @@ -453,6 +453,16 @@ export enum WebhookEventType { } /** - * @deprecated Use WebhookEventType instead. + * @deprecated Renamed to `WebhookEventType` for clarity. Use `WebhookEventType` instead. + * This alias will be removed in the next major version. + * + * @example + * // Before (deprecated) + * import { WebhookContentType } from 'chargebee'; + * if (event.event_type === WebhookContentType.SubscriptionCreated) { ... } + * + * // After (recommended) + * import { WebhookEventType } from 'chargebee'; + * if (event.event_type === WebhookEventType.SubscriptionCreated) { ... } */ export const WebhookContentType = WebhookEventType; diff --git a/src/resources/webhook/handler.ts b/src/resources/webhook/handler.ts new file mode 100644 index 0000000..be23cf5 --- /dev/null +++ b/src/resources/webhook/handler.ts @@ -0,0 +1,618 @@ +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. + * + * @typeParam ReqT - Framework-specific request type (e.g., `express.Request`) + * @typeParam ResT - Framework-specific response type (e.g., `express.Response`) + * + * @example + * ```typescript + * handler.on('subscription_created', ({ event, request, response }) => { + * console.log('Event ID:', event.id); + * console.log('Event Type:', event.event_type); + * response?.status(200).send('OK'); + * }); + * ``` + */ +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; +} + +/** + * Event map defining all available webhook events and their payload types. + * + * Includes: + * - All Chargebee event types (e.g., `subscription_created`, `customer_changed`) + * - `unhandled_event` - Emitted when an event has no registered listener + * - `error` - Emitted when an error occurs during webhook processing + * + * @typeParam ReqT - Framework-specific request type + * @typeParam ResT - Framework-specific response type + */ +export interface WebhookEventMap + extends Record]> { + unhandled_event: [WebhookContext]; + error: [Error]; +} + +/** + * Type for webhook event listener functions. + * + * @typeParam ReqT - Framework-specific request type + * @typeParam ResT - Framework-specific response type + * @typeParam K - The specific event type key from WebhookEventMap + */ +export type WebhookEventListener< + ReqT, + ResT, + K extends keyof WebhookEventMap, +> = (...args: WebhookEventMap[K]) => Promise | void; + +/** + * Validator function type for authenticating webhook requests. + * + * Receives HTTP headers and should throw an error if authentication fails. + * Can be synchronous or asynchronous (e.g., for database lookups). + * + * @param headers - HTTP headers from the incoming request + * @throws Error if authentication fails + * + * @example + * ```typescript + * // Custom validator example + * const customValidator: RequestValidator = async (headers) => { + * const apiKey = headers['x-api-key']; + * if (apiKey !== process.env.WEBHOOK_API_KEY) { + * throw new Error('Invalid API key'); + * } + * }; + * ``` + */ +export type RequestValidator = ( + headers: Record, +) => void | Promise; + +/** + * Configuration options for creating a WebhookHandler instance. + */ +export interface WebhookHandlerOptions { + /** + * Optional validator function to authenticate incoming webhook requests. + * + * Common use cases: + * - Basic Auth validation using `basicAuthValidator()` + * - Custom header validation (API keys, signatures) + * - Async validation against a database + * + * If not provided, a warning will be logged on first webhook handling. + * Chargebee supports no-auth webhooks, so this is optional but recommended. + * + * @example + * ```typescript + * // Using built-in Basic Auth validator + * const handler = createHandler({ + * requestValidator: basicAuthValidator((u, p) => u === 'user' && p === 'pass'), + * }); + * ``` + */ + requestValidator?: RequestValidator; +} + +/** + * Options for the {@link WebhookHandler.handle} method. + * + * @typeParam ReqT - Framework-specific request type (e.g., `express.Request`) + * @typeParam ResT - Framework-specific response type (e.g., `express.Response`) + */ +export interface HandleOptions { + /** + * The raw request body as a string or pre-parsed JSON object. + * If a string, it will be parsed as JSON. + */ + body: string | object; + /** + * HTTP headers from the incoming request. + * Required if a `requestValidator` is configured; otherwise authentication is skipped. + */ + headers?: Record; + /** + * Framework-specific request object passed through to event handlers. + * Useful for accessing additional request properties in handlers. + */ + request?: ReqT; + /** + * Framework-specific response object passed through to event handlers. + * Handlers should use this to send HTTP responses back to Chargebee. + * + * @remarks + * **Important:** Always send a response (e.g., `response.status(200).send('OK')`) + * to prevent Chargebee from retrying the webhook. + */ + response?: ResT; +} + +/** + * Webhook handler for processing Chargebee webhook events. + * + * Extends Node.js `EventEmitter` to provide a familiar, event-driven API for + * handling webhooks. Supports type-safe event listeners with full TypeScript + * autocomplete for all Chargebee event types. + * + * @typeParam ReqT - Framework-specific request type (e.g., `express.Request`) + * @typeParam ResT - Framework-specific response type (e.g., `express.Response`) + * + * @remarks + * **Lifecycle Warning:** Event listeners persist for the lifetime of the handler + * instance. Register handlers once at application startup, not per-request. + * + * @example Basic Usage with Express + * ```typescript + * import express from 'express'; + * import { createHandler, basicAuthValidator } from 'chargebee/webhook'; + * + * const app = express(); + * app.use(express.json()); + * + * // Create handler with Basic Auth + * const webhookHandler = createHandler({ + * requestValidator: basicAuthValidator((u, p) => u === 'admin' && p === 'secret'), + * }); + * + * // Register event listeners ONCE at startup (not per-request!) + * webhookHandler.on('subscription_created', async ({ event, response }) => { + * console.log('New subscription:', event.content.subscription.id); + * response?.status(200).send('OK'); + * }); + * + * webhookHandler.on('error', (error) => { + * console.error('Webhook error:', error); + * }); + * + * // Route handler + * app.post('/webhooks', async (req, res) => { + * await webhookHandler.handle({ + * body: req.body, + * headers: req.headers, + * request: req, + * response: res, + * }); + * }); + * ``` + * + * @example Available Event Types + * ```typescript + * // Subscription events + * handler.on('subscription_created', ({ event }) => { ... }); + * handler.on('subscription_changed', ({ event }) => { ... }); + * handler.on('subscription_cancelled', ({ event }) => { ... }); + * + * // Customer events + * handler.on('customer_created', ({ event }) => { ... }); + * handler.on('customer_changed', ({ event }) => { ... }); + * + * // Payment events + * handler.on('payment_succeeded', ({ event }) => { ... }); + * handler.on('payment_failed', ({ event }) => { ... }); + * + * // Special events + * handler.on('unhandled_event', ({ event }) => { + * console.log('Unhandled event type:', event.event_type); + * }); + * + * handler.on('error', (error) => { + * console.error('Processing error:', error); + * }); + * ``` + * + * @example Async Handlers + * ```typescript + * // Handlers can be async - errors are captured and emitted to 'error' event + * handler.on('subscription_created', async ({ event, response }) => { + * await saveToDatabase(event.content.subscription); + * await sendWelcomeEmail(event.content.customer); + * response?.status(200).send('OK'); + * }); + * ``` + */ +export class WebhookHandler< + ReqT = unknown, + ResT = unknown, +> extends EventEmitter> { + private _requestValidator?: RequestValidator; + private _noAuthWarningShown = false; + + /** + * Creates a new WebhookHandler instance. + * + * @param options - Optional configuration options + * + * @example + * ```typescript + * // Without authentication (not recommended for production) + * const handler = new WebhookHandler(); + * + * // With Basic Auth + * const handler = new WebhookHandler({ + * requestValidator: basicAuthValidator((u, p) => u === 'user' && p === 'pass'), + * }); + * ``` + */ + constructor(options?: WebhookHandlerOptions) { + super({ captureRejections: true }); + this._requestValidator = options?.requestValidator; + } + + /** + * Gets the current request validator function. + * + * @returns The configured validator or `undefined` if no authentication is set + * + * @example + * ```typescript + * if (handler.requestValidator) { + * console.log('Authentication is configured'); + * } + * ``` + */ + get requestValidator(): RequestValidator | undefined { + return this._requestValidator; + } + + /** + * Sets or updates the request validator function. + * + * Use this to configure authentication after handler creation, + * or to change validators at runtime. + * + * @param validator - The validator function, or `undefined` to disable authentication + * + * @example + * ```typescript + * // Set up Basic Auth after creation + * handler.requestValidator = basicAuthValidator((u, p) => u === 'admin' && p === 'secret'); + * + * // Custom header validation + * handler.requestValidator = (headers) => { + * if (headers['x-webhook-secret'] !== process.env.WEBHOOK_SECRET) { + * throw new Error('Invalid webhook secret'); + * } + * }; + * + * // Disable authentication (not recommended) + * handler.requestValidator = undefined; + * ``` + */ + set requestValidator(validator: RequestValidator | undefined) { + this._requestValidator = validator; + } + + /** + * Registers an event listener for a specific webhook event type. + * + * This method is inherited from Node.js `EventEmitter` but is documented here + * for clarity on available Chargebee webhook events. + * + * @param eventName - The Chargebee event type to listen for (e.g., `'subscription_created'`) + * @param listener - Callback function invoked when the event occurs + * @returns This handler instance for method chaining + * + * @remarks + * **Memory Leak Warning:** Listeners persist for the handler's lifetime. + * Always register listeners once at application startup, never inside + * request handlers or loops. + * + * @example Available Events + * ```typescript + * // Chargebee business events + * handler.on('subscription_created', ({ event, response }) => { ... }); + * handler.on('subscription_changed', ({ event, response }) => { ... }); + * handler.on('subscription_cancelled', ({ event, response }) => { ... }); + * handler.on('customer_created', ({ event, response }) => { ... }); + * handler.on('payment_succeeded', ({ event, response }) => { ... }); + * handler.on('invoice_generated', ({ event, response }) => { ... }); + * // ... and many more - see WebhookEventType enum for full list + * + * // Special events + * handler.on('unhandled_event', ({ event }) => { + * // Called when no listener exists for the event type + * console.log('Unhandled:', event.event_type); + * }); + * + * handler.on('error', (error) => { + * // Called on validation errors, parse errors, or handler errors + * console.error('Webhook error:', error); + * }); + * ``` + * + * @example Correct Usage + * ```typescript + * // ✅ GOOD: Register once at startup + * const handler = createHandler(); + * handler.on('subscription_created', handleSubscription); + * handler.on('error', handleError); + * + * app.post('/webhooks', async (req, res) => { + * await handler.handle({ body: req.body, headers: req.headers, response: res }); + * }); + * ``` + * + * @example Incorrect Usage + * ```typescript + * // ❌ BAD: Don't register inside request handlers - causes memory leak! + * app.post('/webhooks', async (req, res) => { + * handler.on('subscription_created', async () => { ... }); // Memory leak! + * await handler.handle({ ... }); + * }); + * ``` + */ + // Note: on() is inherited from EventEmitter with proper typing via WebhookEventMap + + /** + * Handles an incoming webhook request from Chargebee. + * + * This method: + * 1. Validates the request using the configured `requestValidator` (if any) + * 2. Parses the request body (if it's a string) + * 3. Validates required fields (`event_type`, `id`) + * 4. Emits the appropriate event to registered listeners + * + * @param options - The webhook request options + * @returns A promise that resolves when the event has been emitted + * + * @throws Error if no `error` listener is registered and an error occurs + * + * @remarks + * **Async Behavior:** This method emits events but does not wait for async + * listeners to complete. Errors in async listeners are captured via + * `captureRejections` and emitted to the `error` event. + * + * **Response Handling:** The handler does NOT automatically send HTTP responses. + * Your event listeners must call `response.status(200).send('OK')` or similar. + * + * @example Express Integration + * ```typescript + * app.post('/webhooks', async (req, res) => { + * try { + * await handler.handle({ + * body: req.body, + * headers: req.headers, + * request: req, + * response: res, + * }); + * } catch (error) { + * // Only reached if no 'error' listener is registered + * res.status(500).send('Internal error'); + * } + * }); + * ``` + * + * @example Fastify Integration + * ```typescript + * fastify.post('/webhooks', async (request, reply) => { + * await handler.handle({ + * body: request.body, + * headers: request.headers, + * request, + * response: reply, + * }); + * }); + * ``` + * + * @example Raw Node.js HTTP + * ```typescript + * http.createServer(async (req, res) => { + * if (req.method === 'POST' && req.url === '/webhooks') { + * const body = await getBody(req); + * await handler.handle({ body, headers: req.headers, response: res }); + * } + * }); + * ``` + */ + async handle(options: HandleOptions): Promise { + const { body, headers, request, response } = options; + try { + 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://www.chargebee.com/docs/billing/2.0/site-configuration/webhook_settings#basic-authentication', + ); + } + + 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, + 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) { + const error = err instanceof Error ? err : new Error(String(err)); + if (this.listenerCount('error') === 0) { + console.warn( + '[chargebee] Webhook error with no handler:', + error.message, + ); + throw error; + } + this.emit('error', error); + } + } +} + +/** + * Creates a new WebhookHandler with custom configuration. + * + * This is the recommended factory function for creating webhook handlers. + * Use this when you need explicit control over authentication configuration. + * + * @typeParam ReqT - Framework-specific request type (e.g., `express.Request`) + * @typeParam ResT - Framework-specific response type (e.g., `express.Response`) + * + * @param options - Optional configuration for the handler + * @returns A new WebhookHandler instance + * + * @remarks + * For multi-route or multi-tenant scenarios, create separate handler instances + * to maintain isolation and avoid event listener conflicts. + * + * @example Basic Auth Configuration + * ```typescript + * import { createHandler, basicAuthValidator } from 'chargebee/webhook'; + * + * const handler = createHandler({ + * requestValidator: basicAuthValidator((username, password) => { + * return username === 'admin' && password === 'secret'; + * }), + * }); + * ``` + * + * @example Custom Authentication + * ```typescript + * const handler = createHandler({ + * requestValidator: async (headers) => { + * const signature = headers['x-chargebee-signature']; + * const isValid = await verifySignature(signature); + * if (!isValid) throw new Error('Invalid signature'); + * }, + * }); + * ``` + * + * @example Multi-Route Setup + * ```typescript + * // Separate handlers for different webhook endpoints + * const billingHandler = createHandler({ ... }); + * const notificationHandler = createHandler({ ... }); + * + * billingHandler.on('subscription_created', handleBillingEvent); + * notificationHandler.on('subscription_created', sendNotification); + * + * app.post('/webhooks/billing', (req, res) => billingHandler.handle({ ... })); + * app.post('/webhooks/notifications', (req, res) => notificationHandler.handle({ ... })); + * ``` + * + * @example Without Authentication (Development Only) + * ```typescript + * // ⚠️ Not recommended for production + * const handler = createHandler(); + * ``` + */ +export function createHandler( + options?: WebhookHandlerOptions, +): WebhookHandler { + return new WebhookHandler(options); +} + +/** + * Creates a WebhookHandler with auto-configured Basic Auth from environment variables. + * + * This is a convenience function that automatically configures Basic Auth + * when the following environment variables are set: + * - `CHARGEBEE_WEBHOOK_USERNAME` + * - `CHARGEBEE_WEBHOOK_PASSWORD` + * + * If these environment variables are not set, the handler is created without + * authentication (a warning will be logged on first webhook handling). + * + * @typeParam ReqT - Framework-specific request type (e.g., `express.Request`) + * @typeParam ResT - Framework-specific response type (e.g., `express.Response`) + * + * @returns A new WebhookHandler instance with optional auto-configured auth + * + * @remarks + * This function is used internally by `chargebee.webhooks` to provide a + * pre-configured handler on the Chargebee instance. + * + * @example Environment-Based Setup + * ```bash + * # .env file + * CHARGEBEE_WEBHOOK_USERNAME=webhook_user + * CHARGEBEE_WEBHOOK_PASSWORD=webhook_secret + * ``` + * + * ```typescript + * import { createDefaultHandler } from 'chargebee/webhook'; + * + * // Auth is automatically configured from env vars + * const handler = createDefaultHandler(); + * + * handler.on('subscription_created', ({ event, response }) => { + * console.log('Subscription:', event.content.subscription.id); + * response?.status(200).send('OK'); + * }); + * ``` + * + * @example Overriding Auto-Configured Auth + * ```typescript + * const handler = createDefaultHandler(); + * + * // Override with custom validator if needed + * handler.requestValidator = myCustomValidator; + * ``` + * + * @see {@link createHandler} for explicit configuration without environment variables + */ +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 new file mode 100644 index 0000000..da1f11c --- /dev/null +++ b/test/webhook.test.ts @@ -0,0 +1,830 @@ +import { expect } from 'chai'; +import { WebhookHandler, createDefaultHandler } 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) { + // 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', + apiKey: 'test-key', + }); + + // Restore original env + 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 create a default webhook handler with fresh env vars +function getDefaultWebhookWithEnv( + env: Record, +): WebhookHandler { + // 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; + } + + // Create handler with env-based auto-config + const webhook = createDefaultHandler(); + + // Restore original env + 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; +} + +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; + }); + + await handler.handle({ body: 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'); + }); + + await handler.handle({ body: makeEventBody('pending_invoice_created'), headers: {} }); + expect(onErrorCalled).to.be.true; + }); + + it('should handle sync callback error', async () => { + 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'); + }); + + await 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 }) => { + onUnhandledCalled = true; + expect(event.event_type).to.equal('non_existing_event'); + }); + + await handler.handle({ body: 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; + }); + + await handler.handle({ body: makeEventBody('pending_invoice_created') }); + expect(pendingInvoiceCalled).to.be.true; + expect(subscriptionCalled).to.be.false; + + pendingInvoiceCalled = false; + await handler.handle({ body: 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; + }); + + await handler.handle({ body: '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 + await handler.handle({ body: makeEventBody('pending_invoice_created'), headers: {} }); + expect(validatorCalled).to.be.true; + expect(onErrorCalled).to.be.true; + + // Success case + validatorCalled = false; + onErrorCalled = false; + await handler.handle({ + body: makeEventBody('pending_invoice_created'), + headers: { '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; + }); + + await handler.handle({ body: 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++; + }); + + await handler.handle({ body: makeEventBody('customer_created') }); + await handler.handle({ body: 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); + await handler.handle({ body: makeEventBody('customer_created') }); + expect(callCount).to.equal(1); + + handler.off('customer_created', listener); + await handler.handle({ body: 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'); + await handler.handle({ body: 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; + }); + + await handler.handle({ body: 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', async () => { + const auth = 'Basic ' + Buffer.from('testuser:testpass').toString('base64'); + // Should not throw + await validator({ authorization: auth }); + }); + + it('should reject invalid credentials', async () => { + const auth = 'Basic ' + Buffer.from('wrong:wrong').toString('base64'); + 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', 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', 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', async () => { + // Node's Buffer.from() decodes base64 leniently, so this tests the credentials format check + 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 () => { + 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({ + id: 'evt_test_auth', + event_type: 'pending_invoice_created', + content: {}, + }); + + await handler.handle({ body, headers: { 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 = 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'); + await webhook.requestValidator!({ authorization: validAuth }); + + // Invalid credentials should fail + const invalidAuth = + 'Basic ' + Buffer.from('wrong:wrong').toString('base64'); + 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 () => { + const webhook = 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 = 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 = 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 = 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 + await webhook.handle({ body, headers: { 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'); + await webhook.handle({ body, headers: { authorization: invalidAuth } }); + expect(callbackCalled).to.be.false; + 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; + }); +}); + +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; + }); +}); 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 diff --git a/types/index.d.ts b/types/index.d.ts index 1f4c38a..051e5fd 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -6,7 +6,6 @@ /// /// /// -/// /// /// /// @@ -249,5 +248,144 @@ declare module 'chargebee' { variant: Variant.VariantResource; virtualBankAccount: VirtualBankAccount.VirtualBankAccountResource; webhookEndpoint: WebhookEndpoint.WebhookEndpointResource; + + /** 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; + }; + } + + // Webhook Handler Types + export type WebhookEventName = EventTypeEnum | 'unhandled_event'; + export type WebhookEventTypeValue = `${WebhookEventType}`; + /** + * @deprecated Renamed to `WebhookEventTypeValue` for clarity. Use `WebhookEventTypeValue` instead. + * This alias will be removed in the next major version. + */ + 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; } + + /** + * 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; + /** Optional framework-specific request object (Express, Fastify, etc.) */ + request?: ReqT; + /** Optional framework-specific response object (Express, Fastify, etc.) */ + response?: ResT; + } + + 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 interface WebhookHandler { + 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(options: HandleOptions): 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; } diff --git a/types/resources/WebhookEvent.d.ts b/types/resources/WebhookEvent.d.ts index a5fe0b3..85b6a3a 100644 --- a/types/resources/WebhookEvent.d.ts +++ b/types/resources/WebhookEvent.d.ts @@ -220,9 +220,9 @@ declare module 'chargebee' { PlanCreated = 'plan_created', PlanUpdated = 'plan_updated', } - /** - * @deprecated Use WebhookEventType instead. + * @deprecated Renamed to `WebhookEventType` for clarity. Use `WebhookEventType` instead. + * This alias will be removed in the next major version. */ export import WebhookContentType = WebhookEventType;