Intent (maintainer's, recorded from a working session 2026-08-07)
"would other webhooks such as github help us out? someday these should get routed through hooksmith"
The org just stood up its service-status layer (infra/cloudflare/status, live at status.bounded.tools), and its POST /webhook/statuspage route is currently its own webhook receiver. That is tolerable only because Statuspage webhooks are unsigned and therefore untrusted by design — the payload is discarded and the event is a mere refresh poke, so there is nothing to verify and nothing to route. The moment a signed webhook source is worth consuming, that excuse expires.
What this issue proposes
Extend hooksmith from git hook events to inbound HTTP webhook events, so receiver infrastructure exists once:
- One place that verifies signatures. GitHub webhooks are HMAC-signed (
X-Hub-Signature-256). Ten workers each verifying HMAC with their own copy of the secret is ten copies of security-critical code drifting apart.
- One place that holds webhook secrets. Same custody argument the org just finished making for the mock lever key (infra#218): a secret with one holder and an audited release path beats a shared string.
- One place that fans events out to consumers. The status worker, the front desk, and whatever comes next should be pure consumers with authenticated inboxes — the same shape
POST /mock already has (bearer-gated), rather than each growing an open one.
Why hooksmith rather than a new thing
Honest framing: hooksmith today is a git hook engine — git → hook-event → policy engine → hook-result → exit, host-native + wasm32-wasip2 components. This is a scope extension, not a wiring task. But the architecture is exactly the right one to extend:
- The core contract already is event stream → pure policy engine → verdict. An HTTP webhook is another event stream; "verify signature, classify, route" is another pure policy given
(headers, body, secret) as inputs.
- The engine's purity (no I/O in
hook_engine) is precisely what makes signature verification and routing decisions testable in isolation — the property that matters most in the one component every external event transits.
- The wasm component build means the same policy engine can embed in a Cloudflare Worker (where the org's receivers live: the broker and status worker precedents) without a second implementation.
What the webhook case adds that the git case never needed: an HTTP listener/host (likely a Worker embedding the component), HMAC verification, secret custody (the broker's /vendor tier or Worker Secrets — see infra/cloudflare/broker), delivery/fan-out with receipts.
First consumers, in value order
- GitHub org webhooks → observed platform health.
workflow_run/workflow_job failure rate across unrelated repos is a leading indicator of platform incidents — during githubstatus qcvjkzcs7j74 (2026-08-06), "job not acquired by hosted runner" was visible in our own runs well before the status page went red. Feed the status layer's splat map (github.actions et al.) with observed health beside vendor-declared health.
- The existing Statuspage poke. Move
POST /webhook/statuspage behind the router; the status worker's inbox shrinks to an authenticated "refresh" call and the last unsigned inbox disappears.
- Front-desk events (issues, PRs, project board) — same routing problem, different consumer.
Non-goals for now
Nothing here is urgent. The Statuspage-direct path shipped in infra#218 is correct until this exists: unsigned poke, payload never parsed, arrival receipts at GET /webhook/last. This issue exists so the "someday" is written down where the code will live, with the constraints already learned (verify-don't-trust, receipts over silence, secrets nobody holds).
Intent (maintainer's, recorded from a working session 2026-08-07)
The org just stood up its service-status layer (
infra/cloudflare/status, live atstatus.bounded.tools), and itsPOST /webhook/statuspageroute is currently its own webhook receiver. That is tolerable only because Statuspage webhooks are unsigned and therefore untrusted by design — the payload is discarded and the event is a mere refresh poke, so there is nothing to verify and nothing to route. The moment a signed webhook source is worth consuming, that excuse expires.What this issue proposes
Extend hooksmith from git hook events to inbound HTTP webhook events, so receiver infrastructure exists once:
X-Hub-Signature-256). Ten workers each verifying HMAC with their own copy of the secret is ten copies of security-critical code drifting apart.POST /mockalready has (bearer-gated), rather than each growing an open one.Why hooksmith rather than a new thing
Honest framing: hooksmith today is a git hook engine —
git → hook-event → policy engine → hook-result → exit, host-native + wasm32-wasip2 components. This is a scope extension, not a wiring task. But the architecture is exactly the right one to extend:(headers, body, secret)as inputs.hook_engine) is precisely what makes signature verification and routing decisions testable in isolation — the property that matters most in the one component every external event transits.What the webhook case adds that the git case never needed: an HTTP listener/host (likely a Worker embedding the component), HMAC verification, secret custody (the broker's
/vendortier or Worker Secrets — seeinfra/cloudflare/broker), delivery/fan-out with receipts.First consumers, in value order
workflow_run/workflow_jobfailure rate across unrelated repos is a leading indicator of platform incidents — during githubstatusqcvjkzcs7j74(2026-08-06), "job not acquired by hosted runner" was visible in our own runs well before the status page went red. Feed the status layer's splat map (github.actionset al.) with observed health beside vendor-declared health.POST /webhook/statuspagebehind the router; the status worker's inbox shrinks to an authenticated "refresh" call and the last unsigned inbox disappears.Non-goals for now
Nothing here is urgent. The Statuspage-direct path shipped in infra#218 is correct until this exists: unsigned poke, payload never parsed, arrival receipts at
GET /webhook/last. This issue exists so the "someday" is written down where the code will live, with the constraints already learned (verify-don't-trust, receipts over silence, secrets nobody holds).