Add headless webhook-service: subscriptions CRUD, HMAC-signed deliveries off SNS/SQS, retries, DLQ, delivery log - #1548
devin-ai-integration[bot] wants to merge 7 commits into
Conversation
…ies off SNS/SQS, retries, DLQ, delivery log - New Go service under services/webhook-service (JSON-only API, pgx + memory stores, SQS consumer, dispatcher with exponential backoff and dead-letter, replay, stats) - Gateway route /api/v1/webhooks, compose service + signed demo sink, LocalStack queues - Headless CI guard, JSON-only smoke test, compose e2e script, CI job
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
Note
This report is out of date. Scroll down for Devin Review's latest report on this PR.
Devin Review found 8 potential issues.
4 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| "/api/v1/admin": c.AdminServiceURL, | ||
| "/api/v1/audit": c.AuditServiceURL, | ||
| "/api/v1/reports": c.ReportServiceURL, | ||
| "/api/v1/webhooks": c.WebhookServiceURL, |
There was a problem hiding this comment.
🔴 Deployed webhook route has no backend
ServiceRoutes registers webhooks, but deployment service lists omit the webhook workload. Every EKS webhook request returns 502.
Learn more
The gateway now exposes the webhook prefix in every environment. Compose defines the backend, but the repository's canonical EKS deploy paths enumerate services explicitly. The dev, spin-up, and tenant lists omit this service, and no webhook Helm chart exists. Kubernetes therefore deploys a gateway that resolves webhook-service:8092 for no matching Service.
Example: A user calls POST /api/v1/webhooks/subscriptions on the main EKS tenant. The gateway selects the new route, DNS cannot resolve webhook-service, and the proxy returns 502 instead of creating a subscription.
Recommended fix: Add a ClusterIP-only webhook Helm chart, image build/push wiring, port mapping, database configuration, gateway URL, and service entries to deploy-dev.sh, spinup-dev.sh, and tenant deployment helpers. Keep SNS/SQS consumers disabled for tenants as required by AGENTS.md, or define an isolated event path before enabling them.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Acknowledged, not addressed in this PR. The request was scoped to a localhost/compose demo; the EKS path (Helm chart, ECR build, deploy-dev/spinup/tenant lists, and a per-tenant event path given SNS/SQS is disabled for tenants per AGENTS.md) is a separate piece of work. Until then the route behaves like any other not-yet-deployed backend (gateway 502). Flagged to the requester as a follow-up decision.
| # Local demo only: lets subscriptions target the in-compose sink / localhost. | ||
| ALLOW_PRIVATE_TARGETS: "true" |
There was a problem hiding this comment.
There was a problem hiding this comment.
Kept, intentionally: the compose stack is the localhost demo and the signed sink lives on the compose network, so ALLOW_PRIVATE_TARGETS=true is required there. The service default is false (private/loopback/link-local targets rejected), and the compose line is commented as local-demo-only. Not set anywhere outside docker-compose.yml.
… timeout-derived lease, filter validation, gateway-only exposure
…ecret fingerprint
- gateway strips caller-supplied X-User-ID before setting it from the JWT - secret rotation is its own store op; ordinary updates never write secret - DLQ record carries the final attempt's count/status/error - /ready also checks the events queue when the consumer is enabled - custom secrets must be >= 32 chars - document_deleted events include owner_id so they fan out - outbound dialer verifies the connected peer is not private
…ed owner id; gateway: stop forwarding caller Host
Summary
Adds
services/webhook-service: a headless (JSON-only) Go service that lets API users register outbound webhooks and receive HMAC-signed deliveries for events already flowing over the LocalStack SNS topicotterworks-events. Replaces the "poll/filesand/documents" pattern; nothing innotification-serviceis touched.Data flow
Signing —
X-OtterWorks-Signature: t=<unix>,v1=hex(hmac_sha256(secret, "<unix>.<raw-body>")), plusX-OtterWorks-Event,-Event-ID,-Delivery-ID. Secrets (whsec_…) are returned only on create /rotate-secret; every other read omits them. Redirects are not followed; non-https / private / localhost targets are rejected unlessALLOW_PRIVATE_TARGETS=true(compose sets it for the demo only; the service default rejects them).API (gateway →
/api/v1/webhooks, owner =X-User-IDfrom the JWT; all ownership-scoped)404/405/unknown routes/errors are JSON too;
requireUUID404s malformed ids andfilterFrom400s malformedsubscriptionId/cursorbefore they reach Postgres.Store —
store.Storeinterface withPostgres(pgx,SELECT … FOR UPDATE SKIP LOCKED+ lease ofDELIVERY_TIMEOUT_SECONDS+30sfor due deliveries; migrations applied at boot) andMemory(tests,DATABASE_URL=memoryfor the CI smoke).Wiring — gateway
WEBHOOK_SERVICE_URL+ route;scripts/localstack-init.shcreates the two queues and the SNS→SQS subscription;docker-compose.ymladdswebhook-service(not host-published — it trustsX-User-ID, so it is reachable only via the gateway) andwebhook-sink(a headless signature-verifying receiver with/hook,/flaky(503,503,200),/fail,/received).Proving it's headless
scripts/headless-guard.sh(runs in CI andmake lint): fails on any.html/.css/.js/.ts, template dirs/html/template, static/public/assets dirs, frontend toolchain files,http.FileServer/ServeFile,//go:embed,text/htmlcontent types or HTML tags.scripts/smoke-json-only.sh: hits every route (incl./index.html,/static/app.js, wrong methods, browserAccept: text/html) and asserts JSON content-type, JSON object body, no HTML, no redirects. CI runs it against a live memory-backed instance; the CI job also builds the image and asserts the container ships exactly/app/server.scripts/e2e-compose.sh(make e2e-webhook): full localhost demo — CRUD through the gateway, SNS publish, 3 signed deliveries (ok / flaky-with-retries / dead-lettered), attempt log, stats, DLQ message, replay, gateway JSON smoke.Verification
go vet,go test -race ./...,staticcheck, headless guard, Docker build and the full compose e2e pass locally; the e2e run log is attached to the Slack thread. Tail of the run:Note: README already claims webhooks exist; it's left as-is since this PR makes it true.
Not in this PR: EKS wiring (Helm chart, ECR build,
deploy-dev.sh/spinup-dev.sh/tenant lists). The route 502s on EKS like any not-yet-deployed backend until that lands; tenants also have SNS/SQS disabled per AGENTS.md, so the event path there needs its own design.Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/7ce0574f3d474cda84d608d428a70941
Open in Devin Desktop: https://partner-workshops.devinenterprise.com/desktop/session/7ce0574f3d474cda84d608d428a70941?variant=devin
Requested by: @mbatchelor81