feat: headless webhook-service for outbound partner webhooks - #1547
Open
devin-ai-integration[bot] wants to merge 12 commits into
Open
devin-ai-integration[bot] wants to merge 12 commits into
devin-ai-integration[bot] wants to merge 12 commits into
Conversation
Contributor
Author
🤖 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:
|
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
Adds a headless outbound partner-webhook capability:
services/webhook-service(Go 1.22 / Chi, port 8092) plus awebhook-sinkdemo receiver (port 8093), wired into the gateway, compose, LocalStack init, Makefile, CI and docs. The service has no UI surface at all —openapi.yamlis the only interface contract.How it works
X-User-ID; the service scopes every subscription/delivery query to that owner (401 if the header is missing).webhook-service-eventsis subscribed to the existingotterworks-eventsSNS topic (scripts/localstack-init.sh). The consumer maps bus events → webhook events (file_shared→file.shared,document_updated→document.updated,comment_added→comment.added), accepting bothevent_type(Python/Kotlin publishers) andeventType(Rust file-service). Unknown/unparseable events are acked and dropped; a message is acked only after every matching delivery row is durably inserted (otherwise SQS redelivers), and(subscription_id, source_message_id)is unique so redelivery never double-fans-out.webhook(subscriptions,deliveries,delivery_attempts), golang-migrate migrations embedded and applied on boot. Secrets arewhsec_<32 random bytes hex>, stored for signing, returned only onPOST /subscriptions.UPDATE … FROM (SELECT … FOR UPDATE SKIP LOCKED) … SET next_attempt_at = now() + lease RETURNING id) so an in-flight delivery can't be double-sent, POSTs{id, event, created_at, data}withX-OtterWorks-Signature: sha256=HMAC_SHA256(secret, "<unix_ts>.<body>"),X-OtterWorks-Event,X-OtterWorks-Delivery-Id,X-OtterWorks-Timestamp; every attempt is recorded; non-2xx/transport errors retry with backoff2s·2^(n-1)(2,4,8,16s) up to 5 attempts, thenstatus=failed(dead-letter).target_urlmust be http/https and (unlessALLOW_PRIVATE_TARGETS=true) must not be localhost or resolve to loopback/private/link-local/metadata IPs; the worker's dialer re-checks resolved IPs at connect time and never follows redirects. Compose setsALLOW_PRIVATE_TARGETS=truebecause the demo sink lives on the compose network.Content-Type: application/jsonon every response, strips anySet-Cookie, returns JSON 406 whenAcceptis HTML-only, JSON 404/405 for unknown routes/methods.WEBHOOK_SECRETis set (5-min timestamp tolerance),GET /receivedreturns the last 500 items,FAIL_MODE=1returns 500 while still recording.Wiring:
WebhookServiceURL+/api/v1/webhooksroute and metrics normalizer in api-gateway;webhook-service/webhook-sinkservices indocker-compose.yml(sink takesWEBHOOK_SINK_FAIL_MODE/WEBHOOK_SINK_SECRET);webhook-service+ always-onwebhook-headless-proofCI jobs;make test/make lint/make demo-webhooks/make assert-headless; ARCHITECTURE.md section; service README.Config knobs (env):
DELIVERY_MAX_ATTEMPTS=5,DELIVERY_BASE_BACKOFF=2s,DELIVERY_TIMEOUT=5s,DELIVERY_CLAIM_LEASE=60s,ALLOW_PRIVATE_TARGETS=false,WORKER_POLL_INTERVAL=1s,SQS_ENABLED,SQS_QUEUE_URL.Headless proof
BASE_REF=origin/main scripts/assert-headless.sh:Smoke tests (
go test -run 'Headless|Contract|OpenAPI' -v ./...— walks every registered route assertingapplication/json, noSet-Cookie, valid JSON body;GET /,GET /nope→ 404; HTML-onlyAccept→ 406; OpenAPI paths == registered routes):Live smoke against the running stack:
No
Set-Cookieheader appeared in any response.openapi-spec-validator services/webhook-service/openapi.yamlpasses (also run in CI).Local demo (
make upthen./scripts/demo-webhooks.sh, run against the local stack)Excerpt (full transcript is 746 lines; repeated poll iterations and duplicate sink dumps elided):
Notes on the transcript: the sink container persists across demo re-runs, so
/receivedalso listsfile.shareditems from earlier subscriptions whose secret differs from the one the sink was restarted with — those correctly showsignature_valid: false; the current run's ping andfile.sharedboth verifytrue. The seededadmin@otterworks.devlogin returns Invalid credentials on this stack (untouched — possibly planted), so the script falls back to registeringwebhook-demo@otterworks.dev.Verification
go vet ./... && go test -race ./...,golangci-lint run— service and sink: pass.WEBHOOK_TEST_DATABASE_URL=… go test ./internal/store/): migrations idempotent, lease prevents immediate re-claim, attempt/retry/failed/delivered lifecycle — pass.go test ./...pass;docker compose configvalid; both images build.make test: API Gateway, Auth, File Service sections pass, then stops at Document Service withpytest: command not found— identical failure onorigin/mainin this environment (tooling gap, not this branch).make lint: stops at pre-existing api-gatewayerrcheckfindings (internal/health/handler.go,internal/middleware/jwt.go,cors_test.go) — identical onorigin/main; not fixed here per the "don't modify existing services beyond wiring" constraint. Webhook-service and sink lint clean.deploy-dev.shentry forwebhook-service(tenant eventing is disabled by policy anyway); the gateway route is inert on EKS until that is added.frontend/**, any planted bug, any existing service beyond the gateway route/metrics + compose/CI wiring.Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/f9bb76780f104722ae9f20e1b0e11adb
Open in Devin Desktop: https://partner-workshops.devinenterprise.com/desktop/session/f9bb76780f104722ae9f20e1b0e11adb?variant=devin
Requested by: @mbatchelor81