feat: add provider-specific webhook ingestion endpoints - #822
Merged
Conversation
Register POST /api/webhooks/stripe and /api/webhooks/generic with provider- specific HMAC verification (internal/middleware/webhook_verification.go) and persist verified events into the outbox repository. - Secrets are resolved exclusively through secrets.Provider; no raw env reads. - Stripe event IDs are derived from the payload and replays are rejected via webhook_event_cache keyed by event_id (defense in depth atop the UNIQUE deduplication_id index). - Handler tests rewritten against the current outbox.Repository interface; EventIDCache regression tests made deterministic. Closes Stellabill#780
Contributor
Author
|
Alot of file changes were made because some other things on upstream are broken affecting my implementation |
supreme2580
had a problem deploying
to
preview-822
August 31, 2026 14:59 — with
GitHub Actions
Failure
thlpkee20-wq
had a problem deploying
to
preview-822
August 31, 2026 16:23 — with
GitHub Actions
Error
5 tasks
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.
Closes #780
Summary
Adds provider-specific webhook ingestion routes (
POST /api/webhooks/stripe,POST /api/webhooks/generic) that apply provider-scoped HMAC verification and persist verified events into the outbox. The legacyPOST /webhooksendpoint remains registered for backward compatibility.Acceptance criteria → code/tests
Routes registered across the requested files
internal/routes/routes.go:/api/webhooksgroup registers/stripeand/genericviawebhookVerificationFor(...); each resolves its provider config throughmiddleware.ProviderConfig+middleware.WebhookVerificationMiddleware(HMAC-SHA256/384/512, 5-min tolerance,DefaultMaxBodySizecap, Stripe compositet=...,v1=...support). LegacyPOST /webhookspreserved as-is.internal/middleware/webhook_verification.go+internal/middleware/webhook_event_cache.go: existing primitives used; replay cache now a first-class part of the flow (see below).internal/outbox/repository.go:NewPostgresRepositorypersists the raw event viaStore(ctx, event)(the DB UNIQUE index ondeduplication_idis the durable replay authority).503 outbox_unavailableinstead of panicking.Security, authorization, validation, data integrity
secrets.Provider(Vault-first chain with env provider inside the abstraction). No rawos.Getenvreads for provider secrets —webhookVerificationFordrops the previous raw-env fallback. Missing secret ⇒ the route is registered to reject every request with403 webhook_secret_not_configured(fail closed, never a placeholder secret).DefaultMaxBodySize(5MB) ⇒ 413.X-Webhook-Event-Id) or, for Stripe (which signs timestamp+payload and sends the id in the body), the payloadid/event_idfield. Duplicates are rejected by the in-memoryEventIDCachebefore persistence and additionally by the DB unique constraint — both layers tested.handlers/webhooks.gonow derives the event id from the payload for providers that don't send an id header, builds the outbox event withdeduplication_id = event_id, and acknowledges idempotently on replay.Failure/retry/concurrency/boundary behavior
TestEventIDCache_SimultaneousWritescovers the race.Regression coverage (empty, invalid, duplicate, boundary)
internal/middleware/webhook_verification_test.go:TestEventIDCachemade deterministic (removed duplicatedLensubtests with wrong accumulated-count assertions) and extended with the empty-event-id boundary; existing wrong-sig / stale-timestamp / oversized / replay cases retained.internal/handlers/webhooks_test.go: rewritten against the realoutbox.Repositoryinterface — happy path (header + Stripe payload event id), replay dedup (Store called once), missing tracking identifiers, missing provider, invalid JSON, store-error idempotent ack, nil-store 503.Compatibility / migration
deduplication_idcolumn + partial UNIQUE index already exist (migrations/0009_add_outbox_deduplication.up.sql).POST /webhooks(sharedWEBHOOK_SECRET) is unchanged.Verification
go build ./internal/routes/... ./internal/handlers/... ./internal/middleware/...✔go test ./internal/handlers -run WebhookHandler✔go test ./internal/middleware -run 'Webhook|EventID'✔tools/slo-lintmixed packages; several pre-existing broken test files ininternal/handlers,internal/middleware,internal/repository); per scope they are left untouched.go test ./...in full is not green on the baseline.