@kontent-ai/webhook-helper
Version:
This utility helps with webhook notifications from Kontent.ai
201 lines (145 loc) • 6.23 kB
Markdown
[](https://badge.fury.io/js/%40kontent-ai%2Fwebhook-helper)
[](https://github.com/kontent-ai/webhook-helper-js/actions/workflows/test.yml)
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](https://kontent.ai/learn/docs/webhooks/webhooks/javascript#a-validate-received-notifications)
Install package:
```bash
npm install @kontent-ai/webhook-helper
pnpm add @kontent-ai/webhook-helper
```
Create a reusable function to parse and validate webhook requests:
```typescript
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:
```typescript
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;
```
```typescript
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:
```typescript
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:
```typescript
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`:
```typescript
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
```typescript
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()`:
```typescript
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:
```typescript
const payload = JSON.stringify(jsonPayload, null, 2);
```
The `replaceLinebreaks()` function normalizes line endings to prevent signature mismatches caused by Windows line breaks:
```typescript
import { replaceLinebreaks } from '@kontent-ai/webhook-helper';
const normalizedPayload = replaceLinebreaks(payload);
```