This package aims to help you with Webhooks received from Kontent.ai projects. It provides:
- Runtime validation and parsing of webhook payloads with Zod
- Type-safe notification handling with TypeScript discriminated unions
- Signature verification
Install package:
npm install @kontent-ai/webhook-helper
pnpm add @kontent-ai/webhook-helperCreate a reusable function to parse and validate webhook requests:
import { parseSignedWebhookResponse, SIGNATURE_HEADER, ParseResult, WebhookResponse } from '@kontent-ai/webhook-helper';
const parseWebhookRequest = async (
request: Request,
secret: string,
): Promise<ParseResult<WebhookResponse>> => {
const signature = request.headers.get(SIGNATURE_HEADER) ?? "";
const payload = await request.text();
return parseSignedWebhookResponse({
payload,
secret,
signature,
});
};
const handleWebhook = async (request: Request) => {
const result = await parseWebhookRequest(request, 'your-webhook-secret');
if (!result.success) {
return new Response(result.error.message, { status: 401 });
}
result.data.notifications.forEach(notification => {
if (notification.object_type === 'content_item') {
console.log('Content item:', notification.data.system.name);
}
});
return new Response('OK', { status: 200 });
};The library provides two main parsing functions:
import { parseSignedWebhookResponse } from '@kontent-ai/webhook-helper';
const result = parseSignedWebhookResponse({payload, secret, signature});
if (!result.success) {
console.error('Validation failed:', result.error);
return;
}
const webhookData = result.data;import { parseWebhookResponse } from '@kontent-ai/webhook-helper';
const result = parseWebhookResponse(body);
if (!result.success) {
console.error('Parsing failed:', result.error);
return;
}
const webhookData = result.data;Each notification has an object_type discriminator that enables type narrowing:
const result = parseWebhookResponse(body);
if (result.success) {
result.data.notifications.forEach(notification => {
switch (notification.object_type) {
case 'content_item':
console.log('Workflow step:', notification.data.system.workflow_step);
console.log('Action:', notification.message.action);
break;
case 'asset':
console.log('Asset ID:', notification.data.system.id);
console.log('Action:', notification.message.action);
break;
case 'content_type':
console.log('Type codename:', notification.data.system.codename);
break;
case 'language':
console.log('Language codename:', notification.data.system.codename);
break;
case 'taxonomy':
console.log('Taxonomy codename:', notification.data.system.codename);
break;
case 'unknown':
console.warn('Unknown notification type:', notification.original_notification);
break;
}
});
}To enable the type-safe discrimination shown above, this library "uplifts" the object_type property to the top level of the notification object. This is a convenience helper that creates a discriminated union, allowing TypeScript to automatically narrow the types.
This means the parsed notification object is a slight superset of the original Kontent.ai webhook payload.
If you need to strictly match the original notification shape (without the top-level object_type), you can use the Omit utility type:
import { AssetNotification } from '@kontent-ai/webhook-helper';
// Removes the helper 'object_type' property to match the raw payload structure
type RawAssetNotification = Omit<AssetNotification, "object_type">;The library is forward-compatible with future webhook types. When a notification doesn't match any known schema, it's typed as UnknownNotification:
type UnknownNotification = {
object_type: "unknown";
original_notification: Record<PropertyKey, unknown>;
};This allows you to:
- Access raw webhook data via
original_notification - Handle future webhook types without breaking your application
- Use exhaustive type checking in switch statements
result.data.notifications.forEach(notification => {
if (notification.object_type === 'unknown') {
console.log('Unrecognized webhook:', notification.original_notification);
}
});If you need to verify signatures separately from parsing, use isSignatureValid():
import { isSignatureValid, replaceLinebreaks, SIGNATURE_HEADER } from '@kontent-ai/webhook-helper';
const verifyWebhookSignature = async (request: Request, secret: string): Promise<boolean> => {
const signature = request.headers.get(SIGNATURE_HEADER);
const payload = await request.text();
return isSignatureValid({payload, secret, signature});
}
if (!await verifyWebhookSignature(request, 'your-webhook-secret')) {
throw new Error('Invalid signature');
}The payload must be exactly the same (including whitespaces) as the original webhook body. If you've parsed the payload into an object, you can reconstruct it:
const payload = JSON.stringify(jsonPayload, null, 2);The replaceLinebreaks() function normalizes line endings to prevent signature mismatches caused by Windows line breaks:
import { replaceLinebreaks } from '@kontent-ai/webhook-helper';
const normalizedPayload = replaceLinebreaks(payload);