From 9ac51011fbdd4e5dde3300643618c051de8c35aa Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Mon, 24 Nov 2025 14:32:29 +0530 Subject: [PATCH 01/10] event and listners for webhook --- .../handlebar/NameFormatHelpers.java | 7 + src/main/java/com/chargebee/sdk/Language.java | 1 + .../java/com/chargebee/sdk/node/NodeV3.java | 34 +++- .../sdk/node/webhook/WebhookGenerator.java | 152 ++++++++++++++++++ .../templates/node/webhook_auth.ts.hbs | 29 ++++ .../templates/node/webhook_content.ts.hbs | 27 ++++ .../templates/node/webhook_event_types.ts.hbs | 6 + .../templates/node/webhook_handler.ts.hbs | 86 ++++++++++ 8 files changed, 339 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/chargebee/sdk/node/webhook/WebhookGenerator.java create mode 100644 src/main/resources/templates/node/webhook_auth.ts.hbs create mode 100644 src/main/resources/templates/node/webhook_content.ts.hbs create mode 100644 src/main/resources/templates/node/webhook_event_types.ts.hbs create mode 100644 src/main/resources/templates/node/webhook_handler.ts.hbs diff --git a/src/main/java/com/chargebee/handlebar/NameFormatHelpers.java b/src/main/java/com/chargebee/handlebar/NameFormatHelpers.java index 42c3ba0..8e5b02e 100644 --- a/src/main/java/com/chargebee/handlebar/NameFormatHelpers.java +++ b/src/main/java/com/chargebee/handlebar/NameFormatHelpers.java @@ -146,5 +146,12 @@ public CharSequence apply(final Object value, final Options options) { } return result.toString(); } + }, + + CONSTANT_CASE { + @Override + public CharSequence apply(final Object value, final Options options) { + return value.toString().toUpperCase().replace("-", "_"); + } } } diff --git a/src/main/java/com/chargebee/sdk/Language.java b/src/main/java/com/chargebee/sdk/Language.java index 675e41d..8c7d2b7 100644 --- a/src/main/java/com/chargebee/sdk/Language.java +++ b/src/main/java/com/chargebee/sdk/Language.java @@ -56,6 +56,7 @@ private void initialise() throws IOException { handlebars.registerHelper("pascalCase", NameFormatHelpers.TO_PASCAL); handlebars.registerHelper( "operationNameToPascalCase", NameFormatHelpers.OPERATION_NAME_TO_PASCAL_CASE); + handlebars.registerHelper("constantCase", NameFormatHelpers.CONSTANT_CASE); handlebars.registerHelper( "snakeCaseToPascalCaseAndSingularize", diff --git a/src/main/java/com/chargebee/sdk/node/NodeV3.java b/src/main/java/com/chargebee/sdk/node/NodeV3.java index 3d973cf..9750e89 100644 --- a/src/main/java/com/chargebee/sdk/node/NodeV3.java +++ b/src/main/java/com/chargebee/sdk/node/NodeV3.java @@ -4,6 +4,7 @@ import com.chargebee.openapi.Spec; import com.chargebee.sdk.FileOp; import com.chargebee.sdk.Language; +import com.chargebee.sdk.node.webhook.WebhookGenerator; import com.github.jknack.handlebars.Template; import java.io.IOException; import java.util.*; @@ -19,19 +20,46 @@ protected List generateSDK(String outputDirectoryPath, Spec spec) throws .filter(resource -> !Arrays.stream(this.hiddenOverride).toList().contains(resource.id)) .sorted(Comparator.comparing(Resource::sortOrder)) .toList(); - return List.of(generateApiEndpointsFile(outputDirectoryPath, resources)); + List fileOps = new ArrayList<>(); + fileOps.add(generateApiEndpointsFile(outputDirectoryPath, resources)); + + // Generate webhook files (event types, content, handler) + { + Template eventTypesTemplate = getTemplateContent("webhookEventTypes"); + Template contentTemplate = getTemplateContent("webhookContent"); + Template handlerTemplate = getTemplateContent("webhookHandler"); + Template authTemplate = getTemplateContent("webhookAuth"); + fileOps.addAll( + WebhookGenerator.generate( + outputDirectoryPath, + spec, + eventTypesTemplate, + contentTemplate, + handlerTemplate, + authTemplate + ) + ); + } + + return fileOps; } @Override protected Map templatesDefinition() { - return Map.of("api_endpoints", "/templates/node/api_endpoints.ts.hbs"); + return Map.of( + "api_endpoints", "/templates/node/api_endpoints.ts.hbs", + "webhookEventTypes", "/templates/node/webhook_event_types.ts.hbs", + "webhookContent", "/templates/node/webhook_content.ts.hbs", + "webhookHandler", "/templates/node/webhook_handler.ts.hbs", + "webhookAuth", "/templates/node/webhook_auth.ts.hbs" + ); } private FileOp generateApiEndpointsFile(String resourcesDirectoryPath, List resources) throws IOException { List> resourcesMap = resources.stream().map(resource -> resource.templateParams(this)).toList(); - Map templateParams = Map.of("resources", resourcesMap); + Map templateParams = Map.of("resources", resourcesMap); Template resourceTemplate = getTemplateContent("api_endpoints"); return new FileOp.WriteString( resourcesDirectoryPath, "api_endpoints.ts", resourceTemplate.apply(templateParams)); diff --git a/src/main/java/com/chargebee/sdk/node/webhook/WebhookGenerator.java b/src/main/java/com/chargebee/sdk/node/webhook/WebhookGenerator.java new file mode 100644 index 0000000..3dc20d9 --- /dev/null +++ b/src/main/java/com/chargebee/sdk/node/webhook/WebhookGenerator.java @@ -0,0 +1,152 @@ +package com.chargebee.sdk.node.webhook; + +import com.chargebee.openapi.Attribute; +import com.chargebee.openapi.Resource; +import com.chargebee.openapi.Spec; +import com.chargebee.sdk.FileOp; +import com.github.jknack.handlebars.Template; +import java.io.IOException; +import java.util.*; + +public class WebhookGenerator { + + private static List getEventResourcesForAEvent(Resource eventResource) { + List resources = new ArrayList<>(); + if (eventResource != null) { + for (Attribute attribute : eventResource.attributes()) { + if (attribute.name.equals("content")) { + attribute + .attributes() + .forEach( + (innerAttribute -> { + String ref = innerAttribute.schema.get$ref(); + if (ref != null && ref.contains("/")) { + String schemaName = ref.substring(ref.lastIndexOf("/") + 1); + resources.add(schemaName); + } + })); + } + } + } + return resources; + } + + public static List generate( + String outputDirectoryPath, + Spec spec, + Template eventTypesTemplate, + Template contentTemplate, + Template handlerTemplate, + Template authTemplate) + throws IOException { + final String webhookDirectoryPath = "/webhook"; + List fileOps = new ArrayList<>(); + // Ensure webhook directory exists + fileOps.add(new FileOp.CreateDirectory(outputDirectoryPath, webhookDirectoryPath)); + + // Include deprecated webhook events (like PCV1) since customers may still receive them + var webhookInfo = spec.extractWebhookInfo(true); + var eventSchema = spec.resourcesForEvents(); + + if (webhookInfo.isEmpty()) { + return fileOps; + } + + List> events = new ArrayList<>(); + Set seenTypes = new HashSet<>(); + Set uniqueImports = new HashSet<>(); + + // Compute models directory by taking parent of webhook output dir + java.io.File webhookDir = new java.io.File(outputDirectoryPath + webhookDirectoryPath); + java.io.File chargebeeRoot = webhookDir.getParentFile(); + + for (Map info : webhookInfo) { + String type = info.get("type"); + if (seenTypes.contains(type)) { + continue; + } + seenTypes.add(type); + + String resourceSchemaName = info.get("resource_schema_name"); + Resource matchedSchema = + eventSchema.stream() + .filter(schema -> schema.name.equals(resourceSchemaName)) + .findFirst() + .orElse(null); + + List allSchemas = getEventResourcesForAEvent(matchedSchema); + List schemaImports = new ArrayList<>(); + + for(String schema : allSchemas) { + // In Node we import Resource classes/interfaces. + // Assuming 'Customer' -> 'Customer' in types + schemaImports.add(schema); + uniqueImports.add(schema); + } + + Map params = new HashMap<>(); + params.put("type", type); + params.put("resource_schemas", schemaImports); + events.add(params); + } + + events.sort(Comparator.comparing(e -> e.get("type").toString())); + + // event_types.ts + { + Map ctx = new HashMap<>(); + ctx.put("events", events); + fileOps.add( + new FileOp.WriteString( + outputDirectoryPath + webhookDirectoryPath, + "event_types.ts", + eventTypesTemplate.apply(ctx) + ) + ); + } + + // content.ts + { + Map ctx = new HashMap<>(); + ctx.put("events", events); + List importsList = new ArrayList<>(uniqueImports); + Collections.sort(importsList); + ctx.put("unique_imports", importsList); + + fileOps.add( + new FileOp.WriteString( + outputDirectoryPath + webhookDirectoryPath, + "content.ts", + contentTemplate.apply(ctx) + ) + ); + } + + // handler.ts + { + Map ctx = new HashMap<>(); + ctx.put("events", events); + fileOps.add( + new FileOp.WriteString( + outputDirectoryPath + webhookDirectoryPath, + "handler.ts", + handlerTemplate.apply(ctx) + ) + ); + } + + // auth.ts + { + fileOps.add( + new FileOp.WriteString( + outputDirectoryPath + webhookDirectoryPath, + "auth.ts", + authTemplate.apply("") + ) + ); + } + + return fileOps; + } +} + diff --git a/src/main/resources/templates/node/webhook_auth.ts.hbs b/src/main/resources/templates/node/webhook_auth.ts.hbs new file mode 100644 index 0000000..ce3d588 --- /dev/null +++ b/src/main/resources/templates/node/webhook_auth.ts.hbs @@ -0,0 +1,29 @@ +export const basicAuthValidator = (validateCredentials: (username: string, password: string) => boolean) => { + return (headers: Record) => { + const authHeader = headers['authorization'] || headers['Authorization']; + + if (!authHeader) { + throw new Error("Invalid 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"); + } + + const credentials = Buffer.from(parts[1], 'base64').toString().split(':'); + if (credentials.length !== 2) { + throw new Error("Invalid credentials"); + } + + if (!validateCredentials(credentials[0], credentials[1])) { + throw new Error("Invalid credentials"); + } + }; +}; + diff --git a/src/main/resources/templates/node/webhook_content.ts.hbs b/src/main/resources/templates/node/webhook_content.ts.hbs new file mode 100644 index 0000000..3f08cd8 --- /dev/null +++ b/src/main/resources/templates/node/webhook_content.ts.hbs @@ -0,0 +1,27 @@ +declare module 'chargebee' { + {{#each unique_imports}} + export interface {{this}} {} + {{/each}} +} + +{{#each events}} +export interface {{snakeCaseToPascalCase type}}Content { + {{#each resource_schemas}} + {{camelCase this}}: import('chargebee').{{this}}; + {{/each}} +} + +{{/each}} +export interface WebhookEvent { + id: string; + occurred_at: number; + source: string; + user?: string; + webhook_status: string; + webhook_failure_reason?: string; + webhooks?: any[]; + event_type: string; + api_version: string; + content: any; +} + diff --git a/src/main/resources/templates/node/webhook_event_types.ts.hbs b/src/main/resources/templates/node/webhook_event_types.ts.hbs new file mode 100644 index 0000000..bb72667 --- /dev/null +++ b/src/main/resources/templates/node/webhook_event_types.ts.hbs @@ -0,0 +1,6 @@ +export enum EventType { +{{#each events}} + {{constantCase type}} = '{{type}}', +{{/each}} +} + diff --git a/src/main/resources/templates/node/webhook_handler.ts.hbs b/src/main/resources/templates/node/webhook_handler.ts.hbs new file mode 100644 index 0000000..1bfe5fb --- /dev/null +++ b/src/main/resources/templates/node/webhook_handler.ts.hbs @@ -0,0 +1,86 @@ +import { EventType } from './event_types.js'; +import { + {{#each events}} + {{snakeCaseToPascalCase type}}Content, + {{/each}} + WebhookEvent +} from './content.js'; + +export interface WebhookHandlers { + {{#each events}} + on{{snakeCaseToPascalCase type}}?: (event: WebhookEvent & { content: {{snakeCaseToPascalCase type}}Content }) => Promise; + {{/each}} +} + +export class WebhookHandler { + private _handlers: WebhookHandlers = {}; + + /** + * Optional callback for unhandled events. + */ + onUnhandledEvent?: (event: WebhookEvent) => Promise; + + /** + * Optional callback for errors during processing. + */ + onError?: (error: any) => void; + + /** + * Optional validator for request headers. + */ + requestValidator?: (headers: Record) => void; + + constructor(handlers: WebhookHandlers = {}) { + this._handlers = handlers; + } + + {{#each events}} + set on{{snakeCaseToPascalCase type}}(handler: ((event: WebhookEvent & { content: {{snakeCaseToPascalCase type}}Content }) => Promise) | undefined) { + this._handlers.on{{snakeCaseToPascalCase type}} = handler; + } + + get on{{snakeCaseToPascalCase type}}() { + return this._handlers.on{{snakeCaseToPascalCase type}}; + } + + {{/each}} + + async handle(body: string | object, headers?: Record): Promise { + try { + if (this.requestValidator && headers) { + this.requestValidator(headers); + } + + let event: WebhookEvent; + if (typeof body === 'string') { + event = JSON.parse(body); + } else { + event = body as WebhookEvent; + } + + const eventType = event.event_type; + + switch (eventType) { + {{#each events}} + case EventType.{{constantCase type}}: + if (this._handlers.on{{snakeCaseToPascalCase type}}) { + await this._handlers.on{{snakeCaseToPascalCase type}}(event as WebhookEvent & { content: {{snakeCaseToPascalCase type}}Content }); + return; + } + break; + {{/each}} + } + + if (this.onUnhandledEvent) { + await this.onUnhandledEvent(event); + } + } catch (err) { + if (this.onError) { + this.onError(err); + } else { + throw err; + } + } + } +} + From e396c2e0ef682a8b53b965fea54f3b279bf8c5f9 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Mon, 1 Dec 2025 13:30:57 +0530 Subject: [PATCH 02/10] update type --- src/main/resources/templates/ts/chargebee.ts.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/templates/ts/chargebee.ts.hbs b/src/main/resources/templates/ts/chargebee.ts.hbs index b300913..b113f8c 100644 --- a/src/main/resources/templates/ts/chargebee.ts.hbs +++ b/src/main/resources/templates/ts/chargebee.ts.hbs @@ -18,7 +18,7 @@ export class ChargeBee { ChargeBee._env.timeout = timeout; } {{#each resources}} - get {{pascalCaseToSnakeCase name}}() { + get {{pascalCaseToSnakeCaseAndPluralize name}}() { return resources.{{name}}; }{{/each}} } From a4a3ba4519ae6a4c2b33f4f1af02379adacf2d51 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Mon, 1 Dec 2025 13:34:27 +0530 Subject: [PATCH 03/10] include pc1 events --- src/main/java/com/chargebee/openapi/Spec.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/chargebee/openapi/Spec.java b/src/main/java/com/chargebee/openapi/Spec.java index ecc0152..2bcbe82 100644 --- a/src/main/java/com/chargebee/openapi/Spec.java +++ b/src/main/java/com/chargebee/openapi/Spec.java @@ -146,6 +146,10 @@ private boolean isErrorSchema(String schemaName, Schema schema) { } public List> extractWebhookInfo() { + return extractWebhookInfo(false); + } + + public List> extractWebhookInfo(boolean includeDeprecated) { List> result = new ArrayList<>(); Map webhooks = openAPI.getWebhooks(); @@ -157,7 +161,7 @@ public List> extractWebhookInfo() { String resourceSchema = null; Operation postOp = pathItem.getPost(); - if (postOp != null && Boolean.TRUE.equals(postOp.getDeprecated())) { + if (!includeDeprecated && postOp != null && Boolean.TRUE.equals(postOp.getDeprecated())) { continue; } if (postOp != null && postOp.getRequestBody() != null) { @@ -168,7 +172,7 @@ public List> extractWebhookInfo() { MediaType mediaType = requestBody.getContent().get("application/json"); Schema schema = mediaType.getSchema(); - if (schema.getDeprecated() != null && schema.getDeprecated() == true) { + if (!includeDeprecated && schema.getDeprecated() != null && schema.getDeprecated() == true) { continue; } From b6810d92e2163539fa16d975ea0e0515118eaa2e Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Mon, 1 Dec 2025 13:51:34 +0530 Subject: [PATCH 04/10] node webhook hbs changes --- .../java/com/chargebee/sdk/node/NodeV3.java | 5 +- .../sdk/node/webhook/WebhookGenerator.java | 26 +--- .../templates/node/webhook_auth.ts.hbs | 17 +-- .../templates/node/webhook_content.ts.hbs | 37 +++--- .../templates/node/webhook_handler.ts.hbs | 111 ++++++++---------- 5 files changed, 77 insertions(+), 119 deletions(-) diff --git a/src/main/java/com/chargebee/sdk/node/NodeV3.java b/src/main/java/com/chargebee/sdk/node/NodeV3.java index 9750e89..84ea159 100644 --- a/src/main/java/com/chargebee/sdk/node/NodeV3.java +++ b/src/main/java/com/chargebee/sdk/node/NodeV3.java @@ -23,9 +23,8 @@ protected List generateSDK(String outputDirectoryPath, Spec spec) throws List fileOps = new ArrayList<>(); fileOps.add(generateApiEndpointsFile(outputDirectoryPath, resources)); - // Generate webhook files (event types, content, handler) + // Generate webhook files (content, handler, auth) { - Template eventTypesTemplate = getTemplateContent("webhookEventTypes"); Template contentTemplate = getTemplateContent("webhookContent"); Template handlerTemplate = getTemplateContent("webhookHandler"); Template authTemplate = getTemplateContent("webhookAuth"); @@ -33,7 +32,6 @@ protected List generateSDK(String outputDirectoryPath, Spec spec) throws WebhookGenerator.generate( outputDirectoryPath, spec, - eventTypesTemplate, contentTemplate, handlerTemplate, authTemplate @@ -48,7 +46,6 @@ protected List generateSDK(String outputDirectoryPath, Spec spec) throws protected Map templatesDefinition() { return Map.of( "api_endpoints", "/templates/node/api_endpoints.ts.hbs", - "webhookEventTypes", "/templates/node/webhook_event_types.ts.hbs", "webhookContent", "/templates/node/webhook_content.ts.hbs", "webhookHandler", "/templates/node/webhook_handler.ts.hbs", "webhookAuth", "/templates/node/webhook_auth.ts.hbs" diff --git a/src/main/java/com/chargebee/sdk/node/webhook/WebhookGenerator.java b/src/main/java/com/chargebee/sdk/node/webhook/WebhookGenerator.java index 3dc20d9..4b81701 100644 --- a/src/main/java/com/chargebee/sdk/node/webhook/WebhookGenerator.java +++ b/src/main/java/com/chargebee/sdk/node/webhook/WebhookGenerator.java @@ -34,7 +34,6 @@ private static List getEventResourcesForAEvent(Resource eventResource) { public static List generate( String outputDirectoryPath, Spec spec, - Template eventTypesTemplate, Template contentTemplate, Template handlerTemplate, Template authTemplate) @@ -56,10 +55,6 @@ public static List generate( Set seenTypes = new HashSet<>(); Set uniqueImports = new HashSet<>(); - // Compute models directory by taking parent of webhook output dir - java.io.File webhookDir = new java.io.File(outputDirectoryPath + webhookDirectoryPath); - java.io.File chargebeeRoot = webhookDir.getParentFile(); - for (Map info : webhookInfo) { String type = info.get("type"); if (seenTypes.contains(type)) { @@ -78,8 +73,6 @@ public static List generate( List schemaImports = new ArrayList<>(); for(String schema : allSchemas) { - // In Node we import Resource classes/interfaces. - // Assuming 'Customer' -> 'Customer' in types schemaImports.add(schema); uniqueImports.add(schema); } @@ -92,19 +85,6 @@ public static List generate( events.sort(Comparator.comparing(e -> e.get("type").toString())); - // event_types.ts - { - Map ctx = new HashMap<>(); - ctx.put("events", events); - fileOps.add( - new FileOp.WriteString( - outputDirectoryPath + webhookDirectoryPath, - "event_types.ts", - eventTypesTemplate.apply(ctx) - ) - ); - } - // content.ts { Map ctx = new HashMap<>(); @@ -122,15 +102,13 @@ public static List generate( ); } - // handler.ts + // handler.ts (static template) { - Map ctx = new HashMap<>(); - ctx.put("events", events); fileOps.add( new FileOp.WriteString( outputDirectoryPath + webhookDirectoryPath, "handler.ts", - handlerTemplate.apply(ctx) + handlerTemplate.apply("") ) ); } diff --git a/src/main/resources/templates/node/webhook_auth.ts.hbs b/src/main/resources/templates/node/webhook_auth.ts.hbs index ce3d588..23a207a 100644 --- a/src/main/resources/templates/node/webhook_auth.ts.hbs +++ b/src/main/resources/templates/node/webhook_auth.ts.hbs @@ -1,29 +1,30 @@ -export const basicAuthValidator = (validateCredentials: (username: string, password: string) => boolean) => { +export const basicAuthValidator = ( + validateCredentials: (username: string, password: string) => boolean, +) => { return (headers: Record) => { const authHeader = headers['authorization'] || headers['Authorization']; - + if (!authHeader) { - throw new Error("Invalid authorization header"); + throw new Error('Invalid authorization header'); } const authStr = Array.isArray(authHeader) ? authHeader[0] : authHeader; if (!authStr) { - throw new Error("Invalid authorization header"); + throw new Error('Invalid authorization header'); } const parts = authStr.split(' '); if (parts.length !== 2 || parts[0] !== 'Basic') { - throw new Error("Invalid authorization header"); + throw new Error('Invalid authorization header'); } const credentials = Buffer.from(parts[1], 'base64').toString().split(':'); if (credentials.length !== 2) { - throw new Error("Invalid credentials"); + throw new Error('Invalid credentials'); } if (!validateCredentials(credentials[0], credentials[1])) { - throw new Error("Invalid credentials"); + throw new Error('Invalid credentials'); } }; }; - diff --git a/src/main/resources/templates/node/webhook_content.ts.hbs b/src/main/resources/templates/node/webhook_content.ts.hbs index 3f08cd8..db08e33 100644 --- a/src/main/resources/templates/node/webhook_content.ts.hbs +++ b/src/main/resources/templates/node/webhook_content.ts.hbs @@ -1,27 +1,22 @@ -declare module 'chargebee' { - {{#each unique_imports}} - export interface {{this}} {} - {{/each}} -} - +/// {{#each events}} + export interface {{snakeCaseToPascalCase type}}Content { - {{#each resource_schemas}} - {{camelCase this}}: import('chargebee').{{this}}; - {{/each}} +{{#each resource_schemas}} + {{this}}: import('chargebee').{{this}}; +{{/each}} } - {{/each}} + export interface WebhookEvent { - id: string; - occurred_at: number; - source: string; - user?: string; - webhook_status: string; - webhook_failure_reason?: string; - webhooks?: any[]; - event_type: string; - api_version: string; - content: any; + id: string; + occurred_at: number; + source: string; + user?: string; + webhook_status: string; + webhook_failure_reason?: string; + webhooks?: any[]; + event_type: string; + api_version: string; + content: any; } - diff --git a/src/main/resources/templates/node/webhook_handler.ts.hbs b/src/main/resources/templates/node/webhook_handler.ts.hbs index 1bfe5fb..0d8499e 100644 --- a/src/main/resources/templates/node/webhook_handler.ts.hbs +++ b/src/main/resources/templates/node/webhook_handler.ts.hbs @@ -1,86 +1,73 @@ -import { EventType } from './event_types.js'; -import { - {{#each events}} - {{snakeCaseToPascalCase type}}Content, - {{/each}} - WebhookEvent -} from './content.js'; +import { EventEmitter } from 'node:events'; +import { WebhookEvent } from './content.js'; -export interface WebhookHandlers { - {{#each events}} - on{{snakeCaseToPascalCase type}}?: (event: WebhookEvent & { content: {{snakeCaseToPascalCase type}}Content }) => Promise; - {{/each}} -} +export type EventType = import('chargebee').EventTypeEnum; -export class WebhookHandler { - private _handlers: WebhookHandlers = {}; - - /** - * Optional callback for unhandled events. - */ - onUnhandledEvent?: (event: WebhookEvent) => Promise; +export interface WebhookEventMap extends Record { + unhandled_event: WebhookEvent; + error: unknown; +} - /** - * Optional callback for errors during processing. - */ - onError?: (error: any) => void; +export type WebhookEventListener = ( + event: WebhookEventMap[K], +) => Promise | void; - /** - * Optional validator for request headers. - */ - requestValidator?: (headers: Record) => void; +export class WebhookHandler extends EventEmitter { + requestValidator?: ( + headers: Record, + ) => void; - constructor(handlers: WebhookHandlers = {}) { - this._handlers = handlers; + on( + eventName: K, + listener: WebhookEventListener, + ): this { + return super.on(eventName, listener as (...args: unknown[]) => void); } - {{#each events}} - set on{{snakeCaseToPascalCase type}}(handler: ((event: WebhookEvent & { content: {{snakeCaseToPascalCase type}}Content }) => Promise) | undefined) { - this._handlers.on{{snakeCaseToPascalCase type}} = handler; + once( + eventName: K, + listener: WebhookEventListener, + ): this { + return super.once(eventName, listener as (...args: unknown[]) => void); } - - get on{{snakeCaseToPascalCase type}}() { - return this._handlers.on{{snakeCaseToPascalCase type}}; + + off( + eventName: K, + listener: WebhookEventListener, + ): this { + return super.off(eventName, listener as (...args: unknown[]) => void); } - {{/each}} + emit( + eventName: K, + event: WebhookEventMap[K], + ): boolean { + return super.emit(eventName, event); + } - async handle(body: string | object, headers?: Record): Promise { + handle( + body: string | object, + headers?: Record, + ): void { try { if (this.requestValidator && headers) { this.requestValidator(headers); } - let event: WebhookEvent; - if (typeof body === 'string') { - event = JSON.parse(body); - } else { - event = body as WebhookEvent; - } - - const eventType = event.event_type; + const event: WebhookEvent = + typeof body === 'string' ? JSON.parse(body) : (body as WebhookEvent); - switch (eventType) { - {{#each events}} - case EventType.{{constantCase type}}: - if (this._handlers.on{{snakeCaseToPascalCase type}}) { - await this._handlers.on{{snakeCaseToPascalCase type}}(event as WebhookEvent & { content: {{snakeCaseToPascalCase type}}Content }); - return; - } - break; - {{/each}} - } + const eventType = event.event_type as keyof WebhookEventMap; - if (this.onUnhandledEvent) { - await this.onUnhandledEvent(event); + if (this.listenerCount(eventType) > 0) { + this.emit(eventType, event); + } else if (this.listenerCount('unhandled_event') > 0) { + this.emit('unhandled_event', event); } } catch (err) { - if (this.onError) { - this.onError(err); - } else { - throw err; - } + this.emit('error', err); } } } +export type { WebhookEvent } from './content.js'; From 7419c12d28c3d40a1745fc009b1a0acc54ecb465 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Thu, 4 Dec 2025 17:03:14 +0530 Subject: [PATCH 05/10] add default webhook handler instance --- .../templates/node/webhook_handler.ts.hbs | 59 ++++++++----------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/src/main/resources/templates/node/webhook_handler.ts.hbs b/src/main/resources/templates/node/webhook_handler.ts.hbs index 0d8499e..175d09a 100644 --- a/src/main/resources/templates/node/webhook_handler.ts.hbs +++ b/src/main/resources/templates/node/webhook_handler.ts.hbs @@ -1,50 +1,27 @@ import { EventEmitter } from 'node:events'; import { WebhookEvent } from './content.js'; +import { basicAuthValidator } from './auth.js'; export type EventType = import('chargebee').EventTypeEnum; -export interface WebhookEventMap extends Record { - unhandled_event: WebhookEvent; - error: unknown; +export interface WebhookEventMap extends Record { + unhandled_event: [WebhookEvent]; + error: [Error]; } export type WebhookEventListener = ( - event: WebhookEventMap[K], + ...args: WebhookEventMap[K] ) => Promise | void; -export class WebhookHandler extends EventEmitter { +export class WebhookHandler extends EventEmitter { requestValidator?: ( headers: Record, ) => void; - on( - eventName: K, - listener: WebhookEventListener, - ): this { - return super.on(eventName, listener as (...args: unknown[]) => void); + constructor() { + super({ captureRejections: true }); } - - once( - eventName: K, - listener: WebhookEventListener, - ): this { - return super.once(eventName, listener as (...args: unknown[]) => void); - } - - off( - eventName: K, - listener: WebhookEventListener, - ): this { - return super.off(eventName, listener as (...args: unknown[]) => void); - } - - emit( - eventName: K, - event: WebhookEventMap[K], - ): boolean { - return super.emit(eventName, event); - } - + handle( body: string | object, headers?: Record, @@ -61,13 +38,27 @@ export class WebhookHandler extends EventEmitter { if (this.listenerCount(eventType) > 0) { this.emit(eventType, event); - } else if (this.listenerCount('unhandled_event') > 0) { + } else { this.emit('unhandled_event', event); } } catch (err) { - this.emit('error', err); + this.emit('error', err instanceof Error ? err : new Error(String(err))); } } } +const webhook = new WebhookHandler(); + +// Auto-configure basic auth if env vars are present +const username = process.env.CHARGEBEE_WEBHOOK_USERNAME; +const password = process.env.CHARGEBEE_WEBHOOK_PASSWORD; + +if (username && password) { + webhook.requestValidator = basicAuthValidator( + (u, p) => u === username && p === password, + ); +} + +export default webhook; + export type { WebhookEvent } from './content.js'; From 583508fe673cd9d061997e8d75cedeaf2d1156ea Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Tue, 9 Dec 2025 16:24:37 +0530 Subject: [PATCH 06/10] Update index.d.ts.hbs --- .../templates/ts/typings/v3/index.d.ts.hbs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/main/resources/templates/ts/typings/v3/index.d.ts.hbs b/src/main/resources/templates/ts/typings/v3/index.d.ts.hbs index dde5276..1d63895 100644 --- a/src/main/resources/templates/ts/typings/v3/index.d.ts.hbs +++ b/src/main/resources/templates/ts/typings/v3/index.d.ts.hbs @@ -83,4 +83,47 @@ declare module 'chargebee' { {{#each resources}}{{#if hasActions}} {{snakeCaseToCamelCaseAndSingularize id}}: {{name}}.{{name}}Resource; {{/if}}{{/each}} } + + // Webhook Handler + export type WebhookEventType = EventTypeEnum | 'unhandled_event'; + export type WebhookContentTypeValue = `${WebhookContentType}`; + export type WebhookEventListener = (event: WebhookEvent) => Promise | void; + export type WebhookErrorListener = (error: Error) => Promise | void; + + // Helper type to map string literal to enum member + type StringToWebhookContentType = { + [K in WebhookContentType]: `${K}` extends S ? K : never + }[WebhookContentType]; + + export class 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( + body: string | object, + headers?: Record, + ): void; + onError?: (error: any) => void; + requestValidator?: ( + headers: Record, + ) => void; + } + + // Webhook Auth + export function basicAuthValidator( + validateCredentials: (username: string, password: string) => boolean, + ): (headers: Record) => void; + + // Default webhook handler instance + export const webhook: WebhookHandler; + } \ No newline at end of file From 5b2930388b40d8aebeb094ea397331bad9e45e9a Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Wed, 10 Dec 2025 09:40:36 +0530 Subject: [PATCH 07/10] Event type class name change --- .../templates/ts/typings/v3/index.d.ts.hbs | 27 ++++++++++--------- .../ts/typings/v3/webhookContent.d.ts.hbs | 12 ++++++--- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/main/resources/templates/ts/typings/v3/index.d.ts.hbs b/src/main/resources/templates/ts/typings/v3/index.d.ts.hbs index 1d63895..329ba95 100644 --- a/src/main/resources/templates/ts/typings/v3/index.d.ts.hbs +++ b/src/main/resources/templates/ts/typings/v3/index.d.ts.hbs @@ -85,27 +85,30 @@ declare module 'chargebee' { } // Webhook Handler - export type WebhookEventType = EventTypeEnum | 'unhandled_event'; - export type WebhookContentTypeValue = `${WebhookContentType}`; - export type WebhookEventListener = (event: WebhookEvent) => Promise | void; + export type WebhookEventName = EventTypeEnum | 'unhandled_event'; + export type WebhookEventTypeValue = `${WebhookEventType}`; + /** @deprecated Use WebhookEventTypeValue instead */ + export type WebhookContentTypeValue = WebhookEventTypeValue; + + export type WebhookEventListener = (event: WebhookEvent) => Promise | void; export type WebhookErrorListener = (error: Error) => Promise | void; // Helper type to map string literal to enum member - type StringToWebhookContentType = { - [K in WebhookContentType]: `${K}` extends S ? K : never - }[WebhookContentType]; + type StringToWebhookEventType = { + [K in WebhookEventType]: `${K}` extends S ? K : never + }[WebhookEventType]; export class WebhookHandler { - on(eventName: T, listener: WebhookEventListener): this; - on(eventName: S, listener: WebhookEventListener>): this; + 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: 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: T, listener: WebhookEventListener): this; + off(eventName: S, listener: WebhookEventListener>): this; off(eventName: 'unhandled_event', listener: WebhookEventListener): this; off(eventName: 'error', listener: WebhookErrorListener): this; handle( diff --git a/src/main/resources/templates/ts/typings/v3/webhookContent.d.ts.hbs b/src/main/resources/templates/ts/typings/v3/webhookContent.d.ts.hbs index f99dcc7..867f1d9 100644 --- a/src/main/resources/templates/ts/typings/v3/webhookContent.d.ts.hbs +++ b/src/main/resources/templates/ts/typings/v3/webhookContent.d.ts.hbs @@ -4,17 +4,21 @@ declare module 'chargebee' { - export enum WebhookContentType { {{#each this}} + export enum WebhookEventType { {{#each this}} {{snakeCaseToPascalCase type}} = '{{type}}',{{/each}} } + /** + * @deprecated Use WebhookEventType instead. + */ + export import WebhookContentType = WebhookEventType; export type WebhookContentMap = { {{#each this}} - [WebhookContentType.{{snakeCaseToPascalCase type}}] : {{snakeCaseToPascalCase type}}Content;{{/each}} + [WebhookEventType.{{snakeCaseToPascalCase type}}] : {{snakeCaseToPascalCase type}}Content;{{/each}} }; - export type ContentFor = WebhookContentMap[T]; + export type ContentFor = WebhookContentMap[T]; - export interface WebhookEvent { + export interface WebhookEvent { content: ContentFor; id:string; occurred_at:number; From 93f57bf52761bebd44f81cdfb238b30709a5a2b3 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Tue, 16 Dec 2025 14:01:09 +0530 Subject: [PATCH 08/10] add Event type run-time export --- .../java/com/chargebee/sdk/node/NodeV3.java | 42 +++++++++++++++---- .../sdk/node/webhook/WebhookGenerator.java | 16 ++++++- .../templates/node/chargebee_cjs.ts.hbs | 22 ++++++++++ .../templates/node/chargebee_esm.ts.hbs | 16 +++++++ .../templates/node/webhook_event_types.ts.hbs | 14 ++++++- .../templates/node/webhook_handler.ts.hbs | 3 ++ .../ts/typings/v3/webhookContent.d.ts.hbs | 1 + 7 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 src/main/resources/templates/node/chargebee_cjs.ts.hbs create mode 100644 src/main/resources/templates/node/chargebee_esm.ts.hbs diff --git a/src/main/java/com/chargebee/sdk/node/NodeV3.java b/src/main/java/com/chargebee/sdk/node/NodeV3.java index 84ea159..e2698a1 100644 --- a/src/main/java/com/chargebee/sdk/node/NodeV3.java +++ b/src/main/java/com/chargebee/sdk/node/NodeV3.java @@ -23,18 +23,41 @@ protected List generateSDK(String outputDirectoryPath, Spec spec) throws List fileOps = new ArrayList<>(); fileOps.add(generateApiEndpointsFile(outputDirectoryPath, resources)); - // Generate webhook files (content, handler, auth) + // Generate webhook files (content, handler, auth, eventType) { Template contentTemplate = getTemplateContent("webhookContent"); Template handlerTemplate = getTemplateContent("webhookHandler"); Template authTemplate = getTemplateContent("webhookAuth"); + Template eventTypesTemplate = getTemplateContent("webhookEventTypes"); fileOps.addAll( WebhookGenerator.generate( outputDirectoryPath, spec, contentTemplate, handlerTemplate, - authTemplate + authTemplate, + eventTypesTemplate + ) + ); + } + + // Generate entry point files (in parent directory of resources) + { + String parentDirectoryPath = outputDirectoryPath.replace("/resources", ""); + Template esmTemplate = getTemplateContent("chargebeeEsm"); + Template cjsTemplate = getTemplateContent("chargebeeCjs"); + fileOps.add( + new FileOp.WriteString( + parentDirectoryPath, + "chargebee.esm.ts", + esmTemplate.apply("") + ) + ); + fileOps.add( + new FileOp.WriteString( + parentDirectoryPath, + "chargebee.cjs.ts", + cjsTemplate.apply("") ) ); } @@ -44,12 +67,15 @@ protected List generateSDK(String outputDirectoryPath, Spec spec) throws @Override protected Map templatesDefinition() { - return Map.of( - "api_endpoints", "/templates/node/api_endpoints.ts.hbs", - "webhookContent", "/templates/node/webhook_content.ts.hbs", - "webhookHandler", "/templates/node/webhook_handler.ts.hbs", - "webhookAuth", "/templates/node/webhook_auth.ts.hbs" - ); + var templates = new HashMap(); + templates.put("api_endpoints", "/templates/node/api_endpoints.ts.hbs"); + templates.put("webhookContent", "/templates/node/webhook_content.ts.hbs"); + templates.put("webhookHandler", "/templates/node/webhook_handler.ts.hbs"); + templates.put("webhookAuth", "/templates/node/webhook_auth.ts.hbs"); + templates.put("webhookEventTypes", "/templates/node/webhook_event_types.ts.hbs"); + templates.put("chargebeeEsm", "/templates/node/chargebee_esm.ts.hbs"); + templates.put("chargebeeCjs", "/templates/node/chargebee_cjs.ts.hbs"); + return templates; } private FileOp generateApiEndpointsFile(String resourcesDirectoryPath, List resources) diff --git a/src/main/java/com/chargebee/sdk/node/webhook/WebhookGenerator.java b/src/main/java/com/chargebee/sdk/node/webhook/WebhookGenerator.java index 4b81701..f4f1495 100644 --- a/src/main/java/com/chargebee/sdk/node/webhook/WebhookGenerator.java +++ b/src/main/java/com/chargebee/sdk/node/webhook/WebhookGenerator.java @@ -36,7 +36,8 @@ public static List generate( Spec spec, Template contentTemplate, Template handlerTemplate, - Template authTemplate) + Template authTemplate, + Template eventTypesTemplate) throws IOException { final String webhookDirectoryPath = "/webhook"; List fileOps = new ArrayList<>(); @@ -124,6 +125,19 @@ public static List generate( ); } + // eventType.ts + { + Map ctx = new HashMap<>(); + ctx.put("events", events); + fileOps.add( + new FileOp.WriteString( + outputDirectoryPath + webhookDirectoryPath, + "eventType.ts", + eventTypesTemplate.apply(ctx) + ) + ); + } + return fileOps; } } diff --git a/src/main/resources/templates/node/chargebee_cjs.ts.hbs b/src/main/resources/templates/node/chargebee_cjs.ts.hbs new file mode 100644 index 0000000..dc2d0c4 --- /dev/null +++ b/src/main/resources/templates/node/chargebee_cjs.ts.hbs @@ -0,0 +1,22 @@ +import { CreateChargebee } from './createChargebee.js'; +import { FetchHttpClient } from './net/FetchClient.js'; +import { WebhookHandler, WebhookEventType, WebhookContentType } from './resources/webhook/handler.js'; +import webhookInstance from './resources/webhook/handler.js'; +import { basicAuthValidator } from './resources/webhook/auth.js'; + +const httpClient = new FetchHttpClient(); +const Chargebee = CreateChargebee(httpClient); +module.exports = Chargebee; +module.exports.Chargebee = Chargebee; +module.exports.default = Chargebee; + +// Export webhook modules +module.exports.WebhookHandler = WebhookHandler; +module.exports.WebhookEventType = WebhookEventType; +module.exports.WebhookContentType = WebhookContentType; +module.exports.webhook = webhookInstance; +module.exports.basicAuthValidator = basicAuthValidator; + +// Export webhook types +export type { WebhookEvent } from './resources/webhook/content.js'; + diff --git a/src/main/resources/templates/node/chargebee_esm.ts.hbs b/src/main/resources/templates/node/chargebee_esm.ts.hbs new file mode 100644 index 0000000..d9001a3 --- /dev/null +++ b/src/main/resources/templates/node/chargebee_esm.ts.hbs @@ -0,0 +1,16 @@ +import { CreateChargebee } from './createChargebee.js'; +import { FetchHttpClient } from './net/FetchClient.js'; + +const httpClient = new FetchHttpClient(); +const Chargebee = CreateChargebee(httpClient); + +export default Chargebee; + +// Export webhook modules +export { WebhookHandler, WebhookEventType, WebhookContentType } from './resources/webhook/handler.js'; +export { default as webhook } from './resources/webhook/handler.js'; +export { basicAuthValidator } from './resources/webhook/auth.js'; + +// Export webhook types +export type { WebhookEvent } from './resources/webhook/content.js'; + diff --git a/src/main/resources/templates/node/webhook_event_types.ts.hbs b/src/main/resources/templates/node/webhook_event_types.ts.hbs index bb72667..b74129f 100644 --- a/src/main/resources/templates/node/webhook_event_types.ts.hbs +++ b/src/main/resources/templates/node/webhook_event_types.ts.hbs @@ -1,6 +1,16 @@ -export enum EventType { +/** + * Enum representing all possible webhook event types from Chargebee. + * This enum provides both compile-time type safety and runtime values. + */ +export enum WebhookEventType { {{#each events}} - {{constantCase type}} = '{{type}}', + {{snakeCaseToPascalCase type}} = '{{type}}', {{/each}} + UnhandledEvent = 'unhandled_event', } +/** + * @deprecated Use WebhookEventType instead. + */ +export const WebhookContentType = WebhookEventType; + diff --git a/src/main/resources/templates/node/webhook_handler.ts.hbs b/src/main/resources/templates/node/webhook_handler.ts.hbs index 175d09a..c1fa325 100644 --- a/src/main/resources/templates/node/webhook_handler.ts.hbs +++ b/src/main/resources/templates/node/webhook_handler.ts.hbs @@ -1,6 +1,9 @@ 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; diff --git a/src/main/resources/templates/ts/typings/v3/webhookContent.d.ts.hbs b/src/main/resources/templates/ts/typings/v3/webhookContent.d.ts.hbs index 867f1d9..2a58efd 100644 --- a/src/main/resources/templates/ts/typings/v3/webhookContent.d.ts.hbs +++ b/src/main/resources/templates/ts/typings/v3/webhookContent.d.ts.hbs @@ -6,6 +6,7 @@ declare module 'chargebee' { export enum WebhookEventType { {{#each this}} {{snakeCaseToPascalCase type}} = '{{type}}',{{/each}} + UnhandledEvent = 'unhandled_event', } /** * @deprecated Use WebhookEventType instead. From df23d800c652bfd1b269d4bb514024f6f537b8e2 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Wed, 17 Dec 2025 12:39:50 +0530 Subject: [PATCH 09/10] moved un-handled out --- src/main/resources/templates/node/chargebee_cjs.ts.hbs | 1 - src/main/resources/templates/node/chargebee_esm.ts.hbs | 1 - src/main/resources/templates/node/webhook_event_types.ts.hbs | 2 -- src/main/resources/templates/node/webhook_handler.ts.hbs | 2 +- .../resources/templates/ts/typings/v3/webhookContent.d.ts.hbs | 1 - 5 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/resources/templates/node/chargebee_cjs.ts.hbs b/src/main/resources/templates/node/chargebee_cjs.ts.hbs index dc2d0c4..141bce3 100644 --- a/src/main/resources/templates/node/chargebee_cjs.ts.hbs +++ b/src/main/resources/templates/node/chargebee_cjs.ts.hbs @@ -19,4 +19,3 @@ module.exports.basicAuthValidator = basicAuthValidator; // Export webhook types export type { WebhookEvent } from './resources/webhook/content.js'; - diff --git a/src/main/resources/templates/node/chargebee_esm.ts.hbs b/src/main/resources/templates/node/chargebee_esm.ts.hbs index d9001a3..75df20d 100644 --- a/src/main/resources/templates/node/chargebee_esm.ts.hbs +++ b/src/main/resources/templates/node/chargebee_esm.ts.hbs @@ -13,4 +13,3 @@ export { basicAuthValidator } from './resources/webhook/auth.js'; // Export webhook types export type { WebhookEvent } from './resources/webhook/content.js'; - diff --git a/src/main/resources/templates/node/webhook_event_types.ts.hbs b/src/main/resources/templates/node/webhook_event_types.ts.hbs index b74129f..859534d 100644 --- a/src/main/resources/templates/node/webhook_event_types.ts.hbs +++ b/src/main/resources/templates/node/webhook_event_types.ts.hbs @@ -6,11 +6,9 @@ export enum WebhookEventType { {{#each events}} {{snakeCaseToPascalCase type}} = '{{type}}', {{/each}} - UnhandledEvent = 'unhandled_event', } /** * @deprecated Use WebhookEventType instead. */ export const WebhookContentType = WebhookEventType; - diff --git a/src/main/resources/templates/node/webhook_handler.ts.hbs b/src/main/resources/templates/node/webhook_handler.ts.hbs index c1fa325..7c1cf22 100644 --- a/src/main/resources/templates/node/webhook_handler.ts.hbs +++ b/src/main/resources/templates/node/webhook_handler.ts.hbs @@ -24,7 +24,7 @@ export class WebhookHandler extends EventEmitter { constructor() { super({ captureRejections: true }); } - + handle( body: string | object, headers?: Record, diff --git a/src/main/resources/templates/ts/typings/v3/webhookContent.d.ts.hbs b/src/main/resources/templates/ts/typings/v3/webhookContent.d.ts.hbs index 2a58efd..867f1d9 100644 --- a/src/main/resources/templates/ts/typings/v3/webhookContent.d.ts.hbs +++ b/src/main/resources/templates/ts/typings/v3/webhookContent.d.ts.hbs @@ -6,7 +6,6 @@ declare module 'chargebee' { export enum WebhookEventType { {{#each this}} {{snakeCaseToPascalCase type}} = '{{type}}',{{/each}} - UnhandledEvent = 'unhandled_event', } /** * @deprecated Use WebhookEventType instead. From 24544c5bce6222559d9c49007435bc027ce1f9d9 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Wed, 24 Dec 2025 13:32:32 +0530 Subject: [PATCH 10/10] framwork agnostic request-response type --- .../templates/node/chargebee_cjs.ts.hbs | 3 +- .../templates/node/chargebee_esm.ts.hbs | 3 +- .../templates/node/webhook_auth.ts.hbs | 50 ++++++++-- .../templates/node/webhook_handler.ts.hbs | 98 ++++++++++++++++--- .../templates/ts/typings/v3/index.d.ts.hbs | 83 ++++++++++++---- 5 files changed, 192 insertions(+), 45 deletions(-) diff --git a/src/main/resources/templates/node/chargebee_cjs.ts.hbs b/src/main/resources/templates/node/chargebee_cjs.ts.hbs index 141bce3..38886df 100644 --- a/src/main/resources/templates/node/chargebee_cjs.ts.hbs +++ b/src/main/resources/templates/node/chargebee_cjs.ts.hbs @@ -18,4 +18,5 @@ module.exports.webhook = webhookInstance; module.exports.basicAuthValidator = basicAuthValidator; // Export webhook types -export type { WebhookEvent } from './resources/webhook/content.js'; +export type { WebhookEvent, WebhookContext, WebhookHandlerOptions, RequestValidator } from './resources/webhook/handler.js'; +export type { CredentialValidator } from './resources/webhook/auth.js'; diff --git a/src/main/resources/templates/node/chargebee_esm.ts.hbs b/src/main/resources/templates/node/chargebee_esm.ts.hbs index 75df20d..c7acf3b 100644 --- a/src/main/resources/templates/node/chargebee_esm.ts.hbs +++ b/src/main/resources/templates/node/chargebee_esm.ts.hbs @@ -12,4 +12,5 @@ export { default as webhook } from './resources/webhook/handler.js'; export { basicAuthValidator } from './resources/webhook/auth.js'; // Export webhook types -export type { WebhookEvent } from './resources/webhook/content.js'; +export type { WebhookEvent, WebhookContext, WebhookHandlerOptions, RequestValidator } from './resources/webhook/handler.js'; +export type { CredentialValidator } from './resources/webhook/auth.js'; diff --git a/src/main/resources/templates/node/webhook_auth.ts.hbs b/src/main/resources/templates/node/webhook_auth.ts.hbs index 23a207a..34cc906 100644 --- a/src/main/resources/templates/node/webhook_auth.ts.hbs +++ b/src/main/resources/templates/node/webhook_auth.ts.hbs @@ -1,11 +1,39 @@ +/** + * 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: (username: string, password: string) => boolean, + validateCredentials: CredentialValidator, ) => { - return (headers: Record) => { + return async (headers: Record): Promise => { const authHeader = headers['authorization'] || headers['Authorization']; if (!authHeader) { - throw new Error('Invalid authorization header'); + throw new Error('Missing authorization header'); } const authStr = Array.isArray(authHeader) ? authHeader[0] : authHeader; @@ -15,15 +43,21 @@ export const basicAuthValidator = ( const parts = authStr.split(' '); if (parts.length !== 2 || parts[0] !== 'Basic') { - throw new Error('Invalid authorization header'); + throw new Error('Invalid authorization header format'); } - const credentials = Buffer.from(parts[1], 'base64').toString().split(':'); - if (credentials.length !== 2) { - throw new Error('Invalid credentials'); + const decoded = Buffer.from(parts[1], 'base64').toString(); + const separatorIndex = decoded.indexOf(':'); + + if (separatorIndex === -1) { + throw new Error('Invalid credentials format'); } - if (!validateCredentials(credentials[0], credentials[1])) { + 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/main/resources/templates/node/webhook_handler.ts.hbs b/src/main/resources/templates/node/webhook_handler.ts.hbs index 7c1cf22..8eb4c28 100644 --- a/src/main/resources/templates/node/webhook_handler.ts.hbs +++ b/src/main/resources/templates/node/webhook_handler.ts.hbs @@ -7,42 +7,106 @@ export { WebhookEventType, WebhookContentType }; export type EventType = import('chargebee').EventTypeEnum; -export interface WebhookEventMap extends Record { - unhandled_event: [WebhookEvent]; +/** + * Context object passed to webhook event listeners. + * Wraps the event data with optional framework-specific request/response objects. + */ +export interface WebhookContext { + /** The parsed webhook event from Chargebee */ + event: WebhookEvent; + /** Framework-specific request object (Express, Fastify, etc.) */ + request?: ReqT; + /** Framework-specific response object (Express, Fastify, etc.) */ + response?: ResT; +} + +export interface WebhookEventMap extends Record]> { + unhandled_event: [WebhookContext]; error: [Error]; } -export type WebhookEventListener = ( - ...args: WebhookEventMap[K] +export type WebhookEventListener> = ( + ...args: WebhookEventMap[K] ) => Promise | void; -export class WebhookHandler extends EventEmitter { - requestValidator?: ( - headers: Record, - ) => void; +/** + * Validator function type for authenticating webhook requests. + * Can be synchronous or asynchronous. + */ +export type RequestValidator = ( + headers: Record, +) => void | Promise; + +/** + * Configuration options for WebhookHandler. + */ +export interface WebhookHandlerOptions { + /** + * Optional validator function to authenticate incoming webhook requests. + * Typically used for Basic Auth validation. + * Can be sync or async - throw an error to reject the request. + */ + requestValidator?: RequestValidator; +} + +export class WebhookHandler extends EventEmitter> { + private _requestValidator?: RequestValidator; - constructor() { + constructor(options?: WebhookHandlerOptions) { super({ captureRejections: true }); + this._requestValidator = options?.requestValidator; } - handle( + /** + * Gets the current request validator. + */ + get requestValidator(): RequestValidator | undefined { + return this._requestValidator; + } + + /** + * Sets a new request validator. + */ + set requestValidator(validator: RequestValidator | undefined) { + this._requestValidator = validator; + } + + /** + * Handles an incoming webhook request. + * Validates the request (if validator configured), parses the body, + * and emits the appropriate event. + * + * @param body - The raw request body (string) or pre-parsed object + * @param headers - Optional HTTP headers for validation + * @param request - Optional framework-specific request object + * @param response - Optional framework-specific response object + */ + async handle( body: string | object, headers?: Record, - ): void { + request?: ReqT, + response?: ResT, + ): Promise { try { - if (this.requestValidator && headers) { - this.requestValidator(headers); + if (this._requestValidator && headers) { + await this._requestValidator(headers); } const event: WebhookEvent = typeof body === 'string' ? JSON.parse(body) : (body as WebhookEvent); - const eventType = event.event_type as keyof WebhookEventMap; + const context: WebhookContext = { + event, + request, + response, + }; + + const eventType = event.event_type as keyof WebhookEventMap; if (this.listenerCount(eventType) > 0) { - this.emit(eventType, event); + this.emit(eventType, context); } else { - this.emit('unhandled_event', event); + this.emit('unhandled_event', context); } } catch (err) { this.emit('error', err instanceof Error ? err : new Error(String(err))); @@ -50,6 +114,7 @@ export class WebhookHandler extends EventEmitter { } } +// Default instance for simple use cases const webhook = new WebhookHandler(); // Auto-configure basic auth if env vars are present @@ -65,3 +130,4 @@ if (username && password) { export default webhook; export type { WebhookEvent } from './content.js'; +export { basicAuthValidator, type CredentialValidator } from './auth.js'; diff --git a/src/main/resources/templates/ts/typings/v3/index.d.ts.hbs b/src/main/resources/templates/ts/typings/v3/index.d.ts.hbs index 329ba95..cb3150b 100644 --- a/src/main/resources/templates/ts/typings/v3/index.d.ts.hbs +++ b/src/main/resources/templates/ts/typings/v3/index.d.ts.hbs @@ -84,13 +84,46 @@ declare module 'chargebee' { {{/if}}{{/each}} } - // Webhook Handler + // Webhook Handler Types export type WebhookEventName = EventTypeEnum | 'unhandled_event'; export type WebhookEventTypeValue = `${WebhookEventType}`; /** @deprecated Use WebhookEventTypeValue instead */ export type WebhookContentTypeValue = WebhookEventTypeValue; - export type WebhookEventListener = (event: WebhookEvent) => Promise | void; + /** + * Context object passed to webhook event listeners. + * Wraps the event data with optional framework-specific request/response objects. + */ + export interface WebhookContext { + /** The parsed webhook event from Chargebee */ + event: WebhookEvent; + /** Framework-specific request object (Express, Fastify, etc.) */ + request?: ReqT; + /** Framework-specific response object (Express, Fastify, etc.) */ + response?: ResT; + } + + /** + * Validator function type for authenticating webhook requests. + * Can be synchronous or asynchronous. + */ + export type RequestValidator = ( + headers: Record, + ) => void | Promise; + + /** + * Configuration options for WebhookHandler. + */ + export interface WebhookHandlerOptions { + /** + * Optional validator function to authenticate incoming webhook requests. + * Typically used for Basic Auth validation. + * Can be sync or async - throw an error to reject the request. + */ + requestValidator?: RequestValidator; + } + + export type WebhookEventListener = (context: WebhookContext & { event: WebhookEvent }) => Promise | void; export type WebhookErrorListener = (error: Error) => Promise | void; // Helper type to map string literal to enum member @@ -98,33 +131,45 @@ declare module 'chargebee' { [K in WebhookEventType]: `${K}` extends S ? K : never }[WebhookEventType]; - export class WebhookHandler { - on(eventName: T, listener: WebhookEventListener): this; - on(eventName: S, listener: WebhookEventListener>): this; - on(eventName: 'unhandled_event', listener: WebhookEventListener): this; + export class WebhookHandler { + constructor(options?: WebhookHandlerOptions); + on(eventName: T, listener: WebhookEventListener): this; + on(eventName: S, listener: WebhookEventListener>): this; + on(eventName: 'unhandled_event', listener: WebhookEventListener): this; on(eventName: 'error', listener: WebhookErrorListener): this; - once(eventName: T, listener: WebhookEventListener): this; - once(eventName: S, listener: WebhookEventListener>): this; - once(eventName: 'unhandled_event', listener: WebhookEventListener): this; + once(eventName: 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: T, listener: WebhookEventListener): this; + off(eventName: S, listener: WebhookEventListener>): this; + off(eventName: 'unhandled_event', listener: WebhookEventListener): this; off(eventName: 'error', listener: WebhookErrorListener): this; handle( body: string | object, headers?: Record, - ): void; - onError?: (error: any) => void; - requestValidator?: ( - headers: Record, - ) => void; + request?: ReqT, + response?: ResT, + ): Promise; + requestValidator: RequestValidator | undefined; } // Webhook Auth + /** + * Credential validator function type. + * Can be synchronous or asynchronous (e.g., for database lookups). + */ + export type CredentialValidator = ( + username: string, + password: string, + ) => boolean | Promise; + + /** + * Creates a Basic Auth validator for webhook requests. + */ export function basicAuthValidator( - validateCredentials: (username: string, password: string) => boolean, - ): (headers: Record) => void; + validateCredentials: CredentialValidator, + ): (headers: Record) => Promise; // Default webhook handler instance export const webhook: WebhookHandler;