Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9ac5101
event and listners for webhook
cb-karthikp Nov 24, 2025
e396c2e
update type
cb-karthikp Dec 1, 2025
a4a3ba4
include pc1 events
cb-karthikp Dec 1, 2025
b6810d9
node webhook hbs changes
cb-karthikp Dec 1, 2025
7419c12
add default webhook handler instance
cb-karthikp Dec 4, 2025
583508f
Update index.d.ts.hbs
cb-karthikp Dec 9, 2025
5b29303
Event type class name change
cb-karthikp Dec 10, 2025
93f57bf
add Event type run-time export
cb-karthikp Dec 16, 2025
df23d80
moved un-handled out
cb-karthikp Dec 17, 2025
24544c5
framwork agnostic request-response type
cb-karthikp Dec 24, 2025
64c417c
Merge branch 'main' into node-webhook-handler
cb-karthikp Jan 21, 2026
76d5496
SDK changes
cb-karthikp Feb 4, 2026
ec368d3
move default auth validation to util
cb-karthikp Feb 4, 2026
6696142
add warning for no-auth webhook flow
cb-karthikp Feb 4, 2026
24e07ef
add field validation
cb-karthikp Feb 4, 2026
46822f1
better error management
cb-karthikp Feb 4, 2026
b69976a
add deprecation message and strict-content type
cb-karthikp Feb 4, 2026
0940ac9
add comments
cb-karthikp Feb 4, 2026
c4b6050
Merge branch 'main' into node-webhook-handler
cb-karthikp Feb 4, 2026
a545d0a
Update test case
cb-karthikp Feb 5, 2026
9087f91
Merge branch 'main' into node-webhook-handler
cb-karthikp Feb 5, 2026
fc49503
fix testcase
cb-karthikp Feb 5, 2026
a2b132e
Fixed an issue with the webhook content for hidden resources.
cb-alish Feb 5, 2026
228ce2e
add support on error.
cb-karthikp Feb 11, 2026
0f74017
Merge branch 'main' into node-webhook-handler
cb-karthikp Feb 11, 2026
90cf45e
Fix TypeScriptTypingV3Tests for updated webhook
cb-karthikp Feb 11, 2026
bc4769d
Webhook error changes
cb-karthikp Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/main/java/com/chargebee/handlebar/NameFormatHelpers.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,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("-", "_");
}
}
}
1 change: 1 addition & 0 deletions src/main/java/com/chargebee/sdk/Language.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,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",
Expand Down

Large diffs are not rendered by default.

36 changes: 31 additions & 5 deletions src/main/java/com/chargebee/sdk/node/NodeV3.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand All @@ -19,16 +20,37 @@ protected List<FileOp> generateSDK(String outputDirectoryPath, Spec spec) throws
.filter(resource -> !Arrays.stream(this.hiddenOverride).toList().contains(resource.id))
.sorted(Comparator.comparing(Resource::sortOrder))
.toList();

List<FileOp> fileOps = new ArrayList<>();
fileOps.add(generateApiEndpointsFile(outputDirectoryPath, resources));

// Generate webhook event types file
fileOps.addAll(generateWebhookEventTypes(outputDirectoryPath, spec));
// Generate webhook files (content, handler, auth, eventType, errors)
{
Template contentTemplate = getTemplateContent("webhookContent");
Template handlerTemplate = getTemplateContent("webhookHandler");
Template authTemplate = getTemplateContent("webhookAuth");
Template eventTypesTemplate = getTemplateContent("webhookEventTypes");
Template errorsTemplate = getTemplateContent("webhookErrors");
fileOps.addAll(
WebhookGenerator.generate(
outputDirectoryPath,
spec,
contentTemplate,
handlerTemplate,
authTemplate,
eventTypesTemplate,
errorsTemplate));
}

// Generate entry point files (in parent directory of resources)
String parentDirectoryPath = outputDirectoryPath.replace("/resources", "");
fileOps.addAll(generateEntryPoints(parentDirectoryPath));
{
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("")));
}

return fileOps;
}
Expand All @@ -37,7 +59,11 @@ protected List<FileOp> generateSDK(String outputDirectoryPath, Spec spec) throws
protected Map<String, String> templatesDefinition() {
var templates = new HashMap<String, String>();
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("webhookErrors", "/templates/node/webhook_errors.ts.hbs");
templates.put("chargebeeEsm", "/templates/node/chargebee_esm.ts.hbs");
templates.put("chargebeeCjs", "/templates/node/chargebee_cjs.ts.hbs");
return templates;
Expand Down
168 changes: 168 additions & 0 deletions src/main/java/com/chargebee/sdk/node/webhook/WebhookGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
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 io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.Schema;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

public class WebhookGenerator {
private static List<String> getEventResourcesForAEvent(Resource eventResource, Spec spec) {
List<String> resources = new ArrayList<>();
for (Attribute attribute : eventResource.attributes()) {
if (attribute.name.equals("content")) {
attribute
.attributes()
.forEach(
(innerAttribute -> {
Schema<?> schema = innerAttribute.schema;
String ref = null;
boolean isArray = false;
if (schema instanceof ArraySchema) {
ArraySchema arraySchema = (ArraySchema) schema;
Schema<?> itemSchema = arraySchema.getItems();
if (itemSchema != null) {
ref = itemSchema.get$ref();
isArray = true;
}
} else {
ref = schema.get$ref();
}
Set<String> hiddenResourceNames = getHiddenResources(spec);
if (ref != null && ref.contains("/")) {
String schemaName = ref.substring(ref.lastIndexOf("/") + 1);
if (hiddenResourceNames.contains(schemaName)) {
return;
}
if (isArray) {
resources.add(
String.format("%s: import('chargebee').%s[];", schemaName, schemaName));
} else {
resources.add(
String.format("%s: import('chargebee').%s;", schemaName, schemaName));
}
}
}));
}
}
return resources;
}

public static List<FileOp> generate(
String outputDirectoryPath,
Spec spec,
Template contentTemplate,
Template handlerTemplate,
Template authTemplate,
Template eventTypesTemplate,
Template errorsTemplate)
throws IOException {
final String webhookDirectoryPath = "/webhook";
List<FileOp> 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<Map<String, Object>> events = new ArrayList<>();
Set<String> seenTypes = new HashSet<>();
Set<String> uniqueImports = new HashSet<>();

for (Map<String, String> 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<String> allSchemas = getEventResourcesForAEvent(matchedSchema, spec);
List<String> schemaImports = new ArrayList<>();

for (String schema : allSchemas) {
schemaImports.add(schema);
uniqueImports.add(schema);
}

Map<String, Object> params = new HashMap<>();
params.put("type", type);
params.put("resource_schemas", schemaImports);
events.add(params);
}

events.sort(Comparator.comparing(e -> e.get("type").toString()));

// content.ts
{
Map<String, Object> ctx = new HashMap<>();
ctx.put("events", events);
List<String> 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 (static template)
{
fileOps.add(
new FileOp.WriteString(
outputDirectoryPath + webhookDirectoryPath, "handler.ts", handlerTemplate.apply("")));
}

// auth.ts
{
fileOps.add(
new FileOp.WriteString(
outputDirectoryPath + webhookDirectoryPath, "auth.ts", authTemplate.apply("")));
}

// errors.ts
{
fileOps.add(
new FileOp.WriteString(
outputDirectoryPath + webhookDirectoryPath, "errors.ts", errorsTemplate.apply("")));
}

// eventType.ts
{
Map<String, Object> ctx = new HashMap<>();
ctx.put("events", events);
fileOps.add(
new FileOp.WriteString(
outputDirectoryPath + webhookDirectoryPath,
"eventType.ts",
eventTypesTemplate.apply(ctx)));
}

return fileOps;
}

public static Set<String> getHiddenResources(Spec spec) {
return spec.allResources().stream()
.filter((res) -> !res.isNotHiddenFromSDKGeneration())
.map((res) -> res.name)
.collect(Collectors.toSet());
}
}
15 changes: 13 additions & 2 deletions src/main/resources/templates/node/chargebee_cjs.ts.hbs
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { CreateChargebee } from './createChargebee.js';
import { FetchHttpClient } from './net/FetchClient.js';
import { WebhookEventType, WebhookContentType } from './resources/webhook/eventType.js';
import { WebhookEventType, WebhookContentType, WebhookError, WebhookAuthenticationError, WebhookPayloadValidationError, WebhookPayloadParseError } 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 event types
// Export webhook utilities
module.exports.WebhookEventType = WebhookEventType;
module.exports.WebhookContentType = WebhookContentType;
module.exports.basicAuthValidator = basicAuthValidator;

// Export webhook error classes
module.exports.WebhookError = WebhookError;
module.exports.WebhookAuthenticationError = WebhookAuthenticationError;
module.exports.WebhookPayloadValidationError = WebhookPayloadValidationError;
module.exports.WebhookPayloadParseError = WebhookPayloadParseError;

// Export webhook types
export type { WebhookEvent, WebhookContext, WebhookHandlerOptions, HandleOptions, RequestValidator } from './resources/webhook/handler.js';
export type { CredentialValidator } from './resources/webhook/auth.js';
9 changes: 7 additions & 2 deletions src/main/resources/templates/node/chargebee_esm.ts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ const Chargebee = CreateChargebee(httpClient);

export default Chargebee;

// Export webhook event types
export { WebhookEventType, WebhookContentType } from './resources/webhook/eventType.js';
// Export webhook utilities
export { WebhookEventType, WebhookContentType } from './resources/webhook/handler.js';
export { basicAuthValidator } from './resources/webhook/auth.js';
export { WebhookError, WebhookAuthenticationError, WebhookPayloadValidationError, WebhookPayloadParseError } from './resources/webhook/handler.js';

// Export webhook types
export type { WebhookEvent, WebhookContext, WebhookHandlerOptions, HandleOptions, RequestValidator } from './resources/webhook/handler.js';
export type { CredentialValidator } from './resources/webhook/auth.js';
68 changes: 68 additions & 0 deletions src/main/resources/templates/node/webhook_auth.ts.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { WebhookAuthenticationError } from './errors.js';

/**
* Credential validator function type.
* Can be synchronous or asynchronous (e.g., for database lookups).
*/
export type CredentialValidator = (
username: string,
password: string,
) => boolean | Promise<boolean>;

/**
* 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
*
* @throws {WebhookAuthenticationError} When authentication fails
*
* @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<string, string | string[] | undefined>): Promise<void> => {
const authHeader = headers['authorization'] || headers['Authorization'];

if (!authHeader) {
throw new WebhookAuthenticationError('Missing authorization header');
}

const authStr = Array.isArray(authHeader) ? authHeader[0] : authHeader;
if (!authStr) {
throw new WebhookAuthenticationError('Invalid authorization header');
}

const parts = authStr.split(' ');
if (parts.length !== 2 || parts[0] !== 'Basic') {
throw new WebhookAuthenticationError('Invalid authorization header format');
}

const decoded = Buffer.from(parts[1], 'base64').toString();
const separatorIndex = decoded.indexOf(':');

if (separatorIndex === -1) {
throw new WebhookAuthenticationError('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 WebhookAuthenticationError('Invalid credentials');
}
};
};
Loading