Repository navigation
Outbound webhooks: endpoints, a versioned event catalog, signed delivery with retries - #306
Merged
Merged
Conversation
nambok
force-pushed
the
feat/outbound-webhooks
branch
from
October 26, 2026 17:24
0dcdd60 to
6d27b16
Compare
nambok
force-pushed
the
feat/outbound-webhooks
branch
3 times, most recently
from
October 26, 2026 21:13
58007e5 to
7f7cdfb
Compare
…ery with retries Teams can now get CoderHelm's events on their own systems. Admins add webhook endpoints in Settings, Webhooks or through the API (scope `webhooks`); CoderHelm signs each request the Standard Webhooks way and records every attempt in Deliveries. - common::webhooks: the event catalog (api_version 2026-10-26), emit_event (queues only), endpoints with KMS-sealed secrets and rotation overlap, Standard Webhooks signing, address checks at save and at every send (pinned to the checked address), retries for about a day, turning off an endpoint after 5 days of failures. - Events emitted from the real code paths: run.created/completed/failed/ needs_input, pull_request.opened, review.completed, ci_fix.started/ completed, alert.received, log_finding.created/resolved (log analyzer), release_notes.published, plan.approved, plugin.run.completed. - Gateway: /api/webhooks routes (endpoints, secret reveal and rotate, test, deliveries, resend, replay), in permissions and the OpenAPI document with the events as OpenAPI 3.1 webhooks. Inbound webhook routes move to the "Incoming webhooks" tag. Deliveries can resend webhook deliveries. - Worker: webhook_emit and webhook_send, admin email and chat note when an endpoint is turned off. - Infra: a webhooks queue wired to the worker; gateway, streaming and the log analyzer can send to it; the webhook-disabled email template.
…h safeguard - Events are scrubbed before they are kept or sent: tokens by prefix, the word after Bearer/token, passwords in addresses, key=value pairs that name a secret, private key blocks; long text is cut. The log analyzer's events are scrubbed in the worker too, and its titles on its side. - An endpoint's answer is read up to 4 KB and never parsed. - Tests: secrets come from the CSPRNG and look random, no log call prints a secret, every endpoint route is admin-only and scoped to the caller's team, outbound webhook routes have no inbound path, IPv6 private and mapped addresses are refused, and a huge answer is read only to the cap.
… are Webhook endpoints The docs keep one webhooks area: the reference pages of the webhooks CoderHelm receives stay at their published addresses, and the routes that manage outbound endpoints get their own tag beside them.
…ted app The catalog's examples are shown to self-hosted installs too, and hosted addresses don't belong in shipped code.
nambok
force-pushed
the
feat/outbound-webhooks
branch
from
October 26, 2026 22:33
7f7cdfb to
4eccb0d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Outbound webhooks: CoderHelm now tells a team's own systems what it did. Admins add webhook endpoints (Settings, Webhooks, or the API with the
webhooksscope), pick events, and CoderHelm sends each event signed the Standard Webhooks way, retries for about a day, turns off an endpoint after 5 days of failures (and tells admins), and records every attempt in Deliveries.Paired with CoderHelm/coderhelm#223 (settings page, Deliveries changes, and the rewritten https://docs.coderhelm.com/api/webhooks/).
Changes
common::webhooks(new)catalog: the versioned event catalog (api_version2026-10-26), each event'sdataschema and example. Only events with a real emit point are listed; a test reads the sources and fails if a catalog event has no emit.event: the envelope{id, type, created_at, team_id, api_version, data}andemit_event, which only queues on the new webhooks queue so the work never waits on endpoints. Events are kept 14 days for Resend and replay; event ids are idempotency keys (a duplicate queue message is sent once; SNS redeliveries map to the same id).endpoint: endpoints on the settings table, signing secret sealed with KMS (bound to team, endpoint and purpose), rotation with an overlap window (default 24h, max 168h) during which both secrets sign; health kept with conditional updates.sign:webhook-id,webhook-timestamp,webhook-signature: v1,<base64 HMAC-SHA256 of id.timestamp.body>, tested against the Standard Webhooks test vector.deliver: https only; the host is resolved before every send and every address must be public (private, loopback, link-local, CGNAT, metadata refused), the connection is pinned to the checked address (no DNS rebinding), redirects not followed, 10s timeout. Retries on timeouts, connection failures, 408, 429 and 5xx with backoff 5s, 1m, 5m, 30m, 1h, 3h, 6h, 6h, 8h (about 24.6h); other 4xx and 3xx are final. 5 days of failures with no success turns the endpoint off.endpoint_idand sourcewebhook, aWebhookredeliver kind and plain reasons with fixes; the Deliveries page's Redeliver resends webhook deliveries.emithelper per service):run.created,run.completed,run.failed,run.needs_input(every status write, read back from the run record),pull_request.opened,review.completed(verdict and how it was posted),ci_fix.started/ci_fix.completed(pushed, no_changes, failed),alert.received,log_finding.created/log_finding.resolved(the Python log analyzer queues them in the same envelope),release_notes.published,plan.approved,plugin.run.completed(behind the plugins flag). Pluswebhook.test./api/webhooks/*: event types; endpoints list, create (secret shown once), get, update, delete; secret reveal and rotate (audited); send test event; endpoint deliveries; resend; replay a time range (only-failed option, 500 cap). All inpermissions.rsunder a new admin capabilitywebhooks, public for API keys, with OpenAPI schemas and examples. The address is checked after DNS at save time too.webhooksentry. The endpoint routes are tagged "Webhook endpoints"; the received webhooks keep the "Webhooks" tag, so their reference page URLs don't change.webhook_emit(keep, fan out, first attempt) andwebhook_send(retries; waits longer than SQS allows are carried bydue_at). Admins get an email (newwebhook-disabledSES template) and the team's alert chat channel a message when an endpoint is turned off.webhook-ids.key=valuesecrets, private key blocks) and cut, for the log analyzer's events too./webhooksrouter (tested).<prefix>-webhooksqueue (DLQ, 16 min visibility), worker event source (batch 1, max concurrency 5), send grants andWEBHOOK_QUEUE_URLfor gateway, worker, streaming and the log analyzer; the email template.Verification
cargo fmt --all --check: clean.cargo clippy --workspace --all-targets -- -D warnings: clean.cargo test --workspace(rebased on main): common 1025 passed, gateway 471 passed, worker 688 passed, 0 failed. New tests cover the Standard Webhooks vector, multi-signature rotation overlap, timestamp tolerance, retry classes and the backoff total, disable after 5 days, SSRF at save and send (a name that rebinds to 169.254.169.254, a mixed public and private answer, literal private IPs never reaching DNS), real HTTP sends against a loopback server (headers verify with both secrets, a timeout retries, a 503 is recorded), secret sealing bound to team and endpoint, replay range limits, and each emit'sdataagainst the catalog schema. The OpenAPI document is regenerated and its tests pass.python3 -m unittest discover -s lambda/tests: 113 tests OK (envelope, uppercase ULID ids, failures never raise).npm testin infra: 61 pass, 0 fail (new: queue, event source, grants, env, email template). Self-hostcdk synth --all -c config=config/selfhost.example.json: exit 0, no hosted domain in any template.https://coderhelm.example.com/…; PR Self-host: every link points at the install, never at coderhelm.com #312'sno_hosted_urlslint, run against this branch, flags nothing in the webhook code.