@octokit/webhooks signature verification and event handlers: working versions (tested 2026-10-15)
Sign and verify GitHub X-Hub-Signature-256 payloads with @octokit/webhooks, dispatch issues.opened, push and onAny handlers, and serve them through createNodeMiddleware over real local HTTP.
passed4 passed · 0 failed2026-10-15 20:09 UTC- Status
- passed
- Last passed
- 2026-10-15 20:09 UTC
- Tests
- 4 / 4
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions3
@octokit/webhooks@14.2.0@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38799498972
The code
Exactly the files that passed. Your agent can pull them with get_recipe("octokit-webhooks-verify-handler") (install), or see them on GitHub.
package.json
{
"name": "octokit-webhooks-verify-handler",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"@octokit/webhooks": "14.2.0"
},
"devDependencies": {
"@types/node": "26.6.5",
"tsx": "4.23.15"
}
}
src/hooks.ts
import { Webhooks, createNodeMiddleware } from "@octokit/webhooks";
import http from "node:http";
export type Seen = { event: string; detail: string }[];
export function makeWebhooks(secret: string, seen: Seen) {
const webhooks = new Webhooks({ secret });
webhooks.on("issues.opened", ({ id, payload }) => {
seen.push({ event: "issues.opened", detail: `${id}:#${payload.issue.number} ${payload.issue.title}` });
});
webhooks.on(["push"], ({ payload }) => {
seen.push({ event: "push", detail: `${payload.ref} ${payload.commits.length} commits` });
});
webhooks.onAny(({ name }) => { seen.push({ event: "any", detail: String(name) }); });
return webhooks;
}
/** Node http server that verifies X-Hub-Signature-256 and dispatches at /api/github/webhooks. */
export function makeServer(webhooks: Webhooks) {
const middleware = createNodeMiddleware(webhooks, { path: "/api/github/webhooks" });
return http.createServer(async (req, res) => {
if (await middleware(req, res)) return;
res.writeHead(404).end("not found");
});
}
test/hooks.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import type { AddressInfo } from "node:net";
import { Webhooks } from "@octokit/webhooks";
import { makeWebhooks, makeServer, type Seen } from "../src/hooks.ts";
const SECRET = "it's a secret to everybody";
const issuePayload = {
action: "opened",
issue: { number: 7, title: "Gauge broken", user: { login: "jp" } },
repository: { full_name: "acme/app" },
sender: { login: "jp" },
};
test("sign and verify round trip", async () => {
const wh = new Webhooks({ secret: SECRET });
const body = JSON.stringify(issuePayload);
const sig = await wh.sign(body);
assert.match(sig, /^sha256=[0-9a-f]{64}$/);
assert.equal(await wh.verify(body, sig), true);
assert.equal(await wh.verify(body + " ", sig), false);
});
test("verifyAndReceive dispatches to specific and onAny handlers", async () => {
const seen: Seen = [];
const wh = makeWebhooks(SECRET, seen);
const payload = JSON.stringify(issuePayload);
await wh.verifyAndReceive({ id: "d-1", name: "issues", payload, signature: await wh.sign(payload) });
const byEvent = [...seen].sort((a, b) => a.event.localeCompare(b.event));
assert.deepEqual(byEvent, [
{ event: "any", detail: "issues" },
{ event: "issues.opened", detail: "d-1:#7 Gauge broken" },
]);
});
test("bad signature is rejected", async () => {
const wh = makeWebhooks(SECRET, []);
const payload = JSON.stringify(issuePayload);
await assert.rejects(wh.verifyAndReceive({ id: "d-2", name: "issues", payload, signature: "sha256=" + "0".repeat(64) }));
});
test("node middleware over real HTTP", async () => {
const seen: Seen = [];
const wh = makeWebhooks(SECRET, seen);
const server = makeServer(wh).listen(0);
await new Promise((r) => server.once("listening", r));
const url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/api/github/webhooks`;
const push = JSON.stringify({ ref: "refs/heads/main", commits: [{ id: "a" }, { id: "b" }], repository: { full_name: "acme/app" }, sender: { login: "jp" } });
const headers = (sig: string) => ({ "content-type": "application/json", "x-github-event": "push", "x-github-delivery": "d-3", "x-hub-signature-256": sig });
try {
const ok = await fetch(url, { method: "POST", body: push, headers: headers(await wh.sign(push)) });
assert.equal(ok.status, 200);
assert.ok(seen.some((s) => s.event === "push" && s.detail === "refs/heads/main 2 commits"));
const bad = await fetch(url, { method: "POST", body: push, headers: headers("sha256=" + "f".repeat(64)) });
assert.equal(bad.status, 400);
const other = await fetch(url.replace("/api/github/webhooks", "/nope"));
assert.equal(other.status, 404);
} finally {
server.close();
}
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.