@northly/webhook-verify
v1.0.0
Published
Verify webhook signatures from the Northly OKR Tracker
Maintainers
Readme
@northly/webhook-verify
Lightweight Node.js SDK for verifying webhook signatures from the Northly OKR Tracker. Zero runtime dependencies -- uses only the built-in Node.js crypto module.
Installation
npm install @northly/webhook-verifyQuick start
import { constructEvent, WebhookSignatureError } from "@northly/webhook-verify";
const secret = process.env.NORTHLY_WEBHOOK_SECRET; // starts with whsec_
try {
const event = constructEvent(rawBody, signatureHeader, secret);
console.log(event.type); // e.g. "objective.created"
console.log(event.data); // event payload
} catch (err) {
if (err instanceof WebhookSignatureError) {
// Signature mismatch — reject the request
}
}Headers sent by Northly
| Header | Description |
|---|---|
| X-Webhook-Signature | sha256=<hex> HMAC-SHA256 of the raw body |
| X-Webhook-Event | Event type, e.g. objective.created |
| Content-Type | application/json |
Usage with Express.js
import express from "express";
import { constructEvent, WebhookSignatureError } from "@northly/webhook-verify";
const app = express();
// IMPORTANT: use raw body — do not parse JSON before verification
app.post(
"/webhooks/northly",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["x-webhook-signature"] as string;
try {
const event = constructEvent(req.body, signature, process.env.NORTHLY_WEBHOOK_SECRET!);
switch (event.type) {
case "objective.created":
// handle objective created
break;
case "checkin.created":
// handle check-in created
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.status(200).json({ received: true });
} catch (err) {
if (err instanceof WebhookSignatureError) {
return res.status(401).json({ error: "Invalid signature" });
}
res.status(500).json({ error: "Internal server error" });
}
}
);Usage with Next.js API route (App Router)
// app/api/webhooks/northly/route.ts
import { constructEvent, WebhookSignatureError } from "@northly/webhook-verify";
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const signature = req.headers.get("x-webhook-signature");
if (!signature) {
return NextResponse.json({ error: "Missing signature" }, { status: 401 });
}
try {
const event = constructEvent(rawBody, signature, process.env.NORTHLY_WEBHOOK_SECRET!);
switch (event.type) {
case "objective.created":
// handle objective created
break;
case "checkin.created":
// handle check-in created
break;
}
return NextResponse.json({ received: true });
} catch (err) {
if (err instanceof WebhookSignatureError) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}API reference
verifyWebhookSignature(payload, signature, secret): boolean
Verify the HMAC-SHA256 signature of a webhook payload using constant-time comparison.
| Parameter | Type | Description |
|---|---|---|
| payload | string \| Buffer | The raw request body |
| signature | string | The X-Webhook-Signature header value (with or without sha256= prefix) |
| secret | string | Your webhook signing secret |
Returns true if valid, false otherwise. Never throws.
constructEvent(payload, signature, secret): WebhookEvent
Verify the signature and parse the payload into a typed event object. Throws WebhookSignatureError if the signature is invalid or the payload cannot be parsed.
| Parameter | Type | Description |
|---|---|---|
| payload | string \| Buffer | The raw request body |
| signature | string | The X-Webhook-Signature header value |
| secret | string | Your webhook signing secret |
WebhookEvent
interface WebhookEvent {
type: string; // Event name, e.g. "objective.created"
created_at: string; // ISO-8601 timestamp
data: Record<string, unknown>; // Event payload
}WebhookSignatureError
Custom error class thrown by constructEvent when signature verification fails.
Signature algorithm
Northly computes signatures as follows:
- Take the raw JSON request body as a UTF-8 string.
- Compute
HMAC-SHA256(body, secret)using the webhook signing secret. - Encode the result as a lowercase hex string.
- Send it in the
X-Webhook-Signatureheader prefixed withsha256=.
This package reproduces that process and compares using crypto.timingSafeEqual to prevent timing attacks.
