ProvenPin Install

@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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.